/* ============================================================================
   Valutorio — dark violet edition (v4)
   One stylesheet. The only JavaScript on the page is a 5-line reduced-motion
   guard for the hero video.

   WHY THIS PALETTE: the shipped app icon is purple 3D cubes on a near-black
   rounded tile. v2 used warm orange ink inherited from Editorio, which is a
   different product; a brand cannot use a colour its own icon does not contain.
   Black dominates, violet accents. See plans/DECISIONS.md D-23.

   Execution guardrails that keep purple from reading as generic AI-design
   (D-21, still in force): no cross-hue gradients (violet never runs into blue
   or pink), no glassmorphism and no blurred translucency, no gradient headline text.
   Single-hue violet light pools and vignettes are the permitted depth tool,
   which is what Linear/Raycast actually ship (research/impressive-macapp-sites.md).

   Typography is unchanged and deliberately so: Besley (variable Clarendon) for
   display and prose, Spline Sans Mono for every machine voice.
   See research/typography-research.md.

   WHAT ROUND 3 CHANGED, and why it matters to anyone editing this file:
     - The hero is two columns (argument left, product right). The app window is
       bounded by its grid column, so it can no longer bleed off the viewport —
       the failure mode the owner rejected twice.
     - SUPERSEDED: the app window was a hand-built CSS/DOM mock in the app's real
       default-dark theme. It is now a real screen recording of the shipping app
       (.appwin frame + .appvid), so the whole --aw-* interior palette and every
       .aw-* rule are gone. Violet is still the OUTSIDE only: border, shadow,
       light pool.
     - The full-height datasheet table is gone, replaced by a ~100px marquee
       ribbon. Motion on this page is now the nebula loop, the hero recording and
       the marquee; all three stop under reduced motion.
     - The features and ownership sections are gone: features restated the
       datasheet, ownership restated sync item three. Ownership's file-format
       argument survives as the closing subsection of the sync section.

   Structural conventions:
     - radius 0 everywhere except the app-window frame, which is a physical
       object and needs it
     - separation is hairlines, whitespace and one barely-raised surface —
       never nested cards
     - motion: the hero loop and the ribbon, plus hover/focus colour
       transitions, all of which collapse under prefers-reduced-motion
   ========================================================================== */

/* --- fonts ---------------------------------------------------------------- */
/* Both faces are OFL variable fonts, subset to the page's actual character set
   and clamped to the weight range it actually uses. Besley 400-700: 400 body,
   700 display. Spline 400-500: 400 text, 500 labels. Neither has an italic cut
   here — nothing on this page is italic, and a serif-italic accent is itself a
   documented AI tell.

   Spline Sans Mono ships without U+2190 and the light box-drawing glyphs the
   file tree needs; they were drawn into the subset at the font's own stroke
   weight and cell width, because falling back to Menlo for four glyphs would
   break the tree's column alignment. */
@font-face {
  font-family: "Besley";
  src: url("../fonts/besley-var.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-stretch: normal;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Spline Sans Mono";
  src: url("../fonts/splinemono-var.woff2") format("woff2-variations");
  font-weight: 400 500;
  font-stretch: normal;
  font-style: normal;
  font-display: swap;
}

/* --- tokens -------------------------------------------------------------- */
:root {
  --bg:        #08060D;   /* near-black with a violet cast */
  --surface:   #100C18;   /* the one raised surface: ribbon band, file tree */
  --rule:      #26203A;   /* every hairline */
  --rule-lift: #3A3154;   /* the brighter hairline: top edges, ribbon dots */
  --ink:       #E6E2F0;
  --ink-bright:#F2EFFA;   /* display only */
  --ink-quiet: #9B93B0;   /* captions and labels. 6.9:1 on --bg, measured */

  --accent:      #8B5CF6; /* structural violet: borders, glows, shadow tint */
  --accent-lift: #A78BFA; /* text-safe violet: links, hover */
  --accent-num:  #C4B5FD; /* the ribbon numerals */

  /* The app-window frame's fill, behind the recording's poster. The app's real
     editor background, read out of Merkdown/Merkdown/Resources/Themes/
     default-dark.valutorio-theme (--bn-background), so the sliver of frame the
     poster does not cover during decode is the product's colour rather than the
     website's. The rest of the old --aw-* palette went with the CSS mock the
     recording replaced: the video carries the app's theme by construction. */
  --aw-edit: #26292B;

  /* Clarendon-ish fallbacks: the closest slab-serifs likely to be installed. */
  --serif: "Besley", Rockwell, "Roboto Slab", "Bitstream Vera Serif", serif;
  --mono:  "Spline Sans Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  /* Target ~68 rendered characters per line. A ch is the width of "0", which in
     Besley is 1.155x its average lowercase advance (probed from the subset), so
     58ch lands on ~67 real characters. */
  --measure: 58ch;
  --gutter: clamp(1.25rem, 4vw, 4.5rem);
  --page: 1360px;

  /* 120px tiling fractal noise. Purely to break up the large violet gradients:
     an 8-bit radial gradient over 900px of near-black bands visibly, and the
     banding is the single cheapest-looking artefact on a dark page. Inline so
     it costs no request. */
  --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");

}

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

html {
  background: var(--bg);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--serif);
  /* Besley's x-height is 0.520em, so 17-18px here reads as large as 20-22px in
     a normal-x-height serif. Line-height 1.68 clears its 1.05em ink span, which
     needs the extra leading on a dark ground where the strokes bloom. */
  font-size: clamp(1.0625rem, 0.4vw + 1rem, 1.125rem);
  line-height: 1.68;
  font-kerning: normal;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;  /* belt-and-braces: nothing on this page may bleed sideways */
}

