:root {
  --bg-canvas: #F5F2EB;
  --bg-surface: #FFFFFF;
  --bg-surface-raised: #FBFAF7;
  --text-primary: #17161A;
  --text-secondary: #6C6A73;
  --text-tertiary: #736E5F;
  --accent: #D8452B;
  --accent-text: #BE3A22;
  --accent-quiet: #FBE7E1;
  --hairline: #E2DCD0;
  /* In the app --ink is fixed in both modes, because it paints the icon's
     glyph on a plate that never changes. On the web the mark sits on the
     page's own canvas, so it follows the theme instead. Deliberate
     difference, matching the token spec's own wordmark-without-plate rule. */
  --ink: #17161A;
  /* The token spec's `raised` elevation. It carries a colour, so it is a token
     and it exists in all three theme blocks. The dark value is not the light
     one: six percent black under a #1C1B20 tile on a #141317 canvas does
     nothing at all, and a tile that reads as raised in one theme and flat in
     the other is not the same design twice. */
  --elevation-raised: 0 1px 2px rgba(0, 0, 0, 0.06);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg-canvas: #141317;
    --bg-surface: #1C1B20;
    --bg-surface-raised: #23222A;
    --text-primary: #F3F0E8;
    --text-secondary: #9A97A0;
    --text-tertiary: #8A8792;
    --accent: #F06A4C;
    --accent-text: #F06A4C;
    --accent-quiet: #2A1C19;
    --hairline: #2A2930;
    --ink: #F3F0E8;
    --elevation-raised: 0 1px 2px rgba(0, 0, 0, 0.5);
  }
}

:root[data-theme="dark"] {
  --bg-canvas: #141317;
  --bg-surface: #1C1B20;
  --bg-surface-raised: #23222A;
  --text-primary: #F3F0E8;
  --text-secondary: #9A97A0;
  --text-tertiary: #8A8792;
  --accent: #F06A4C;
  --accent-text: #F06A4C;
  --accent-quiet: #2A1C19;
  --hairline: #2A2930;
  --ink: #F3F0E8;
  --elevation-raised: 0 1px 2px rgba(0, 0, 0, 0.5);
}

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

body {
  margin: 0;
  background: var(--bg-canvas);
  color: var(--text-primary);
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
  font-size: 17px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

@font-face {
  font-family: "Outfit";
  src: url("assets/fonts/outfit-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "IBM Plex Mono";
  src: url("assets/fonts/plexmono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --font-display: "Outfit", -apple-system, system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  /* The radius ladder from the token spec's section 4, named rather than
     retyped. A raw px radius in a component rule is how a fifth corner is
     born, which is the same argument the colour check makes one property
     over: the page's only filled control had drifted to 12. */
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --radius-xl: 22px;
}

.eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin: 0;
}

.wordmark {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.035em;
}

.hero { padding: 96px 24px 48px; max-width: 1120px; margin: 0 auto; }

.hero-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(34px, 5.2vw, 60px);
  line-height: 1.06;
  letter-spacing: -0.035em;
  margin: 32px 0 24px;
  text-wrap: balance;
}

.hero-sentence { display: block; }

.promise { display: inline-grid; vertical-align: top; }

.promise-phrase {
  grid-area: 1 / 1;
  color: var(--accent-text);
  opacity: 0;
  transform: translateY(6px);
  /* 240ms out, then 240ms in, and the two never run together. A phrase that is
     neither current nor leaving is simply not painted. */
  transition: opacity 240ms ease, transform 240ms ease;
}

.promise-phrase.is-current {
  opacity: 1;
  transform: translateY(0);
}

.promise-phrase.is-leaving {
  opacity: 0;
  transform: translateY(-6px);
}

.hero-fixed {
  max-width: 46ch;
  font-size: 19px;
  color: var(--text-secondary);
  margin: 0;
}

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

/* The download section sits directly under the hero and shares its own
   content width with the bento below, so the whole page reads as one column
   of aligned edges rather than three sections each choosing their own. */
.download {
  width: min(100% - 48px, 1072px);
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px 24px;
}

.store-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/* Until a store link exists this host renders no buttons, and an empty flex
   ITEM is still an item: it took the row's 24 px column gap and pushed the
   holding line 24 px right of the single column every other block on the page
   shares (measured at 1280: everything at 104, the holding line at 128). */
.store-buttons:empty { display: none; }

.store-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 16px 24px;
  border-radius: var(--radius-md);
  background: var(--ink);
  color: var(--bg-canvas);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -0.01em;
  text-decoration: none;
}