/* --- shared primitives --------------------------------------------------- */
.wrap {
  max-width: var(--page);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.skiplink {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--bg);
  color: var(--accent-lift);
  font-family: var(--mono);
  font-size: 0.8125rem;
  padding: 0.6rem 1rem;
  border: 1px solid var(--accent);
  z-index: 20;
}
.skiplink:focus { left: 0.5rem; top: 0.5rem; }

a { color: var(--accent-lift); text-decoration-thickness: 1px; text-underline-offset: 0.18em; }
a:hover { color: var(--ink-bright); }

:focus-visible {
  outline: 2px solid var(--accent-lift);
  outline-offset: 3px;
}

/* Mono voice: kickers, folios, labels, numbers, captions. */
.kicker, .folio, .cta-note, .sources, .gs-cap, .ribbon-intro, .ribbon-set,
.tree, .foot-nav, .foot-credit, .foot-copy {
  font-family: var(--mono);
}

/* ========================================================================== */
/* --- the hero stage ------------------------------------------------------ */
/* Layers bottom to top: (1) the nebula loop, object-fit cover, full-bleed;
   (2) a scrim that is dark in the centre, heavier at the edges, and fades to
   solid --bg at the bottom so the section dissolves into the page instead of
   ending on a line; (3) noise; (4) content.

   The masthead is absolutely positioned over this stage rather than sitting
   above it, so the video starts at y=0 and the first viewport is all product. */
.stage {
  position: relative;
  isolation: isolate;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: clamp(7rem, 15vh, 10rem) clamp(3rem, 7vh, 5.5rem);
}

.stage-media {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  /* Poster as a background as well as a video attribute: when the reduced-motion
     guard removes the <video>, this is what remains, and it also covers the gap
     before the first frame decodes. */
  background: #08060D url("../media/hero-poster.jpg") center / cover no-repeat;
}

.stage-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border: 0;
}

/* Two gradients on one element. The radial keeps the text column readable and
   pushes the nebula's brightest passages to the edges; the linear lands on
   solid --bg at the bottom so the section dissolves into the page. */
.stage-scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 130% 105% at 50% 40%,
      rgba(8, 6, 13, 0.34) 0%,
      rgba(8, 6, 13, 0.70) 52%,
      rgba(8, 6, 13, 0.93) 100%),
    linear-gradient(to bottom,
      rgba(8, 6, 13, 0.72) 0%,
      rgba(8, 6, 13, 0.34) 26%,
      rgba(8, 6, 13, 0.62) 62%,
      rgba(8, 6, 13, 0.95) 86%,
      #08060D 100%);
}

/* Grain over the whole stage. 4.5% of a fractal-noise tile is invisible as
   texture and decisive against gradient banding. */
.stage-noise {
  position: absolute;
  inset: 0;
  background-image: var(--noise);
  opacity: 0.045;
  mix-blend-mode: overlay;
}

/* --- masthead ------------------------------------------------------------ */
/* Floats over the stage: no bar, no blur, no bg. It scrolls away with the hero,
   which keeps the video full-bleed and the first screen uninterrupted.
   Mark plus wordmark, nothing else: the App Store badge is the page's only
   download affordance, and a text link up here competed with it. */
.masthead {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 6;
}
.masthead-in {
  padding-block: 1.35rem;
  display: flex;
  align-items: center;
  gap: 0.7rem;
}
/* The mark repeats no information the wordmark beside it does not already
   state, so it is aria-hidden in the markup. Sized in px: it must not scale
   with the root font-size or a larger text setting would inflate the logo. */
.mark {
  display: block;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 9px;
}
.wordmark {
  margin: 0;
  font-size: 1.375rem;
  font-weight: 700;
  color: var(--ink-bright);
  /* Clarendons are drawn tight; tracking a slab out makes it look broken. */
  letter-spacing: 0;
  line-height: 1;
}

/* --- hero layout: argument left, product right -------------------------- */
/* Both columns are minmax(0, Nfr). That zero minimum is the whole reason the
   window cannot bleed any more: an `auto` minimum would let the window's own
   min-content width push the grid wider than the viewport, which is exactly how
   the previous two versions grew a horizontal scrollbar. */
.hero-in {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: clamp(2rem, 4vw, 3.75rem);
  align-items: center;
}

.hero-text { min-width: 0; }

/* The kicker is a plate of machine facts, so it is drawn as one hairline chip
   rather than set as a line of prose. */
.kicker {
  display: inline-block;
  margin: 0 0 clamp(1.25rem, 2.5vh, 1.85rem);
  padding: 0.42rem 0.85rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: rgba(16, 12, 24, 0.72);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--ink-quiet);
}

h1 {
  margin: 0 0 1.15rem;
  /* Retuned for the narrower column: the centred one-column hero could carry
     4.25rem, a 5/11 column cannot. 700 rather than the available 900 — Besley
     Black clots into solid slabs at display size on a dark ground where 700
     stays articulated. */
  font-size: clamp(2.25rem, 3.6vw, 3.375rem);
  font-weight: 700;
  line-height: 1.07;
  letter-spacing: 0;
  color: var(--ink-bright);
  text-wrap: balance;
}

.lede {
  margin: 0 0 clamp(1.75rem, 3.5vh, 2.35rem);
  max-width: 32rem;
  font-size: clamp(1.0625rem, 0.5vw + 1rem, 1.1875rem);
  line-height: 1.58;
  color: var(--ink);
}

/* --- App Store badge CTA ------------------------------------------------- */
.cta-row {
  margin: 0 0 0.9rem;
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
}

.cta {
  display: inline-block;
  line-height: 0;
  text-decoration: none;
  border: 0;
  border-radius: 10px;
  /* The badge is a black rectangle on a near-black page: without an ambient
     violet lift it disappears. This is the same light the app window sits in. */
  box-shadow: 0 14px 40px -14px rgba(139, 92, 246, 0.55);
  transition: box-shadow 140ms linear;
}
.cta:hover { box-shadow: 0 16px 48px -12px rgba(167, 139, 250, 0.7); }
.badge { display: block; height: auto; max-width: 100%; }
.badge-frame { transition: stroke 120ms linear; }
.cta:hover .badge-frame { stroke: var(--accent-lift); }
.cta:focus-visible { outline-offset: 4px; }

/* Apple's badge is a third-party brand asset reproduced with its own type and
   corner radius. This is the only system sans on the page and it belongs to
   the badge, not to Valutorio. */