.store-holding,
.store-note {
  margin: 0;
  font-size: 15px;
  color: var(--text-secondary);
}

/* Same width formula as the hero's content and as the grate drawn on this
   grid's top edge, so the three share an edge at every viewport width without
   anybody measuring anything. */
.bento {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  /* Rows 40 apart, columns 16. The row gap is not decoration: it is where the
     cursor's segment and its dot sit for every row below the first, with the
     same clearance the first row gets from the grate above it. */
  gap: 40px 16px;
  width: min(100% - 48px, 1072px);
  margin: 48px auto;
}

@media (max-width: 900px) {
  .bento { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 560px) {
  .bento { grid-template-columns: 1fr; }
}

.tile {
  background: var(--bg-surface);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-raised);
  padding: 32px;
  margin: 0;
  grid-column: span 1;
}

/* THE TILES ANSWER. Hovering one (or scrolling it into the middle of the
   window on a phone) moves the dot over it and lights it: the grate stops
   being a picture and becomes the track a pointer runs along, which is also
   the product's own gesture, sift and then show me what is current. */
.tile {
  transition: border-color 260ms ease, background-color 260ms ease, transform 260ms ease;
}

.tile.is-active {
  border-color: var(--accent);
  background: var(--accent-quiet);
}

.tile[data-span="2"] { grid-column: span 2; }

@media (max-width: 560px) {
  .tile[data-span="2"] { grid-column: span 1; }
}

.tile h2 {
  font-family: var(--font-display);
  font-size: 21px;
  line-height: 1.2;
  margin: 0 0 8px;
}

.tile p {
  font-size: 16px;
  color: var(--text-secondary);
  margin: 0;
}

/* ONE grate component, two roles, and it never travels between them.

   Role 1, `--hero`: the playful one. Tasks fall, three of them are stopped by
   the bars, one finds the gap and comes out the other side in vermilion. It
   loops while it is on screen and it is the first thing on the page, because
   the product is named after what it is doing.

   Role 2, `--grid`: the same bars drawn on the bento's own top edge, still.

   Scrolling cross-dissolves role 1 into role 2 (see main.js). An earlier build
   MOVED one set of bars between the two positions, and since the headline, the
   promise and the download line all sit between them, every frame of that
   journey was drawn across the copy. A dissolve has no path, so there is
   nothing in its way, and the idea survives: the grate you played with becomes
   the edge the grid is built on.

   Horizontal geometry is the icon's, from the token spec's table, expressed as
   fractions of the grate's own span (x168 to x856 on the 1024 grid, so 688):
     left bar   x168 w192  ->  left 0,        width 192/688
     mid bar    x410 w204  ->  left 242/688,  width 204/688
     right bar  x664 w192  ->  left 496/688,  width 192/688
     gap        360 to 410, centre 385  ->  217/688 = 31.541%
   The dot falls at --gap-center, so the gap's centre is read off the bars
   rather than nudged to meet them. The extra decimals are the fraction's own. */
.grate {
  --bar-h: 14px;
  --gap-center: 31.541%;
  position: relative;
}

.grate--hero {
  height: 168px;
  width: min(76vw, 420px);
  margin-bottom: 8px;
  transform-origin: 20% 60%;
}

/* The wrap exists so the cursor has a positioning context that spans every row
   of the grid, not just its top edge. */
.bento-wrap { position: relative; padding-top: 40px; }

.mark-bar {
  position: absolute;
  top: 50%;
  height: var(--bar-h);
  border-radius: calc(var(--bar-h) / 2);
  background: var(--ink);
}

.mark-bar[data-bar="left"]  { left: 0;        width: 27.907%; }
.mark-bar[data-bar="mid"]   { left: 35.174%;  width: 29.651%; }
.mark-bar[data-bar="right"] { left: 72.093%;  width: 27.907%; }

/* The falling tasks. Each carries its own column and its own delay, so the
   four of them read as a handful of things arriving rather than as one thing
   repeated: --x is where it falls, --wait is when it starts, and --stop is how
   far it gets. Three are stopped by the bars. The fourth is at the gap. */