.badge-sm, .badge-lg {
  fill: #FFFFFF;
  font-family: -apple-system, "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.badge-sm { font-size: 9.6px; letter-spacing: 0.012em; }
.badge-lg { font-size: 19.5px; letter-spacing: -0.004em; }

.cta-note {
  margin: 0;
  font-size: 0.75rem;
  color: var(--ink-quiet);
  letter-spacing: 0.03em;
}

/* ========================================================================== */
/* --- the showcase: the app recording ------------------------------------- */
/* Bounded entirely by its grid column: width 100%, min-width 0, no negative
   margins, no max-width of its own. The whole window is the point, so any rule
   added here that could make it wider than the column is a regression. */
.showcase {
  position: relative;
  width: 100%;
  min-width: 0;
}

/* A single-hue violet light pool behind the window, taller than the window so
   the light appears to come from behind the product. This is the Linear/Raycast
   "backlit" build done in CSS instead of a baked PNG.

   Positioned entirely with insets and NO transform. The obvious
   `left:50%; width:100%; translate(-50%,-50%)` is wrong here: scrollWidth is
   computed from the untransformed box, so the browser counts the layer as
   reaching past the container and the page grows a horizontal scrollbar that
   the transform then hides visually — which is exactly the class of bug that
   made the previous versions bleed off the right edge. Insets have no such
   shadow geometry; the negative top/bottom insets let the halo extend beyond
   the window vertically without ever widening the layout box. */
.showcase::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -16%;
  bottom: -16%;
  background: radial-gradient(ellipse 64% 60% at 50% 48%,
    rgba(139, 92, 246, 0.40) 0%,
    rgba(110, 66, 214, 0.19) 38%,
    rgba(59, 29, 110, 0.07) 62%,
    rgba(8, 6, 13, 0) 80%);
  pointer-events: none;
  z-index: -1;
}

/* The premium exterior, unchanged from the mock it now frames: 1px light border
   with a brighter top edge for the light-from-above read, 12px radius, violet
   ambient bloom plus a tight black contact shadow so the window still sits on
   something. What changed is the INSIDE — it used to be a hand-built DOM
   recreation of the app in the app's own theme, and it is now a real screen
   recording of the app, which carries that theme by construction.

   overflow: hidden is load-bearing, not tidiness: a <video> paints its own
   rectangular box and ignores an ancestor's border-radius unless a clip exists,
   so without it the recording's square corners would sit proud of this frame's
   rounded ones at every corner.

   line-height: 0 kills the inline-layout descender gap under the video. A
   replaced element is inline-level by default, so the line box adds a few
   pixels of leading below it — which reads as a thin band of frame background
   along the bottom edge only, the asymmetry that makes a frame look wrong
   without announcing why. display:block on .appvid handles the same problem
   from the child's side; both are here because either alone is enough and the
   pair costs nothing.

   aspect-ratio is load-bearing for the reduced-motion case, not decoration: the
   frame's height comes entirely from the video it contains, so once the guard
   REMOVES that video the box collapses to its borders (measured: 646x2px) and
   the poster background has nothing to paint on. Declaring the recording's own
   1000:640 ratio means the frame keeps its exact size either way, which is what
   makes the poster a true substitute for the removed video.

   box-sizing: content-box is why that ratio is a no-op while the video IS
   present, and the reason this is not a one-liner. aspect-ratio resolves against
   whichever box box-sizing names, and the page-wide default is border-box: under
   border-box the 1px border is inside the ratio, so the frame's CONTENT box ends
   up ~0.7px shorter than the video its own ratio describes, and overflow:hidden
   shaves a sub-pixel strip off the bottom of the footage. Sizing the content box
   instead makes frame and video resolve to the identical height at every width
   — no clip with the video, no collapse without it. */
.appwin {
  position: relative;
  box-sizing: content-box;
  aspect-ratio: 1000 / 640;
  border: 1px solid rgba(255, 255, 255, 0.10);
  border-top-color: rgba(255, 255, 255, 0.20);
  border-radius: 12px;
  /* The poster, so the frame is never an empty box: it covers the gap before
     the first frame decodes, and it is what remains under reduced motion once
     the guard removes the <video> — the static equivalent of the old mock. */
  background: var(--aw-edit) url("../media/app-demo-poster.jpg") center / cover no-repeat;
  overflow: hidden;
  line-height: 0;
  box-shadow:
    0 40px 120px -20px rgba(139, 92, 246, 0.35),
    0 24px 60px -24px rgba(0, 0, 0, 0.85);
}

/* Fluid down to the narrowest breakpoint: width 100% of the frame, height from
   the intrinsic 1000x640 ratio the width/height attributes declare. No
   max-width and no min-width here — the video must never be the reason the grid
   column, and with it the page, gets wider than the viewport. That bleed is the
   failure mode this hero has had twice; auto height plus a 100% width cannot
   reintroduce it. */
.appvid {
  display: block;
  width: 100%;
  height: auto;
  border: 0;
}

/* ========================================================================== */
/* --- the hero git strip -------------------------------------------------- */
/* A band inside the stage, below the two hero columns and still over the
   nebula. It sits directly on the scrim with a single hairline above it rather
   than on a panel: the app window two hundred pixels up is the hero's only raised
   surface, and a second one here read as two competing cards. The hairline
   picks up the violet-struck treatment of the section rules so the strip is
   legibly part of the same system.

   The strip is allowed to make the stage taller than 100svh. min-height is a
   floor; the two-column pair keeps its own space above and the strip gets a
   generous top margin instead of being squeezed into the remaining viewport. */
.gitstrip {
  position: relative;
  z-index: 2;
  margin-top: clamp(3.25rem, 7vh, 5rem);
  padding-top: clamp(2.25rem, 4.5vh, 3.25rem);
  display: grid;
  /* Same minmax(0, …) discipline as .hero-in: an `auto` minimum would let the
     grid's max-content width push this column — and the page — wider than the
     viewport, which is the exact bug class that bled the earlier versions. */
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  gap: clamp(1.75rem, 4vw, 3.75rem);
  align-items: center;
  border-top: 1px solid var(--rule);
}
/* The struck first stretch of the hairline, same gesture as .sec::before.
   Inset by the wrap's own gutter so it starts where the text does. */
.gitstrip::before {
  content: "";
  position: absolute;
  top: -1px;
  left: var(--gutter);
  width: min(18rem, 34%);
  height: 1px;
  background: linear-gradient(to right, var(--accent), rgba(139, 92, 246, 0));
}

.gs-say { min-width: 0; }

/* A heading in every way except the outline (see the note in the markup). */
.strip-head {
  margin: 0 0 0.7rem;
  font-family: var(--serif);
  font-size: clamp(1.5rem, 1.6vw + 0.95rem, 1.75rem);
  font-weight: 700;
  line-height: 1.18;
  letter-spacing: 0;
  color: var(--ink-bright);
  text-wrap: balance;
}
.strip-body {
  margin: 0;
  max-width: 55ch;
  font-size: clamp(0.9375rem, 0.3vw + 0.875rem, 1.0625rem);
  line-height: 1.6;
  color: var(--ink);
}

.gs-show { min-width: 0; }

/* The clip is what keeps a 43-column max-content grid inside a fluid column.
   direction:rtl pins the grid to the container's right edge so the columns that
   fall out of view are the OLDEST ones on the left — which is how a real
   contribution graph truncates. The grid itself is switched back to ltr, or the
   whole authored sequence would mirror. */
.gs-grid-clip {
  overflow: hidden;
  direction: rtl;
  /* Feather the clipped left edge so the oldest visible column fades in rather
     than ending on a hard vertical cut. */
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 3.5rem);
  mask-image: linear-gradient(to right, transparent 0, #000 3.5rem);
}

/* 7 rows, column-major fill: each authored week of 7 spans becomes one column,
   exactly like GitHub's. grid-auto-flow:column with a fixed row count is what
   does that — no per-cell placement, no JS.

   Five intensities: an almost-invisible empty, then four violet steps rising to
   the palette's brightest text-safe violet. The steps are the accent hue at
   increasing opacity over the near-black ground, so the grid belongs to the
   palette rather than introducing a sixth colour. */
.gs-grid {
  --gs-cell: 10px;
  --gs-l0: rgba(167, 139, 250, 0.07);
  --gs-l1: rgba(139, 92, 246, 0.30);
  --gs-l2: rgba(139, 92, 246, 0.55);
  --gs-l3: #8B5CF6;
  --gs-l4: #C4B5FD;
  direction: ltr;
  display: grid;
  grid-template-rows: repeat(7, var(--gs-cell));
  grid-auto-flow: column;
  grid-auto-columns: var(--gs-cell);
  gap: 3px;
  width: max-content;
}
.gs-grid span {
  border-radius: 2px;
  background: var(--gs-l0);
}
.gs-grid .lvl1 { background: var(--gs-l1); }
.gs-grid .lvl2 { background: var(--gs-l2); }
.gs-grid .lvl3 { background: var(--gs-l3); }
/* Only the top level glows, and only by 2px: at 10px a cell is mostly edge, so
   a wider shadow stops reading as a lit cell and starts reading as a blur. */
.gs-grid .lvl4 {
  background: var(--gs-l4);
  box-shadow: 0 0 2px rgba(196, 181, 253, 0.85);
}

.gs-cap {
  margin: 0.9rem 0 0;
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  color: var(--ink-quiet);
}