.mark-task {
  position: absolute;
  top: 0;
  left: var(--x);
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--ink);
  opacity: 0;
  /* Centred on its column with a transform rather than a negative margin: the
     margin would be half of an odd width, which is off the 4 pt grid the token
     spec sets and which check.mjs enforces. Every keyframe below therefore
     carries the -50% itself, because a transform in a keyframe replaces this
     one rather than adding to it. */
  transform: translateX(-50%);
}

.mark-sifted {
  position: absolute;
  top: 50%;
  left: var(--gap-center);
  width: 17px;
  height: 17px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0;
  transform: translateX(-50%);
}

/* Blocked: falls, meets the bar, settles with a small bounce and fades. */
@keyframes task-blocked {
  0%      { transform: translate(-50%, -40px); opacity: 0; }
  12%     { opacity: 1; }
  44%     { transform: translate(-50%, var(--stop)); opacity: 1; }
  52%     { transform: translate(-50%, calc(var(--stop) - 9px)); }
  60%     { transform: translate(-50%, var(--stop)); }
  86%     { transform: translate(-50%, var(--stop)); opacity: 1; }
  100%    { transform: translate(-50%, var(--stop)); opacity: 0; }
}

/* Through: ONE dot, which changes colour as it passes the bars. It used to be
   two elements, an ink one falling and an accent one arriving underneath, and
   David saw exactly what that is: both of them on screen at once. The sift is
   one thing happening to one task, so it is one element, and the colour flips
   at the frame it clears the grate. */
@keyframes task-through {
  0%      { transform: translate(-50%, -40px); opacity: 0; background: var(--ink); }
  12%     { opacity: 1; background: var(--ink); }
  46%     { transform: translate(-50%, 58px); background: var(--ink); }
  50%     { transform: translate(-50%, 70px); background: var(--accent); }
  64%     { transform: translate(-50%, 104px); opacity: 1; background: var(--accent); }
  88%     { transform: translate(-50%, 104px); opacity: 1; background: var(--accent); }
  100%    { transform: translate(-50%, 104px); opacity: 0; background: var(--accent); }
}

.grate--hero.is-playing .mark-task {
  animation: task-blocked 4200ms cubic-bezier(.45,0,.9,.5) var(--wait) infinite;
}

.grate--hero.is-playing .mark-task[data-through] {
  animation-name: task-through;
}

/* THE CURSOR. A vermilion segment exactly as wide as the tile it points at,
   with the sifted dot centred above it, moving in both axes: along the grate
   for the first row, and down into the row gap for the second. The segment is
   what makes the dot's travel mean anything, which is David's note on build
   four: a dot on its own only moves, a dot with a segment under it MARKS.

   It sits in .bento-wrap and its transform is written by main.js in the wrap's
   own coordinates, so a tile in any row is reachable. */
.tile-cursor {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--cursor-w, 0px);
  height: 8px;
  transform: translate(0, 0);
  transition: transform 420ms cubic-bezier(.2,.8,.25,1), width 420ms cubic-bezier(.2,.8,.25,1);
  pointer-events: none;
}

.cursor-bar {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-sm);
  background: var(--accent);
  opacity: 0;
  transition: opacity 260ms ease;
}

.tile-cursor.is-marking .cursor-bar { opacity: 1; }

/* Centred in the 24 px between the segment and the tile: 6.5 above the dot and
   6.5 below it. It used to hang above the line, touching it, which David read
   correctly as stuck to the bar rather than placed in the space. */
.tile-cursor .mark-sifted {
  width: 11px;
  height: 11px;
  top: calc(100% + 6.5px);
  left: 50%;
  opacity: 0;
}

/* The arrival. The dot drops onto the first tile it marks, once, the way it
   fell through the gap in the hero: that is the only remaining thread between
   the two halves of the page now that the ink grate above the grid is gone. */
.tile-cursor.is-arriving .mark-sifted {
  animation: grid-sifted 900ms cubic-bezier(.45,0,.55,1) forwards;
}