/* ========================================================================== */
/* --- sections ------------------------------------------------------------ */
.sec {
  position: relative;
  border-top: 1px solid var(--rule);
  padding-block: clamp(3rem, 5.5vw, 5rem) clamp(3.25rem, 6vw, 5.75rem);
}
/* The section rule brightens to violet for its first stretch, so the hairline
   reads as struck rather than drawn. One accent doing structural work. */
.sec::before {
  content: "";
  position: absolute;
  top: -1px;
  left: 0;
  width: min(22rem, 40%);
  height: 1px;
  background: linear-gradient(to right, var(--accent), rgba(139, 92, 246, 0));
}

.folio {
  margin: 0 0 clamp(2rem, 3.5vw, 3.25rem);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--ink-quiet);
}
.folio-n { color: var(--accent-lift); }

/* Section head: heading left, prose in the wider right column. */
.sec-grid {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 7fr);
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: start;
}

h2 {
  margin: 0;
  font-size: clamp(1.75rem, 2.75vw, 2.5rem);
  font-weight: 700;
  line-height: 1.14;
  letter-spacing: 0;
  color: var(--ink-bright);
  text-wrap: balance;
}

h3 {
  margin: 0 0 0.4rem;
  font-size: 1.125rem;
  font-weight: 700;
  line-height: 1.35;
  color: var(--ink-bright);
}

.prose { max-width: var(--measure); }
.prose p { margin: 0 0 1.15rem; }
.prose p:last-child { margin-bottom: 0; }

.sources {
  margin-top: 1.75rem !important;
  font-size: 0.75rem;
  line-height: 1.9;
  color: var(--ink-quiet);
}
.sources a { color: var(--ink-quiet); text-decoration-color: var(--rule); }
.sources a:hover { color: var(--accent-lift); text-decoration-color: var(--accent); }

/* ========================================================================== */
/* --- the datasheet ribbon ----------------------------------------------- */
/* The numbers used to be a full-height section with a table, and it stopped the
   page dead between two arguments. As a ~100px band they read as instrumentation
   on a running machine, which is what they are.

   Construction: .ribbon-view clips, .ribbon-track holds the eight items TWICE
   and slides exactly -50%, so the loop returns to a pixel-identical position
   and has no visible seam. Because the clip is overflow:hidden on a non-
   positioned-out box, the oversized track never reaches the document's scroll
   area and cannot widen the page. */
.ribbon {
  border-block: 1px solid var(--rule);
  background: var(--surface);
  padding-block: 1.05rem 1.15rem;
  overflow: hidden;
}
.ribbon-intro {
  margin: 0 0 0.65rem;
  font-size: 0.6875rem;
  letter-spacing: 0.05em;
  color: var(--ink-quiet);
}
.ribbon-view { overflow: hidden; }
.ribbon-track {
  display: flex;
  width: max-content;
  animation: rb-slide 60s linear infinite;
}
/* Hovering pauses rather than slows: a number you are trying to read must stop
   moving, and a partial slow-down is the worst of both. */
.ribbon-view:hover .ribbon-track { animation-play-state: paused; }
@keyframes rb-slide {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
.ribbon-set {
  display: flex;
  flex: none;
  align-items: baseline;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ribbon-set li {
  display: flex;
  flex: none;
  align-items: baseline;
  gap: 0.5rem;
  white-space: nowrap;
}
/* A middot in the gap between items, on every item including the last: in an
   infinite ribbon the last item's separator is the one before the wrap. */
.ribbon-set li::after {
  content: "·";
  margin-inline: 1.5rem;
  color: var(--rule-lift);
}
.rb-v {
  font-size: 1.0625rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--accent-num);
}
.rb-l { font-size: 0.75rem; color: var(--ink-quiet); }

/* Feather the two clipped edges so items do not hard-cut mid-word at the
   viewport boundary. Scoped to no-preference: the reduced-motion layout is a
   static wrapped row with nothing to feather. */
@media (prefers-reduced-motion: no-preference) {
  .ribbon-view {
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 5rem, #000 calc(100% - 5rem), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 5rem, #000 calc(100% - 5rem), transparent 100%);
  }
}

/* --- sync: three items, no cards ---------------------------------------- */
.triple {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.75rem, 3.5vw, 3.25rem);
  margin-top: clamp(2.5rem, 4.5vw, 3.75rem);
  padding-top: clamp(1.75rem, 3vw, 2.5rem);
  border-top: 1px solid var(--rule);
}
.triple p { margin: 0; color: var(--ink-quiet); font-size: 0.9375rem; line-height: 1.6; }

/* --- sync subsection: the file format ----------------------------------- */
/* This was its own numbered section and made the same argument twice — sync
   item three already says "it's git, leave any time with everything". Merged
   in as the closing subsection, where the file-format claim belongs. An h3
   under the section's h2, drawn larger than the .triple h3s beside it so the
   heading order stays valid without the hierarchy reading flat. */
.subsec {
  margin-top: clamp(2.75rem, 5vw, 4.25rem);
  padding-top: clamp(2rem, 3.5vw, 2.75rem);
  border-top: 1px solid var(--rule);
}
.subhead {
  margin: 0 0 1.1rem;
  max-width: 26ch;
  font-size: clamp(1.375rem, 1.9vw, 1.75rem);
  line-height: 1.2;
  text-wrap: balance;
}

/* --- the file tree ------------------------------------------------------ */
.tree {
  margin: clamp(1.75rem, 3.5vw, 2.75rem) 0 0;
  padding: 1.5rem 1.75rem;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-left: 2px solid var(--accent);
  color: var(--ink-quiet);
  font-size: 0.8125rem;
  line-height: 1.85;
  overflow-x: auto;
  white-space: pre;
}
/* The .git line is the whole argument of this subsection, so it is the one line
   in the listing that carries the accent. Colour only: no weight change, or the
   monospaced columns stop lining up. */
.tree-git { color: var(--accent-num); }

/* --- final CTA ---------------------------------------------------------- */
/* Its own light pool and the app icon at size: the classic Mac-app close. */
/* overflow:hidden keeps the pool from painting over the footer, which sits in a
   lower paint layer and would otherwise be washed by it. */
.sec-get { overflow: hidden; }
/* Consequence of that clip: the struck section rule at top:-1px would be cut,
   so this one section draws it one pixel lower, inside the box. */
.sec-get::before { top: 0; }
.sec-get::after {
  content: "";
  position: absolute;
  /* Inset-positioned with no transform, for the same scrollWidth reason
     documented on .showcase::before. */
  left: 0;
  right: 0;
  top: 18%;
  bottom: -30%;
  background: radial-gradient(ellipse 54% 62% at 50% 62%,
    rgba(139, 92, 246, 0.24) 0%,
    rgba(110, 66, 214, 0.11) 40%,
    rgba(8, 6, 13, 0) 72%);
  pointer-events: none;
  z-index: 0;
}
.get-in {
  position: relative;
  z-index: 1;
  text-align: center;
  max-width: 40rem;
  margin-inline: auto;
}
.get-icon {
  display: block;
  width: clamp(96px, 12vw, 148px);
  height: auto;
  margin: 0 auto clamp(1.5rem, 3vw, 2.25rem);
  border-radius: 22%;
  filter: drop-shadow(0 26px 60px rgba(139, 92, 246, 0.4));
}
.sec-get h2 { margin-bottom: 1rem; }
.sec-get .prose { margin-inline: auto; }
.sec-get .cta-row { margin-top: clamp(2rem, 3.5vw, 2.75rem); margin-bottom: 0.9rem; justify-content: center; }

/* --- footer ------------------------------------------------------------- */
.site-foot {
  border-top: 1px solid var(--rule);
  padding-block: clamp(2.5rem, 4vw, 3.5rem) 3rem;
}
.foot-grid { display: grid; gap: 1.5rem; }
.foot-mark {
  margin: 0 0 0.25rem;
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1;
  color: var(--ink-bright);
  /* Mark and wordmark on one baseline: the footer echoes the masthead's
     construction at a smaller size rather than repeating its weight. */
  display: flex;
  align-items: center;
  gap: 0.55rem;
}
.mark-foot { width: 24px; height: 24px; border-radius: 6px; }
.foot-line { margin: 0; color: var(--ink-quiet); font-size: 0.9375rem; }
.foot-nav { font-size: 0.75rem; color: var(--ink-quiet); letter-spacing: 0.03em; }
.foot-nav a { color: var(--ink-quiet); text-decoration: none; border-bottom: 1px solid var(--rule); }
.foot-nav a:hover { color: var(--accent-lift); border-bottom-color: var(--accent); }
/* Replaces the trademark disclaimer this page no longer needs. The link sits
   quiet until hovered, which is where the violet underline arrives. */