@keyframes grid-sifted {
  0%   { opacity: 0; transform: translate(-50%, -40px); }
  25%  { opacity: 1; transform: translate(-50%, -22px); }
  70%  { opacity: 1; transform: translate(-50%, 0); }
  84%  { opacity: 1; transform: translate(-50%, -6px); }
  100% { opacity: 1; transform: translate(-50%, 0); }
}

/* Reduced motion keeps the picture and drops the play: a grate with one item
   already through it, which is the icon, and a grid whose edge is simply
   drawn. */
@media (prefers-reduced-motion: reduce) {
  .grate--hero.is-playing .mark-task { animation: none; }
  .grate--hero .mark-task { opacity: 0; }
  .grate--hero .mark-task[data-through] {
    opacity: 1;
    background: var(--accent);
    transform: translate(-50%, 104px);
  }
      .tile-cursor { transition: none; }
  .tile-cursor .mark-sifted { opacity: 1; transform: translate(-50%, 0); animation: none; }
  .tile-cursor .cursor-bar { opacity: 1; }
}

/* The sub-pages carry the mark as markup instead of as motion. They never
   load main.js, so there is nothing to animate and nothing to build: the bars
   are simply already where the landing page's sift leaves them. Same geometry
   as the stage above, same tokens, at a size that fits beside the wordmark.
   The falling task is absent for the same reason the small icon drops it: at
   this height a moving dot would be a smudge, while the grate and the sifted
   dot still read. */
.mark-static {
  --bar-h: 6px;
  --gap-center: 31.541%;
  position: relative;
  width: 92px;
  height: 30px;
  flex: none;
}

.mark-static .mark-bar    { top: 34%; }
.mark-static .mark-sifted { top: 66%; width: 9px; height: 9px; opacity: 1; }

/* Shared shell for privacy and support: one column, no motion, the same
   gutter and the same maximum content width the landing page's sections use,
   so the three pages agree about where the page's edge is. */
.site-head {
  width: min(100% - 48px, 1072px);
  margin: 0 auto;
  padding: 32px 0 8px;
}

/* The lockup gap is the one spacing the token spec states as a formula rather
   than as a step: mark to word = 0.5 x cap-height. Outfit's cap-height is
   0.7 em, so at this 24 px wordmark that is 8.4 px, and 8 is the step it lands
   on. It used to be 14, which is not on the scale at all. */
.site-head a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text-primary);
  text-decoration: none;
  font-size: 24px;
}

.doc {
  width: min(100% - 48px, 680px);
  margin: 0 auto;
  padding: 24px 0 48px;
}

.doc h1 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(32px, 6vw, 44px);
  line-height: 1.1;
  letter-spacing: -0.035em;
  margin: 12px 0 16px;
}

.doc h2 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 21px;
  line-height: 1.25;
  letter-spacing: -0.02em;
  margin: 32px 0 8px;
}

.doc .lead { font-size: 19px; margin: 0 0 8px; }

.doc p, .doc li { color: var(--text-secondary); }

.doc p { margin: 0 0 12px; }

.doc ul { margin: 0 0 12px; padding-left: 24px; }

.doc li { margin-bottom: 8px; }

.doc a { color: var(--accent-text); }

.doc .meta {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-tertiary);
}

.site-foot {
  width: min(100% - 48px, 1072px);
  margin: 0 auto;
  padding: 32px 0 48px;
  border-top: 1px solid var(--hairline);
}

.foot-line {
  margin: 0 0 12px;
  max-width: 62ch;
  font-size: 15px;
  color: var(--text-secondary);
}

.foot-links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 24px;
  font-family: var(--font-mono);
  font-size: 13px;
}

.foot-links a { color: var(--text-tertiary); }

.foot-links a:hover { color: var(--text-primary); }

/* Section 9 of the spec: focus is visible everywhere and never removed, and
   it is drawn in the accent so it is the one thing on the page that cannot be
   mistaken for decoration. */
a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* border-radius does NOT round the outline, it reshapes the focused element's
   own box and the outline then follows it. Declared on every anchor, it snapped
   the store button's corners from 10 to 3 the moment it took keyboard focus,
   which reads as a rendering glitch rather than as the one affordance section 9
   says must be unmistakable. So a control keeps its own radius and only the
   inline text links, which have no corners of their own, ask for one. */
.doc a:focus-visible,
.foot-links a:focus-visible { border-radius: var(--radius-sm); }