.foot-credit, .foot-copy {
  margin: 0;
  font-size: 0.6875rem;
  line-height: 1.7;
  color: var(--ink-quiet);
}
.foot-credit a {
  color: var(--ink-quiet);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  transition: color 120ms linear, border-color 120ms linear;
}
.foot-credit a:hover { color: var(--accent-lift); border-bottom-color: var(--accent); }
.foot-copy { letter-spacing: 0.04em; }

/* --- responsive --------------------------------------------------------- */
/* 900px: the hero pair stacks, text above window. Below this the window column
   would be too narrow for a sidebar plus an honest editor measure. */
@media (max-width: 56.25em) {
  .hero-in { grid-template-columns: minmax(0, 1fr); row-gap: clamp(2.5rem, 6vh, 3.5rem); }
  .hero-text { max-width: 40rem; }
  .showcase { max-width: 44rem; }
  /* The strip stacks the same way the hero pair does: text above grid. The
     grid keeps its authored width and simply loses its oldest columns to the
     clip, which is why no narrower variant of the sequence is needed. */
  .gitstrip { grid-template-columns: minmax(0, 1fr); row-gap: clamp(1.75rem, 4vh, 2.5rem); }
  .gs-say { max-width: 40rem; }
}

@media (max-width: 60rem) {
  .sec-grid { grid-template-columns: 1fr; row-gap: 1.5rem; }
  .triple { grid-template-columns: 1fr; row-gap: 2rem; }
}

@media (max-width: 40rem) {
  .stage { min-height: 0; padding-block: 6.5rem clamp(2.5rem, 6vh, 3.5rem); }
  .kicker { font-size: 0.625rem; letter-spacing: 0.1em; }
  /* No .appwin override here: the recording is fluid by construction (width
     100%, height from its intrinsic ratio), so the frame needs no breakpoint. */
  /* 9px cells with the same 3px gap: at 390px the clip shows ~26 of the 43
     columns, which still reads as a year's worth of activity rather than a
     fragment. The mask narrows with them so the fade stays proportional. */
  .gs-grid { --gs-cell: 9px; }
  .gs-grid-clip {
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 2.5rem);
    mask-image: linear-gradient(to right, transparent 0, #000 2.5rem);
  }
  .ribbon-set li::after { margin-inline: 1.1rem; }
  .rb-v { font-size: 0.9375rem; }
  .tree { padding: 1.1rem 1rem; font-size: 0.75rem; }
}

/* --- motion ------------------------------------------------------------- */
/* Two moving things on this page: the hero loop and the ribbon. Both stop here.
   The inline guard in the document removes the video outright so it is not
   merely paused; this rule is the CSS-first half, effective with JS disabled. */
@media (prefers-reduced-motion: reduce) {
  /* Both videos, so the hero shows the two posters: .stage-media's background
     behind the scrim, .appwin's inside the frame. */
  .stage-video, .appvid { display: none !important; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  /* The blanket rule above would leave the marquee parked at its -50% end
     state with half the ribbon off-screen, so the track is un-animated
     outright and relaid as a static centred row that wraps. The duplicate set
     exists only to make the loop seamless, so it goes: what is left is the
     eight items once, all readable, nothing clipped.
     The band drops its clip too, because a wrapped row is taller than the
     moving one and overflow:hidden would cut its last line. */
  .ribbon, .ribbon-view { overflow: visible; }
  .ribbon-track {
    animation: none !important;
    transform: none !important;
    width: auto;
  }
  .rb-dup { display: none; }
  /* `flex: none` is what makes the marquee track hold its max-content width,
     and it is exactly what must go here: left in place the set keeps that
     width and the row is clipped instead of wrapped. The set becomes a
     full-width shrinkable wrapping row, and so do its items. */
  .ribbon-set {
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
    flex-wrap: wrap;
    justify-content: center;
    column-gap: 2.25rem;
    row-gap: 0.3rem;
  }
  .ribbon-set li { flex: 0 1 auto; min-width: 0; }
  /* Separation becomes the gap, not a middot. In a wrapping row the middot
     lands at the end of every line as a dangling character (visible at 390px,
     where each item is its own row), and the column gap already separates the
     items that do share a line. */
  .ribbon-set li::after { content: none; margin-inline: 0; }
}
