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

:root {
  /* Foundation */
  --ratio: 1.25;
  --base: 1.125rem;
  --line-height: 1.5;

  /* Type scale */
  --text-sm: calc(var(--base) / var(--ratio));
  --text-base: var(--base);
  --text-md: calc(var(--base) * var(--ratio));
  --text-lg: calc(var(--base) * var(--ratio) * var(--ratio));
  --text-xl: calc(var(--base) * var(--ratio) * var(--ratio) * var(--ratio));
  --text-2xl: calc(var(--base) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio));

  /* Spacing */
  --line: calc(var(--base) * var(--line-height));
  --space-xs: calc(var(--line) * 0.25);
  --space-sm: calc(var(--line) * 0.5);
  --space-md: var(--line);
  --space-lg: calc(var(--line) * 2);
  --space-xl: calc(var(--line) * 3);
  --space-2xl: calc(var(--line) * 4);

  /* Container widths */
  --container-sm: 540px;
  --container-md: 640px;
  --container-lg: 960px;
  --container-xl: 1240px;
  /* Breakpoint: 640px (used in media queries) */

  /* Typography details */
  --tracking-tight: -0.035em;
  --tracking-snug: -0.01em;

  /* Font stacks */
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --font-display: "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;

  /* Distance from the top of a hero to its eyebrow (BORINGWARE / back-link).
     Both hero variants anchor to this — it must be global, since .product-hero
     and .platform-hero are siblings, not nested. */
  --hero-top: calc(var(--line) * 3);
  /* How deep the app window sinks below the hero's bottom edge. Global: both
     hero variants size themselves against it, and a var out of scope silently
     invalidates whatever declaration references it. */
  --pocket-depth: 50px;

  /* Code blocks on the branding page */
  --code-bg: rgba(127, 127, 127, 0.08);

  /* Effects */
  --radius-sm: 6px;
  --radius-md: 14px;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
  --transition-fast: 0.15s ease;
  --transition-med: 0.25s ease;

  /* Colors */
  --text: #000;
  --text-secondary: rgba(0, 0, 0, 0.6);
  --bg: #fff;
  --border: rgba(0, 0, 0, 0.15);
  --button-bg: #000;
  --button-text: #fff;
}

/* Skipped under [data-ogplus]: the share-card renderer may report a dark
   colour scheme, and a card must always be the light composition — the brand
   accent with near-white text. Every dark block in this file carries the same
   guard for the same reason. */
@media (prefers-color-scheme: dark) {
  html:not([data-ogplus]) {
    --text: #fff;
    --text-secondary: rgba(255, 255, 255, 0.65);
    --bg: #000;
    --border: rgba(255, 255, 255, 0.15);
    --button-bg: #fff;
    --button-text: #000;
  }
}

html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--line-height);
  color: var(--text);
  background: var(--bg);
}

a {
  color: inherit;
  text-decoration: none;
}

/* Layout */
.page {
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

.container {
  width: 100%;
  max-width: var(--container-sm);
  margin: 0 auto;
  padding: 0 var(--space-md);
}

/* ── The app mosaic ──────────────────────────────────────────────────────────
   Rendered by Components::Mosaic in two places — the homepage and the "more
   apps" footer on every product page. One grid holds the wordmark and every app. Every app is the same 1:1 square —
   what breaks the grid up is the app itself: each tile crops into that product's
   hero window, bleeding off the bottom-right corner. The wordmark is the one
   odd cell: a banner three columns wide (two at phone width, where two columns
   is the whole grid).

   Cell math, so the mosaic always ends on a full row: the wordmark eats 3 cells
   (2 at phone), so with N apps you want (3 + N) divisible by the column count.
   N = 9 today → 12 cells, which divides evenly by 2, 3, AND 4 — every tier ends
   flush. It was a 2x2 block when there were 8 apps; the ninth made 13 cells and
   left an orphan in the last row, so the block became a banner. Change the app
   count and redo this arithmetic — `grid-auto-flow: dense` only reshuffles
   holes, it can't fill a short final row. ── */
.home-main {
  flex: 1;
  display: flex;
  align-items: center;
  padding: var(--space-lg) var(--space-md);
}

.mosaic {
  width: 100%;
  max-width: var(--container-xl);
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-flow: row dense;
  gap: var(--space-xs);
}

@media (min-width: 640px) {
  .mosaic { grid-template-columns: repeat(3, 1fr); }
}

@media (min-width: 900px) {
  .mosaic { grid-template-columns: repeat(4, 1fr); }
}

/* Every cell is square and sizes its own type off its own width. */
.mosaic-header,
.product-tile {
  container-type: inline-size;
  aspect-ratio: 1;
  min-width: 0;
}

.mosaic-header {
  grid-column: span 2;
  aspect-ratio: 2;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 var(--space-sm);
}

/* Past phone width the wordmark stretches into a three-column banner one row
   tall, which is what keeps 9 apps ending flush at 3 and 4 columns. */
@media (min-width: 640px) {
  .mosaic-header {
    grid-column: span 3;
    aspect-ratio: 3;
    padding: 0 var(--space-md);
  }
}

.lockup-mark {
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 13cqw, 5.5rem);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  line-height: 0.95;
  margin: 0;
}

.lockup-tagline {
  font-size: clamp(0.8rem, 4cqw, 1.4rem);
  font-weight: 400;
  line-height: 1.3;
  color: var(--text-secondary);
  margin: 0.35em 0 0 0;
}

/* Product Tiles */
.mosaic-tiles {
  display: contents;
  list-style: none;
  margin: 0;
  padding: 0;
}

.product-tile {
  --accent: #000;
  --text-lightness: 97%;
  --text-chroma: 0.03;
  --accent-text: oklch(from var(--accent) var(--text-lightness) var(--text-chroma) h);
  --accent-text-secondary: oklch(from var(--accent) var(--text-lightness) var(--text-chroma) h / 0.7);
  --surface: var(--accent);
}

.product-tile-link {
  --pocket-depth: 6cqw;
  --shot-rotation: -2deg;
  --shot-scale: 1;
  display: flex;
  flex-direction: column;
  position: relative;
  height: 100%;
  background: var(--surface);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: transform var(--transition-fast);
}

/* The pocket seam, same trick as the product hero: a hairline at the tile's
   bottom edge casting a soft shadow UP over the window, so the app reads as
   tucked into the tile rather than pasted on top of it. */
.product-tile-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  box-shadow: 0 0 5cqw 2.5cqw rgba(0, 0, 0, 0.22);
  z-index: 2;
}

/* Hover pushes the zoom down into the art via a custom property, so the shot
   and the icon fallback both pick it up without restating their transforms.
   The tile clips, so the app grows INTO the tile rather than out of it. */
.product-tile-link:hover {
  --shot-scale: 1.045;
  transform: translateY(-2px);
}

.product-tile-link:active {
  transform: translateY(0);
}

/* Text owns the top of the square; the app takes everything under it. */
.product-tile-text {
  position: relative;
  z-index: 1;
  flex: none;
  padding: clamp(0.75rem, 7cqw, 2rem);
  padding-bottom: clamp(0.5rem, 3.5cqw, 1rem);
}

.product-name {
  display: block;
  font-size: clamp(1.05rem, 9cqw, 2.6rem);
  font-weight: 700;
  letter-spacing: var(--tracking-snug);
  line-height: 1.1;
  color: var(--accent-text);
}

.product-tagline {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  font-size: clamp(0.8rem, 5.6cqw, 1.55rem);
  font-weight: 400;
  line-height: 1.25;
  color: var(--accent-text-secondary);
  margin-top: 0.35em;
}

/* The app itself, cropped by the tile. It sits in FLOW under the text rather
   than at a fixed offset, so it starts right below the price whether the
   tagline runs one line or three — a fixed top left a band of dead colour on
   the short ones. Wider than the tile on purpose, and anchored left so the
   window keeps its own left edge (traffic lights, sidebar) and bleeds off the
   right: that's what makes it read as a real app and not a thumbnail. No shadow
   or frame — the crop plus the pocket is the whole treatment. */
.product-tile-shot {
  display: block;
  flex: none;
  margin-left: 8cqw;
  margin-top: auto;
  width: 116cqw;
  pointer-events: none;
  /* Origin bottom-left pins the window's own left edge, so the zoom pushes into
     the crop (right + up, behind the text) instead of sliding the sidebar out
     of frame. */
  transform: rotate(var(--shot-rotation)) translateY(var(--pocket-depth)) scale(var(--shot-scale));
  transform-origin: bottom left;
  transition: transform var(--transition-med);
}

.product-tile-shot img {
  display: block;
  width: 100%;
  height: auto;
}

/* Icon stand-in for a product with no hero capture yet — same pocket, same
   rotation, so the row still reads as one system. */
.product-tile-mark {
  display: block;
  flex: none;
  width: 40cqw;
  margin: auto auto calc(var(--pocket-depth) * -0.4);
  pointer-events: none;
  /* A mark is a closed shape, not a window that keeps going — it only dips a
     little into the pocket, or it reads as broken rather than tucked. */
  transform: rotate(var(--shot-rotation)) translateY(calc(var(--pocket-depth) * 0.35)) scale(var(--shot-scale));
  transition: transform var(--transition-med);
}

.product-tile-mark svg {
  display: block;
  width: 100%;
  height: auto;
}

/* Phone tiles are small enough that the crop would swallow the tagline. */
/* Phone squares are small: two lines of tagline, and the crop starts higher so
   the app is still a visible slice rather than a sliver. */
@media (max-width: 639px) {
  .product-tile-text {
    padding: 0.7rem;
    padding-bottom: 0.4rem;
  }

  .product-tile-shot {
    margin-left: 10cqw;
    width: 106cqw;
  }
}

/* Reduced motion / print: the tuck is decorative, the crop still reads. */
@media (prefers-reduced-motion: reduce) {
  .product-tile-link,
  .product-tile-shot,
  .product-tile-mark {
    transition: none;
  }

  .product-tile-link:hover {
    --shot-scale: 1;
  }
}

/* Footer */
.site-footer {
  padding: var(--space-lg) var(--space-md);
  max-width: var(--container-md);
  margin: 0 auto;
  width: 100%;
}

.footer-social {
  display: flex;
  justify-content: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-sm);
}

.social-link {
  color: var(--text-secondary);
  transition: color var(--transition-fast), transform var(--transition-fast);
}

.social-link:hover {
  color: var(--text);
  transform: translateY(-1px);
}

.footer-text {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0;
  text-align: center;
}

.footer-text a {
  color: inherit;
  text-decoration: none;
  transition: color var(--transition-fast);
}

.footer-text a:hover {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* Button */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-sm) var(--space-md);
  font-size: var(--text-sm);
  font-weight: 600;
  background: var(--accent-text);
  color: var(--accent);
  border: none;
  cursor: pointer;
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.1);
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}

.btn:hover {
  transform: translateY(-1px);
  box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}

.btn:active {
  transform: translateY(0);
  box-shadow: var(--shadow-sm);
}

/* Signup form */
.signup-wrapper {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin-top: var(--space-lg);
}

.signup-status {
  font-size: var(--text-sm);
  font-weight: 600;
  /* Match the page's accent-derived text (dark on light accents like Mac Deck
     gold, near-white on dark accents) — not a hardcoded white. */
  color: var(--page-text, #fff);
  margin: 0;
}

/* A quiet caption under the form: the status names the state, the hint says
   what the input is for. */
.signup-hint {
  font-size: var(--text-sm);
  color: var(--page-text-secondary, var(--text-secondary));
  margin: 0;
}

/* Signup form derives from the page's own text/accent so it inverts with the
   accent lightness: on dark accents the button is near-white with an accent
   label (as before); on LIGHT accents (e.g. Mac Deck gold) it flips to the
   dark text color — never a stray white block. --page-text is the header/body
   text color, so everything here matches it. */
.signup-form {
  display: flex;
  flex-direction: row;
  /* When the row can't hold input + button, the button wraps under the input
     rather than being pushed out of the form and clipped. */
  flex-wrap: wrap;
  background: oklch(from var(--page-text, #fff) l c h / 0.14);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.signup-input {
  padding: var(--space-sm) var(--space-md);
  font-size: var(--text-sm);
  border: none;
  background: transparent;
  color: var(--page-text, #fff);
  /* A huge grow factor, deliberately: sharing a row with the button the input
     takes essentially all the free space (so the button stays its label's
     width), but once the button wraps to its own row it is alone there and
     grows to fill it. Full-width button under a full-width field, with no
     breakpoint to guess at. */
  flex: 999 1 10rem;
  min-width: 0;
}

.signup-input:focus {
  outline: none;
  background: oklch(from var(--page-text, #fff) l c h / 0.08);
}

.signup-input::placeholder {
  color: oklch(from var(--page-text, #fff) l c h / 0.5);
}

.signup-button {
  padding: var(--space-sm) var(--space-md);
  font-size: var(--text-sm);
  font-weight: 600;
  /* The label never truncates or wraps: the INPUT is the part that flexes
     (its grow factor dwarfs this one). Without a basis of auto plus grow, a
     wrapped button sat at label width with the form's tint stranded beside it.
     Without nowrap a narrow column cut "Notify Me" to "Notify". */
  flex: 1 1 auto;
  white-space: nowrap;
  /* Pill = the accent-derived text color: light on dark/mid accents (white pill,
     accent label) and dark on light accents (e.g. Mac Deck gold → near-black pill,
     gold label). So it always contrasts the accent bg and the label always reads. */
  background: var(--page-text, #fff);
  color: var(--accent);
  border: none;
  cursor: pointer;
  white-space: nowrap;
  transition: opacity var(--transition-fast);
}

.signup-button:hover {
  opacity: 0.9;
}

/* Platform pills — link to the per-platform screenshot pages. Derive from
   --page-text so they read on any accent. */
.platform-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
}

.platform-pill {
  display: inline-flex;
  align-items: center;
  padding: 0.45em 1.15em;
  border-radius: 999px;
  border: 1px solid oklch(from var(--page-text, #fff) l c h / 0.35);
  color: var(--page-text, #fff);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  transition: background var(--transition-fast), border-color var(--transition-fast);
}

.platform-pill:hover {
  background: oklch(from var(--page-text, #fff) l c h / 0.12);
  border-color: oklch(from var(--page-text, #fff) l c h / 0.6);
}

/* Active platform (on a platform page) reads as a filled tab. */
.platform-pill.is-active {
  background: var(--page-text, #fff);
  color: var(--accent);
  border-color: var(--page-text, #fff);
}

.signup-button-unsub {
  background: transparent;
  color: rgba(255, 255, 255, 0.4);
}

.signup-button-unsub:hover {
  background: transparent;
  color: rgba(255, 255, 255, 0.6);
}

/* ── Store CTA — the shipped state of the hero ────────────────────────────────
   Replaces the notify-me form once a product has a real price. A two-segment
   badge cut from --page-text (so it inverts correctly on light accents, like
   the signup pill): Apple mark + "Download on the <store>" lockup on the left,
   the price in its own divided segment on the right, and a one-line
   reassurance underneath. Everything inside .store-buy is sized in em off its
   font-size, so the whole badge scales as one unit (the OG card scales it with
   a single rule). ── */
.store-cta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  margin-top: var(--space-lg);
}

.store-buy {
  font-size: 1rem;
  display: inline-flex;
  align-items: stretch;
  background: var(--page-text, #fff);
  color: var(--accent);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.2);
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
}

.store-buy:hover {
  transform: translateY(-2px);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.2);
}

.store-buy:active {
  transform: translateY(0);
  box-shadow: var(--shadow-sm);
}

.store-buy-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.5em 1em 0.5em 0.85em;
}

.store-buy-apple {
  width: 1.6em;
  height: 1.6em;
  flex: none;
  /* Optical: the glyph's baseline weight sits low; nudge up a hair. */
  transform: translateY(-0.05em);
}

.store-buy-store {
  display: flex;
  flex-direction: column;
  text-align: left;
}

.store-buy-eyebrow {
  font-size: 0.62em;
  font-weight: 600;
  letter-spacing: 0.03em;
  line-height: 1.2;
  opacity: 0.8;
}

.store-buy-name {
  font-weight: 700;
  letter-spacing: var(--tracking-snug);
  line-height: 1.15;
  white-space: nowrap;
}

.store-buy-price {
  display: inline-flex;
  align-items: center;
  padding: 0 0.95em;
  border-left: 1px solid oklch(from var(--accent) l c h / 0.2);
  font-size: 1.15em;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-snug);
}

.store-buy-note {
  margin: var(--space-sm) 0 0;
  font-size: var(--text-sm);
  font-weight: 500;
  color: oklch(from var(--page-text, #fff) l c h / 0.75);
}

/* Phones: the badge spans the content column, price pinned to the far edge —
   same full-width treatment the old hero button had. */
@media (max-width: 639px) {
  .store-cta {
    width: 100%;
  }

  .store-buy {
    width: 100%;
    justify-content: space-between;
  }
}

/* Static pages (legal, etc.) */
.page-main {
  flex: 1;
  padding: var(--space-lg) 0 var(--space-xl) 0;
}

.page-main .container {
  max-width: var(--container-md);
}

/* ── Top nav ──────────────────────────────────────────────────────────────────
   Brand left, the app's sections right. Sits on the accent on every app page,
   platform gallery and support page, so there's always a way out that isn't the
   back button. Colours come from --accent-text, which .product-page and
   .content-page both define. ── */
.page-nav {
  font-size: var(--text-sm);
  position: relative;
  z-index: 30;
}

/* The bar spans; only its contents are capped. Sizing the <nav> itself meant
   depending on flex-item stretch, which left it shrink-to-fit and huddled in
   the middle of the page. */
.page-nav-inner {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  max-width: 1200px;
  margin-inline: auto;
  padding: var(--space-md) var(--space-md) 0;
}

@media (min-width: 640px) {
  .page-nav-inner {
    padding-inline: var(--space-xl);
  }
}

.page-nav a {
  text-decoration: none;
  color: oklch(from var(--accent-text, var(--text)) l c h / 0.72);
  transition: color var(--transition-fast);
}

.page-nav a:hover {
  color: var(--accent-text, var(--text));
}

/* Set as the wordmark is: all caps, tracked out. */
.page-nav-brand {
  font-weight: 800;
  letter-spacing: 0.06em;
}

.page-nav-links {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.75em;
  font-weight: 600;
}

/* Hairline separators between sections, drawn rather than typed so there's
   never a stray leading or trailing one. */
.page-nav-link + .page-nav-link::before {
  content: "|";
  margin-right: 0.75em;
  color: oklch(from var(--accent-text, var(--text)) l c h / 0.35);
}

.page-nav-link.is-current {
  color: var(--accent-text, var(--text));
}

/* ── Content pages (support, trust, branding) ─────────────────────────────────
   A compact accent band instead of a full hero, then the body on a tint of the
   same accent — the treatment the product footer already uses, so a support
   page reads as part of its app rather than as a bare document. Support pages
   inherit their app's colour via ContentPage; site-level pages wear the brand's.

   The h1 is rendered by the layout from frontmatter, never by the content file. ── */
.content-page {
  --text-lightness: 97%;
  --text-chroma: 0.02;
  --accent-text: oklch(from var(--accent) var(--text-lightness) var(--text-chroma) h);
  --surface: var(--accent);
  /* Chroma is clamped to the accent's OWN, not a fixed value: a neutral accent
     (the brand's #111) has no hue to speak of, so a hardcoded chroma invented a
     pink cast on the trust pages. min() keeps colourful accents subtle and
     neutral ones neutral. */
  --tint: oklch(from var(--accent) 96% min(c, 0.012) h);
}

@media (prefers-color-scheme: dark) {
  html:not([data-ogplus]) .content-page {
    --surface: oklch(from var(--accent) calc(l * 0.92) calc(c * 0.7) h);
    --tint: oklch(from var(--accent) 14% min(c, 0.014) h);
  }
}

.content-header {
  background: var(--surface);
  padding: 0 0 var(--space-lg);
}

.content-header .container {
  padding-top: var(--space-xl);
}

.content-header .container {
  max-width: var(--container-md);
}

/* The qualifier half — "for Displays" — steps back in weight and opacity, the
   same two-tone the platform titles use ("Displays for macOS"). */
.content-title-qualifier {
  font-weight: 500;
  color: oklch(from var(--accent-text) l c h / 0.62);
}

.content-title {
  font-family: var(--font-display);
  font-size: clamp(var(--text-xl), 5.5vw, 3.5rem);
  text-wrap: balance;
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  line-height: 1.05;
  color: var(--accent-text);
  margin: 0.25em 0 0 0;
}

.content-subtitle {
  font-size: var(--text-md);
  line-height: 1.3;
  color: oklch(from var(--accent-text) l c h / 0.75);
  margin: 0.5em 0 0 0;
  max-width: 34rem;
  text-wrap: balance;
}

/* The body sits on a tint of the header's colour. Its text is the page's
   normal dark-on-light, so components written for the accent background (the
   store badge, its reassurance line) need --page-text pointed at the body's
   colours here — otherwise they render near-white on a near-white tint. */
.content-body {
  background: var(--tint);
  --page-text: var(--text);
  --page-text-secondary: var(--text-secondary);
}

.content-body .container {
  max-width: var(--container-md);
}

/* ── Feature page ─────────────────────────────────────────────────────────────
   The hero is a promoted showcase card (see Components::FeaturePage). ── */
/* Compound, because the generic .showcase sets a margin-top further down this
   file and would win on source order at equal specificity. */
.showcase.showcase--hero {
  /* .product-main already pads --space-lg above this; adding to it put the
     card 81px from the nav and 27px from the paragraph beneath. */
  margin-top: 0;
}

/* Bigger than a regular card: h1-scale title, roomier copy, and the art is
   allowed out of the card's RIGHT edge too, so a Mac window can sit at 1:1
   with its far side bleeding off rather than being shrunk to the column. */
/* The copy column has a real FLOOR: 24rem, enough for the title, the line and
   an un-truncated CTA. The window is what gives way, since it bleeds off the
   right anyway — at 2fr/3fr the copy was squeezed to ~260px, wrapping the
   title one word per line and cutting "Notify Me" to "Notify". */
.showcase-card.showcase-card--hero {
  grid-template-columns: minmax(24rem, 2fr) minmax(0, 3fr);
  align-items: center;
  min-height: 30rem;
  padding: var(--space-xl) 0 0 var(--space-xl);
}

.showcase-card--hero .showcase-copy {
  padding-bottom: var(--space-xl);
  padding-right: var(--space-lg);
  max-width: none;
}

.showcase-card--hero .showcase-name {
  font-size: clamp(var(--text-xl), 3.6vw, 3rem);
  line-height: 1.03;
}

.showcase-name-qualifier {
  font-weight: 500;
  color: var(--page-text-secondary, var(--text-secondary));
}

.showcase-card--hero .showcase-line {
  font-size: var(--text-md);
  line-height: 1.3;
  margin-top: 0.3em;
}

.showcase-cta {
  margin-top: var(--space-md);
}

/* The CTA components set their own top margin for the product hero. */
.showcase-cta .store-cta,
.showcase-cta .signup-wrapper {
  margin-top: 0;
}

/* 1:1 and bleeding. No cap on the anchor beyond its native size; the card
   clips whatever runs past the right edge, and the usual tuck handles the
   bottom. justify-content: flex-start so it starts at the column's left and
   runs OFF the right, not the other way round. */
.showcase-card--hero .showcase-art {
  justify-content: flex-start;
  overflow: visible;
  margin-bottom: -7cqw;
  min-width: 0;
}

.showcase-card--hero .showcase-shot {
  flex: 0 0 auto;
  width: var(--native, 100%);
  max-width: none;
}

.showcase-card--hero .showcase-shot img {
  width: 100%;
  max-width: none;
}

/* A portrait hero (phone, iPad) is narrow enough to fit its column, so it's
   centred rather than bled off the right, and capped by height so a 2778px
   phone doesn't set a two-metre hero. Orientation is a class the component
   sets; branching on the inline --ar string via :has([style*=]) was flaky. */
.showcase-card--hero.showcase-card--portrait .showcase-art {
  justify-content: center;
}

.showcase-card--hero.showcase-card--portrait .showcase-shot {
  width: auto;
  max-width: 100%;
}

.showcase-card--hero.showcase-card--portrait .showcase-shot img {
  /* The biggest this artwork gets anywhere on the site: it is the one image on
     a feature page. Capped at 1:1 as everywhere else. */
  height: min(var(--hero-art-h, 22rem), var(--native-h, 999px));
  width: auto;
  max-width: 100%;
}

/* The art column shrinks to the artwork. At 3fr a phone sat marooned in the
   middle of 800px of empty accent, which is what made a hero read as an
   afterthought on a wide screen. */
@media (min-width: 1100px) {
  .showcase-card.showcase-card--hero.showcase-card--portrait {
    grid-template-columns: minmax(20rem, 1fr) auto;
  }
}

@media (max-width: 1099px) {
  /* Matches the base rule's specificity. The base is .showcase-card
     .showcase-card--hero (two classes, so it outranks Solo); this was one, so
     it lost inside its own media query and the hero stayed two-column on a
     phone with a ZERO-width art column. Every feature page's hero image was
     invisible on mobile. */
  .showcase-card.showcase-card--hero {
    grid-template-columns: minmax(0, 1fr);
    min-height: 0;
    padding: var(--space-lg) var(--space-lg) 0;
  }

  /* Compound on the card: the hero IS a Solo card, and .showcase-card--solo
     .showcase-copy further down the file sets its own padding-bottom at equal
     specificity, so on source order it won and the copy kept a 54px skirt. */
  .showcase-card.showcase-card--hero .showcase-copy {
    padding: 0 0 var(--space-sm);
  }

  .showcase-card--hero .showcase-shot {
    width: 100%;
  }

  .showcase-card--hero .showcase-shot img {
    max-width: var(--native, none);
  }
}

.feature-page .product-content-card {
  /* One measure for the prose on this page: the lede spans it, and the article
     divides it into two columns. */
  --article-w: 58rem;
  /* Matches the space above the hero, so it sits evenly between the nav and
     the paragraph. */
  padding-top: var(--space-lg);
  padding-bottom: var(--space-lg);
}

.feature-page .product-lede {
  font-size: var(--text-lg);
  line-height: 1.3;
  font-weight: 500;
  /* Same measure as the article below, so the opening line spans the two
     columns rather than sitting narrow against them. */
  max-width: var(--article-w);
  margin-bottom: var(--space-lg);
  text-wrap: balance;
}

/* The feature's article. It uses the app pages' two-column treatment, but this
   column is narrower, so the switch happens later: two columns of a 25rem
   measure read as a newspaper, not as a page. */
/* The article carries the page: it is the only prose on it, and it was set at
   body size in a 34rem column against a 1240px page, which read as a caption
   somebody forgot to finish. Reading size, and a heading that separates the
   problem from the answer. */
.feature-article {
  margin-top: var(--space-lg);
  font-size: 1.0625rem;
  line-height: 1.55;
}

.feature-article h2 {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
  line-height: 1.2;
  margin: 0 0 0.4em;
  color: var(--page-text, var(--text));
}

.feature-article p {
  margin: 0 0 0.9em;
  color: oklch(from var(--page-text, #fff) l c h / 0.88);
  text-wrap: pretty;
}

@media (min-width: 900px) {
  .feature-page .feature-article {
    /* Two columns inside one measure, rather than as many 34rem columns as the
       page happens to fit. The page is 1186px wide here, and columns that ran
       its full width left the lede looking like a narrow block shoved against
       them. --article-w is the measure both share. */
    columns: 2;
    column-gap: var(--space-2xl);
    column-fill: balance;
    max-width: var(--article-w);
  }

  .feature-page .feature-article .prose-block {
    break-inside: avoid;
    /* Bottom margin, never top: a top margin on the block that starts column 2
       drops it below column 1's first heading and the columns lose their shared
       baseline. */
    margin-bottom: 1.5em;
  }

  .feature-page .feature-article .prose-block > *:first-child {
    margin-top: 0;
  }
}

.feature-page .platforms {
  margin-top: var(--space-md);
}

/* "More of <app>" on a feature page. The app page's own section has no heading
   — its lede is the setup — but here the index needs a label. */
.platforms-title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  color: var(--page-text, var(--text));
  margin: 0 0 var(--space-md);
}

/* Product page */
.product-page {
  --text-lightness: 97%;
  --text-chroma: 0.02;
  --accent-text: oklch(from var(--accent) var(--text-lightness) var(--text-chroma) h);
  --page-text: var(--accent-text);
  --page-text-secondary: oklch(from var(--accent) var(--text-lightness) var(--text-chroma) h / 0.85);
  --page-border: oklch(from var(--accent) var(--text-lightness) var(--text-chroma) h / 0.25);
  /* --surface = the accent, eased back (darker + desaturated) in dark mode via
     the block below. The page fill reads from it, matching the screens pages. */
  --surface: var(--accent);
  /* One surface for every text card on an app page (--card-shift comes from
     ProductPage#card_shift, so it lightens on a dark-text accent). */
  /* Chroma eased alongside lightness. Darkening alone kept the page's full
     saturation, so on a vivid accent the index tiles read as saturated blocks
     against a calmer page — the same jarring note the showcase cards had before
     they took a c * 0.88. */
  --card-surface: oklch(from var(--surface, var(--accent)) calc(l * var(--card-shift, 0.8)) calc(c * 0.88) h);
  background: var(--surface);
  position: relative;
}

/* ── Dark mode: ease every accent surface back ────────────────────────────────
   Same hue, a touch darker + desaturated, so the big color fields (product page,
   homepage tiles, footer app tiles) aren't glaring on a dark display. One formula,
   matching screens.css so the two stylesheets don't drift. Skipped under
   [data-ogplus] so share cards always render the raw brand accent. ── */
@media (prefers-color-scheme: dark) {
  html:not([data-ogplus]) .product-page,
  html:not([data-ogplus]) .product-tile {
    --surface: oklch(from var(--accent) calc(l * 0.92) calc(c * 0.7) h);
  }
}

.product-page.has-hero {
  overflow-x: hidden;
}

.has-hero .product-main {
  overflow-x: visible;
}

.product-page::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
  opacity: 0.03;
  pointer-events: none;
  z-index: 0;
}

.product-page > * {
  position: relative;
  z-index: 1;
}

.product-main {
  flex: 1;
  padding: var(--space-lg) 0;
}

.has-hero .product-main {
  padding: 0;
}

.product-main .container {
  max-width: var(--container-md);
}

/* Align the body column with the hero content column: same 1200px wrapper, and
   (on desktop) the same space-xl inset. Otherwise the centered narrow container
   reads as an indent under the left-aligned hero. The prose keeps its readable
   width, left-aligned. */
.has-hero .product-main .container {
  max-width: 1200px;
}

.has-hero .prose {
  position: relative;
  max-width: var(--container-md);
}

@media (min-width: 640px) {
  .has-hero .product-main .container {
    padding-inline: var(--space-xl);
  }
}

.product-header {
  margin-bottom: var(--space-xl);
}

.product-brand {
  display: block;
  font-size: var(--text-sm);
  font-weight: 800;
  color: var(--page-text-secondary);
  text-decoration: none;
  margin-bottom: var(--space-sm);
  transition: color var(--transition-fast);
}

.product-brand:hover {
  color: var(--page-text);
}

.product-brand-hero {
  display: block;
  font-size: var(--text-sm);
  font-weight: 800;
  letter-spacing: 0.05em;
  color: var(--page-text);
  text-decoration: none;
  margin-bottom: var(--space-sm);
  transition: opacity var(--transition-fast);
}

.product-brand-hero:hover {
  opacity: 0.8;
}

.product-page-title {
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 8vw, 4rem);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  line-height: 0.95;
  margin: 0;
  color: var(--page-text);
}

.product-page-tagline {
  font-size: var(--text-md);
  font-weight: 400;
  color: var(--page-text-secondary);
  margin: var(--space-sm) 0 0 0;
}

.product-header .btn {
  margin-top: var(--space-lg);
}

/* Prose */
.prose {
  font-size: var(--text-base);
  line-height: var(--line-height);
  color: var(--page-text, var(--text));
}

.prose > *:first-child {
  margin-top: 0;
}

.prose p {
  margin: 1em 0;
  color: var(--page-text-secondary, var(--text-secondary));
}

.prose h2 {
  font-size: var(--text-md);
  font-weight: 700;
  letter-spacing: var(--tracking-snug);
  margin: 1.5em 0 0.5em 0;
  color: var(--page-text, var(--text));
}

.prose h2:first-child {
  margin-top: 0;
}

.prose h3 {
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: var(--tracking-snug);
  margin: 1.5em 0 0.5em 0;
  color: var(--page-text, var(--text));
}

.prose strong {
  font-weight: 700;
  color: var(--page-text, var(--text));
}

.prose ul, .prose ol {
  margin: 1em 0;
  padding-left: 1.25em;
  color: var(--page-text-secondary, var(--text-secondary));
}

.prose li {
  margin: 0.25em 0;
}

.prose li::marker {
  color: var(--page-text-secondary, var(--text-secondary));
}

.prose a {
  text-decoration: underline;
  text-underline-offset: 0.15em;
  color: var(--page-text, var(--text));
}

.prose > figure:first-child {
  margin-top: 0;
}

.prose figure {
  margin: var(--space-lg) 0;
}

.prose figure img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  box-shadow: var(--shadow-md);
}

.prose figcaption {
  margin-top: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--page-text-secondary, var(--text-secondary));
  text-align: center;
}

/* Per-product support link, in the content so people find help per app */
/* Product page footer */
.product-footer {
  background: oklch(from var(--accent) 93% 0.03 h);
  padding: var(--space-md);
}

@media (prefers-color-scheme: dark) {
  html:not([data-ogplus]) .product-footer {
    background: oklch(from var(--accent) 15% 0.03 h);
  }
}

/* The footer mosaic frames the shared .mosaic grid; the grid itself, the tiles
   and the lockup are all the homepage's rules — see Components::Mosaic. */
.product-footer .mosaic {
  max-width: var(--container-lg);
  margin: 0 auto;
}

.product-footer .lockup-mark {
  color: var(--text);
}

.product-footer .lockup-tagline {
  color: var(--text-secondary);
}

.product-legal {
  background: var(--surface);
  padding: var(--space-md);
}

.product-legal .footer-social {
  margin-bottom: var(--space-sm);
}

.product-legal .social-link {
  color: var(--page-text-secondary);
}

.product-legal .social-link:hover {
  color: var(--page-text);
}

.product-legal .footer-legal {
  color: var(--page-text-secondary);
}

.product-legal .footer-legal a {
  color: var(--page-text-secondary);
  transition: color var(--transition-fast);
}

.product-legal .footer-legal a:hover {
  color: var(--page-text);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

.footer-legal {
  max-width: var(--container-md);
  margin: 0 auto;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-align: center;
}

.footer-legal a {
  color: inherit;
  text-decoration: none;
  transition: color var(--transition-fast);
}

.footer-legal a:hover {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

/* Product Hero */
.product-hero {
  --hero-image-rotation: -2deg;
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
  padding: var(--space-xl) var(--space-md);
  padding-bottom: 0;
  overflow: hidden;
  position: relative;
}

.product-hero::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: -50vw;
  right: -50vw;
  height: 0;
  box-shadow: 0 -7px 26px 13px rgba(0, 0, 0, 0.17);
  z-index: 25;
}

@media (min-width: 640px) {
  .product-hero {
    --hero-height: calc(var(--hero-image-height) - var(--pocket-depth));
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: var(--hero-image-height);
    height: var(--hero-height);
    gap: var(--space-md);
    max-width: 1200px;
    margin-inline: auto;
    padding: 0 var(--space-xl);
    padding-right: 0;
    overflow-x: visible;
    overflow-y: clip;
    position: relative;
  }

  /* The pocket seam. Two things have to be true for the window to read as
     tucked rather than cropped:

     z-index must beat the window's 20 — at 5 the window painted straight over
     the shadow, so its cut edge got no shading at all and looked pasted on.

     The shadow must throw UPWARD, onto the window. It used to offset +8px
     downward, but .product-hero clips its own overflow, so everything below the
     seam was cut away — leaving the shadow terminating in a hard line right at
     the seam. Throwing up keeps the whole gradient inside the clip, landing it
     where it belongs: across the window's base, densest at the cut. */
  .product-hero::after {
    content: "";
    position: absolute;
    bottom: 0;
    /* Bleed past the hero's 1200px box: the window is sized off its own height
       and hangs ~290px beyond the hero's right edge, so a seam that stopped at
       the box left the window's outer third cut with no shading on it. */
    left: -50vw;
    right: -50vw;
    /* Zero height, not 1px. box-shadow paints OUTSIDE the border box, so a 1px
       tall box punches a 1px unshaded line straight through the seam — visible
       as a bright hairline across the window's base. */
    height: 0;
    box-shadow: 0 -7px 26px 13px rgba(0, 0, 0, 0.17);
    z-index: 25;
  }
}

.product-hero-content {
  z-index: 1;
  min-width: 280px;
}

@media (min-width: 640px) {
  /* Centred in the hero — top-aligned left the lockup crowded against the nav
     with a lake of colour under it.
     Centring means the title's y depends on the hero's height, which comes from
     --hero-image-height (set per app from its hero capture). That's what used
     to make the top-left jump between an app page and its platform pages, so
     .platform-hero takes the same height for the same app and centres too: they
     line up because they're the same box, not because of a measured offset. */
  .product-hero .product-hero-content {
    align-self: stretch;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-bottom: var(--pocket-depth);
  }
}

/* ── Platform layout hero (compact, image-less) ───────────────────────────────
   Same 1200px container + space-xl inset as the product hero, so the back-link
   (in the eyebrow slot) and the title share the product page's top-left and don't
   shift. The title spans the content column; no image, no pocket seam. ── */
.platform-hero {
  max-width: 1200px;
  margin-inline: auto;
  padding: var(--space-xl) var(--space-md) 0;
  position: relative;
}
@media (min-width: 640px) {
  /* Compact and top-anchored. Matching the product hero's height and centring
     here was tried and dropped: it left a dead gap between the pills and the
     first screenshot panel, and it still didn't align, because the app page's
     lockup carries an App Store button this one doesn't — centring ties the
     title's position to the block's height, and the two blocks differ. */
  .platform-hero { padding: var(--hero-top) var(--space-xl) 0; }
}
/* No image here, so the content can use the full width — lets the title run on
   one line. The tagline stays at a readable measure. */
.platform-hero .product-hero-content { max-width: none; }
.platform-hero .product-hero-tagline { max-width: 34rem; }
/* Title runs the full width (one line where it fits); the platform part is muted. */
.platform-title { max-width: none; text-wrap: nowrap; }
.product-hero-title-platform { font-weight: 500; color: var(--page-text-secondary, rgba(255,255,255,0.6)); }

/* Below the desktop breakpoint the big title can't fit on one line — let it wrap. */
@media (max-width: 720px) {
  .platform-title { text-wrap: balance; }
}

.product-hero-title {
  font-family: var(--font-display);
  font-size: clamp(var(--text-2xl), 10vw, 5rem);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  line-height: 0.9;
  margin: 0;
  color: var(--page-text);
}

.product-hero-tagline {
  font-size: var(--text-lg);
  color: var(--page-text-secondary);
  margin: var(--space-xs) 0 0 0;
  /* Display-size text needs display leading; var(--line-height) is 1.5, which
     left Mirror's two-line tagline looking like two unrelated lines. */
  line-height: 1.22;
  text-wrap: balance;
}

.product-hero-image {
  position: relative;
  min-width: 0;
}

@media (min-width: 640px) {
  .product-hero-image {
    align-self: end;
  }
}

/* Narrow viewports: a clean, centered window tucked into the pocket.
   (The oversized 120%-width, off-to-the-right, rotated treatment read as
   broken on phones.) */
.product-hero-image picture img {
  display: block;
  width: auto;
  height: auto;
  max-width: min(100%, 480px);
  margin: 0 auto calc(var(--pocket-depth) * -1);
  transform: rotate(var(--hero-image-rotation));
}

@media (min-width: 640px) {
  .product-hero-image picture img {
    width: auto;
    /* The row is --hero-image-height tall and the window fills it, but NEVER
       past its own 1:1 size (--native is half the capture's pixel width; the
       captures are 2x). Sized by height alone the window crept 2-3% past
       native and softened. max-height bounds it in both axes at once. */
    height: var(--hero-image-height);
    max-width: var(--native, none);
    max-height: var(--hero-image-height);
    object-fit: contain;
    object-position: left bottom;
    margin: 0 0 0 10%;
    position: relative;
    z-index: 20;
    /* The window rises from the lower-right and tucks into the pocket: the
       row is --hero-image-height tall, the hero clips at
       (--hero-image-height - --pocket-depth), and the deliberate translateY
       pushes the window down so its base disappears into the pocket seam
       (poking up, bottom hidden). Proportional to the image height so the
       tuck reads the same on every product. Rotate around the bottom-left
       to keep the tuck line steady. */
    transform: rotate(var(--hero-image-rotation)) translateY(calc(var(--hero-image-height) * 0.18));
    transform-origin: bottom left;
  }
}

/* Portrait/mobile hero (phone shot): shown only on narrow viewports for
   products that ship one, replacing the desktop window shot. */
.hero-img-mobile {
  display: none;
}

/* ── Two devices in the hero ──────────────────────────────────────────────────
   On desktop a multi-platform app shows the big screen AND the phone: the phone
   sits in front of the window's lower-left corner, both rotated together, both
   tucking into the same pocket. The pairing is the pitch: big screen, little
   screen, same app. ── */
@media (min-width: 640px) {
  .product-hero-image.has-mobile {
    position: relative;
  }

  .product-hero-image.has-mobile .hero-img-mobile {
    display: block;
    position: absolute;
    left: 0;
    bottom: 0;
    z-index: 21;
    /* Height-bound so the phone reads as a phone beside the window, not as a
       second screen; never past 1:1. */
    width: auto;
  }

  .product-hero-image.has-mobile .hero-img-mobile img {
    height: calc(var(--hero-image-height) * 0.78);
    width: auto;
    max-width: min(var(--native, 100%), 100%);
    margin: 0;
    transform: rotate(var(--hero-image-rotation)) translateY(calc(var(--hero-image-height) * 0.18));
    transform-origin: bottom left;
    filter: drop-shadow(-10px 18px 30px rgba(0, 0, 0, 0.35));
  }

  /* The window steps right to make room for the phone in front of it. */
  .product-hero-image.has-mobile picture:not(.hero-img-mobile) img {
    margin-left: 22%;
  }
}

@media (max-width: 639px) {
  .product-hero-image.has-mobile picture:not(.hero-img-mobile) {
    display: none;
  }

  .product-hero-image.has-mobile .hero-img-mobile {
    display: block;
  }

  /* Wide phone that tucks into the pocket: the container clips (overflow) and
     the big negative bottom margin pulls the phone's base up out of view, so
     the seam lands ~57% up the phone (around "Kitchen Display") — the top with
     the app's title + first rows stays framed, the rest tucks below the pocket.
     The margin is a % of width, so the cut tracks the phone size on any phone. */
  .product-hero-image.has-mobile {
    overflow: hidden;
  }

  .product-hero-image.has-mobile .hero-img-mobile img {
    display: block;
    width: 84%;
    max-width: 84%;
    max-height: none;
    height: auto;
    margin: 0 auto -68%;
    transform: rotate(var(--hero-image-rotation));
  }
}

/* Social share card. OpenGraph+ adds html[data-ogplus] only while it
   screenshots the page (regular visitors never see it), and it renders at
   a viewport that would otherwise give the narrow, window-less layout.
   Force the wide "content left, app window peeking right" composition here
   with viewport units so it fills whatever size ogplus renders at. */
html[data-ogplus] .product-hero {
  display: grid;
  grid-template-columns: 1.18fr 1fr;
  grid-template-rows: 1fr;
  gap: 0;
  width: 100vw;
  height: 100vh;
  max-width: none;
  margin: 0;
  padding: 0 0 0 6.5vw;
  align-items: center;
  overflow: hidden;
}

html[data-ogplus] .product-hero::after {
  display: none;
}

html[data-ogplus] .product-hero-content {
  display: flex;
  flex-direction: column;
  justify-content: center;
  transform: none;
  z-index: 1;
  /* Generous top/bottom edge so the big type never feels crammed. */
  padding: 9vh 0;
}

/* The on-page hero title is clamped to 5rem for real layouts; the card has
   no such constraint, so let the title + subtitle grow to fill it. Sized in
   viewport units so it scales with whatever ogplus renders at. Tuned to fill
   for short names (Mics, Blocker) while keeping longer ones on one line. */
html[data-ogplus] .product-brand-hero {
  font-size: 1.8vw;
  letter-spacing: 0.3em;
  margin-bottom: 2.8vh;
}

html[data-ogplus] .product-hero-title {
  font-size: 10vw;
  line-height: 0.9;
  margin: 0;
}

html[data-ogplus] .product-hero-tagline {
  font-size: 3.6vw;
  line-height: 1.14;
  margin-top: 2.8vh;
  max-width: 24ch;
}

html[data-ogplus] .product-hero-image {
  align-self: stretch;
  height: 100vh;
  display: flex;
  align-items: flex-end;
  overflow: visible;
}

/* Use the dark window in the card (ogplus renders light mode, and the dark
   window reads better on the accent background). .og-window is hidden for
   regular visitors. */
.og-window,
.og-window img {
  display: none;
}

html[data-ogplus] .product-hero-image picture,
html[data-ogplus] .product-hero-image .hero-img-mobile {
  display: none;
}

/* Zoom the window and tuck its base into the pocket — the bottom of the OG
   viewport IS the pocket, so the window rises from the lower-right with its
   bottom bled off below the card (like the on-page hero), its top-left content
   staying framed. One template, every product (compose key UI up top-left). */
html[data-ogplus] .product-hero-image .og-window,
html[data-ogplus] .product-hero-image .og-window img {
  display: block;
  width: 178%;
  max-width: none;
  height: auto;
  margin: 0;
  /* translateX opens a gap between the text column and the window's left edge
     (it just bleeds a touch more off the right) — no risk of wrapping the big
     title, which a column gap or content padding would. */
  transform: rotate(var(--hero-image-rotation)) translate(6%, 19%);
  transform-origin: bottom left;
}

/* The inverse of ogplus:hidden — present only in the share card. The BORINGWARE
   eyebrow lives here: on-page the nav carries the brand, but a share image has
   no nav, so the card still needs it above the app name. */
.og-only {
  display: none;
}

html[data-ogplus] .og-only {
  display: block;
}

/* ogplus:hidden utility — the OpenGraph+ Tailwind variant, done in plain
   CSS: hide the element only while ogplus captures the share card. */
html[data-ogplus] .ogplus\:hidden {
  display: none;
}

/* The buy badge on the share card: scale the whole em-sized lockup with one
   rule to sit under the vw-sized tagline; the reassurance line stays on-page
   only. */
html[data-ogplus] .store-cta {
  margin-top: 3.5vh;
}

html[data-ogplus] .store-buy {
  font-size: 4vh;
}

html[data-ogplus] .store-buy-note {
  display: none;
}

/* Homepage share card. The live homepage is a tall mosaic of squares, which
   overflows and clips in the short, wide OG viewport. Reuse the 4-column
   desktop layout — wordmark block + 8 apps = 12 cells = 3 rows — but drop the
   square ratio and let the rows divide the card height evenly, so the whole
   mosaic fits the card no matter how long the list gets. */
html[data-ogplus] .home-main {
  display: block;
  height: 100vh;
  min-height: 100vh;
  padding: 3vh 3vw;
  overflow: hidden;
}

html[data-ogplus] .mosaic {
  max-width: none;
  height: 94vh;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: minmax(0, 1fr);
  gap: 1.4vh;
}

html[data-ogplus] .mosaic-header,
html[data-ogplus] .product-tile {
  aspect-ratio: auto;
  min-height: 0;
}

html[data-ogplus] .mosaic-header {
  grid-column: span 2;
  grid-row: span 2;
  padding: 0 2vw;
}

html[data-ogplus] .lockup-mark {
  font-size: 9vh;
  line-height: 0.9;
}

html[data-ogplus] .lockup-tagline {
  font-size: 2.6vh;
  margin-top: 1.2vh;
}

html[data-ogplus] .product-tile-text {
  padding: 2vh;
}

html[data-ogplus] .product-name {
  font-size: 3.6vh;
}

html[data-ogplus] .product-tagline {
  font-size: 2.2vh;
  line-height: 1.2;
  -webkit-line-clamp: 2;
  margin-top: 0.25em;
}

/* The card's cells are much wider than they are tall, so the crop is sized off
   the tile's own width here rather than the square-tile ratio. */
html[data-ogplus] .product-tile-shot {
  margin-left: 6%;
  width: 92%;
}

html[data-ogplus] .product-tile-mark {
  width: 22%;
}

html[data-ogplus] .site-footer {
  display: none;
}

/* Content card below hero */
.has-hero .product-content-card {
  position: relative;
  z-index: 10;
  background: var(--surface);
  padding-top: var(--space-xl);
  padding-bottom: var(--space-xl);
}

.has-hero .product-content-card > *:first-child {
  margin-top: 0;
}

/* ── Product body: fill the page ──────────────────────────────────────────────
   The shared .container is a 540px measure, which is right for support and
   trust pages but left the app pages as a thin ribbon down the left of a wide
   display. Widen the card and flow the body into two columns, so the copy
   fills the space it's given instead of stranding half the viewport.

   Scoped to .product-page so support/trust keep their single readable measure.
   The lede spans both columns; headings stay welded to the text under them. ── */
@media (min-width: 1000px) {
  /* Same descendant depth as .has-hero .product-main .container so this wins on
     source order rather than needing a specificity hack. */
  .product-page .product-main .container {
    max-width: var(--container-xl);
  }

  /* .has-hero .prose caps the measure at 640px for the single-column layout;
     the two columns supply their own measure, so the cap comes off. */
  .product-page .prose {
    max-width: none;
    columns: 2;
    column-gap: var(--space-2xl);
  }

  /* A heading and the content under it are one unit — see ProseHelper.
     break-after: avoid on the heading alone is not honoured by Chrome in
     multicol, which is how "Details" ended up alone at the foot of a column. */
  .product-page .prose .prose-block {
    break-inside: avoid;
    /* The gap between blocks is a BOTTOM margin, never a top one. With
       `+ .prose-block { margin-top }`, the block that happens to start column 2
       still had a preceding sibling in the DOM, so it carried the margin and
       sat 1.5em lower than column 1 — the two columns' first headings didn't
       share a baseline. Bottom margins are dropped at a column break, so
       whichever block lands at the top of a column sits flush. */
    margin-bottom: 1.5em;
  }

  .product-page .prose .prose-block > *:first-child {
    margin-top: 0;
  }

  .product-page .prose .prose-block:last-child {
    margin-bottom: 0;
  }

  .product-page .prose p,
  .product-page .prose ul,
  .product-page .prose ol,
  .product-page .prose figure {
    break-inside: avoid;
  }

  /* The opening paragraph reads as a lede across the full width. */
  .product-page .prose > p:first-child {
    column-span: all;
    font-size: var(--text-md);
    line-height: 1.45;
    max-width: 46rem;
    margin-bottom: var(--space-lg);
  }
}

/* ── The lede ─────────────────────────────────────────────────────────────────
   The pitch, one paragraph, set larger than the body copy. First thing under
   the hero, before anything asks the reader to work. ── */
.product-lede {
  font-size: var(--text-md);
  line-height: 1.4;
  color: var(--page-text, var(--text));
  max-width: 46rem;
  margin: 0 0 var(--space-lg);
  text-wrap: pretty;
}

/* ── Platform sections ────────────────────────────────────────────────────────
   One block per platform the app ships on: its name, what it runs on, and its
   App Store screens as a swipeable rail. The tiles are the very same ones the
   capture pipeline renders — sized in container-query units, so they compose
   correctly at rail size without a second set of styles. ── */
.platforms {
  margin: var(--space-lg) 0;
}

/* ── The closer ───────────────────────────────────────────────────────────────
   Centred, unlike everything above it, because a change of rhythm is what says
   "this is the end of the argument". Sits on the same hairline the spec sheet
   uses so it belongs to the page. ── */
/* One centred column, evenly spaced: title, line, form, with the same gap
   between each and the same breathing room above and below. Before, a
   --space-xl margin stacked on a --space-xl padding under the prose's own
   bottom margin, and the form sat a long way under its title. */
.closing {
  border-top: 1px solid oklch(from var(--page-text, #fff) l c h / 0.16);
  /* The content card above already pads its bottom; a margin here stacked on
     that, and the rule sat in a no-man's-land 135px below the last paragraph. */
  margin-top: 0;
  padding: var(--space-xl) 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  text-align: center;
}

.closing-title {
  font-family: var(--font-display);
  font-size: clamp(var(--text-lg), 3.4vw, 2.5rem);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  line-height: 1.1;
  color: var(--page-text, var(--text));
  margin: 0;
  text-wrap: balance;
}

/* Pulled up under the title (the flex gap is for the form, not for this). */
.closing-line {
  font-size: var(--text-base);
  line-height: 1.45;
  color: var(--page-text-secondary, var(--text-secondary));
  margin: calc(var(--space-md) * -1 + 0.4em) 0 0;
  max-width: 34rem;
  text-wrap: pretty;
}

.closing-cta {
  display: flex;
  justify-content: center;
  margin-top: 0;
}

/* The signup/store CTA carries its own top margin for the hero; not here. */
.closing-cta .store-cta,
.closing-cta .signup-wrapper {
  margin-top: 0;
}

/* The CTA is built left-aligned for the hero; centre its innards here. */
.closing-cta .store-cta,
.closing-cta .signup-wrapper {
  align-items: center;
  text-align: center;
}

/* ── The compact index ────────────────────────────────────────────────────────
   The "more of this app" grid on a feature page. These are the OTHER features
   and must not outweigh the one you came to see, so they're thumbnails — not
   the showcase cards. Without these rules the images rendered at full size and
   a feature page ran to 22,000px. ── */
.shots-index {
  list-style: none;
  margin: var(--space-md) 0 0;
  padding: 0;
  display: grid;
  /* auto-FIT, not auto-fill: an app with three features leaves two tiles on
     each feature page, and auto-fill kept the empty tracks so the pair huddled
     at 15rem with a hole to their right. auto-fit collapses the empties and the
     tiles stretch to fill the row, whether there are two of them or six. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: var(--space-md);
}

.shot-thumb {
  min-width: 0;
  /* The container for the tile's own layout query. It sits here, on the list
     item, because .shot-thumb-link is what the query styles and an element
     cannot query itself. */
  container-type: inline-size;
}

/* An index tile is a small card: caption at the top, the app window tucked into
   the pocket below and cropped by the tile's own bottom edge. Same idiom as the
   homepage mosaic. It used to be a padded screenshot with the caption floating
   underneath, which read as two loose objects instead of one tile. */
.shot-thumb-link {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  text-decoration: none;
  color: inherit;
  background: var(--card-surface);
  border-radius: calc(var(--radius-md) * 1.2);
  padding: 1rem 1rem 0;
  overflow: hidden;
  /* A definite height, not a minimum: the artwork has to overflow for the tile
     to crop it. With min-height the flex column shrank the image instead, and a
     phone came out 355x176 — squashed, not pocketed. */
  --tile-h: 20rem;
  height: var(--tile-h);
  transition: transform var(--transition-fast);
}

@media (min-width: 700px) {
  .shot-thumb-link {
    --tile-h: 18rem;
  }
}

@media (min-width: 1100px) {
  .shot-thumb-link {
    --tile-h: 20rem;
  }
}

.shot-thumb-link:hover {
  transform: translateY(-2px);
}

.shot-thumb-text {
  display: block;
  min-width: 0;
}

.shot-thumb-sub {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
  margin-top: 0.3rem;
  font-size: var(--text-sm);
  line-height: 1.35;
  color: oklch(from var(--page-text, #fff) l c h / 0.7);
}

/* A wide tile puts the copy beside the artwork instead of above it. Stacked, a
   full-width tile was a line of text, a phone on the left, and a large hole to
   its right. Container query, not a media query: what matters is how wide THIS
   tile is, and the same tile is full width on a phone and a third of a row on a
   desktop. */
@container (min-width: 28rem) {
  .shot-thumb-link {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr);
    align-items: end;
    gap: 1rem;
  }

  .shot-thumb-text {
    align-self: start;
  }

  .shot-thumb-shot {
    margin-top: 0;
    align-self: end;
    justify-self: center;
  }
}

/* The window sits on the floor of the tile and runs off it. A portrait phone
   shows its top two thirds; a Mac window its top edge and toolbar, which is
   the part that identifies the screen. */
.shot-thumb-shot {
  display: block;
  margin-top: auto;
  align-self: center;
  flex: none;
  max-width: 100%;
}

/* Sized by the TILE'S height, not by its width. Width-sizing made a full-width
   tile on a phone hold an enormous image, and the fixed-height tile then showed
   a band across the top of it: you got a notch and half a toolbar. Pinning the
   art to a fraction over the tile height means the same generous slice of the
   device shows at every width, whatever its shape. */
.shot-thumb-shot img {
  display: block;
  /* Width, with height following the aspect ratio, so it can never distort.
     The target is a fraction over the tile height (so a slice crops off the
     bottom), converted to a width through --ar, and capped at the tile's own
     width so a wide Mac window is not clipped on both sides. */
  width: min(100%, calc(var(--tile-h) * 1.25 * var(--ar, 1)));
  height: auto;
  border-radius: calc(var(--radius-sm) * 0.8) calc(var(--radius-sm) * 0.8) 0 0;
}

.shot-thumb-link:hover picture {
  transform: translateY(-2px);
}

.shot-thumb-name {
  display: block;
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: var(--tracking-tight);
  color: var(--page-text, var(--text));
}

.shot-thumb-device {
  display: block;
  margin-top: 0.35rem;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: oklch(from var(--page-text, #fff) l c h / 0.42);
}

/* ── The showcase: one card per feature ───────────────────────────────────────
   Components::FeatureCards renders one of three cards by device count. The
   three share a surface and a copy treatment; they differ in where the art
   goes, because one, two and three devices want different compositions.
   The variation tracks something real, so the page has rhythm without
   pretending to. ── */
.showcase {
  /* Breaks out of the reading column: a Mac window is a three-pane UI and at
     66% of a 1240px measure it rendered ~400px wide, illegible. */
  width: min(94vw, 1700px);
  margin-inline: calc(50% - min(47vw, 850px));
  container-type: inline-size;
  margin-top: var(--space-md);
  /* A two-track grid: full-width cards span both, Slim cards take one, so two
     phone-only features sit side by side instead of each stranding a lake of
     colour beside a single device. */
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-md);
}

/* Shared: the surface and the copy. */
.showcase-card {
  /* Height budget for the artwork; see "How app windows are sized" above. */
  --art-h: 22rem;
  --hero-art-h: 22rem;
  grid-column: span 2;          /* full width; Slim overrides to 1 */
  /* A calm step from the page, not --card-surface: that one is 20% darker so
     the feature TEXT cards pass AA, and on a card that mostly holds artwork the
     same shift reads as a hard block. */
  /* From --surface, not --accent: the page eases its accent back in dark mode
     (darker, desaturated), and a card computed from the raw accent came out
     BRIGHTER and more saturated than the page around it. --surface already
     carries the mode. */
  background: oklch(from var(--surface, var(--accent)) calc(l * 0.93) calc(c * 0.88) h);
  border-radius: calc(var(--radius-md) * 1.6);
  overflow: hidden;            /* the fold: art runs past the bottom, card clips */
  /* Safari does not apply a rounded overflow clip to composited children (the
     hover transform promotes the shots to layers), so the artwork painted
     straight past the card's edges there. Isolating the card forces the clip
     onto its own context. */
  isolation: isolate;
  transform: translateZ(0);
  display: grid;
  gap: var(--space-lg);
  padding: var(--space-lg) var(--space-lg) 0;
}

.showcase-copy {
  max-width: 44rem;
  min-width: 0;
}

.showcase-name {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  line-height: 1.1;
  color: var(--page-text, var(--text));
  margin: 0;
  text-wrap: balance;
}

.showcase-line {
  font-size: var(--text-base);
  line-height: 1.4;
  color: var(--page-text-secondary, var(--text-secondary));
  margin: 0.25em 0 0;
  text-wrap: pretty;
}

.showcase-art {
  display: flex;
  align-items: flex-end;
  gap: 1.5cqw;
  min-width: 0;
  /* The tuck: 7cqw of the art sits below the fold, but never more than 30% of
     the shortest shot — a fixed cut swallowed small captures whole. */
  margin-bottom: calc(-1 * min(7cqw, var(--min-art-h, 999px) * 0.3));
}

/* ── How app windows are sized ────────────────────────────────────────────────
   One rule for every device on every page. The artwork is the point of a card,
   so it gets a HEIGHT BUDGET that grows with the viewport, and a width ceiling
   at its own 1:1 size. Both are maxima on an auto-sized image, so it fits the
   box without distorting, and a row of a Mac, an iPad and a phone lines up at a
   common height with each one's width falling out of its shape — which is what
   makes a lineup read as a lineup.

   This replaced a per-variant ceiling in cqw (46cqw for solo, 38 for pair, 36
   for trio...). Those are width-derived, so a tall phone came out tiny: 18% of
   its card next to a Mac window, and 0 on a narrow screen. Height is the honest
   limit for portrait devices; width is the honest limit for landscape ones.
   Budgeting height and capping width lets one rule serve both. ── */
@media (min-width: 700px) {
  .showcase-card {
    --art-h: 26rem;
    /* Still stacked at this width, so a tall phone would otherwise fill the
       card and push the copy off the top of it. */
    --hero-art-h: 24rem;
  }
}

@media (min-width: 1100px) {
  .showcase-card {
    --art-h: 32rem;
    --hero-art-h: 38rem;
  }
}

.showcase-card:not(.showcase-card--hero) .showcase-shot img {
  width: auto;
  height: auto;
  max-width: min(100%, var(--native, 100%));
  max-height: min(var(--art-h), var(--native-h, 9999px));
}

.showcase-shot {
  display: block;
  min-width: 0;
  transition: transform var(--transition-fast);
}

.showcase-shot:hover {
  transform: translateY(-4px);
}

.showcase-shot img {
  display: block;
  width: 100%;
  height: auto;
  /* Never past 1:1, whatever the variant above decides. */
  max-width: var(--native, none);
  border-radius: 0.4rem;
}

/* ── Solo: one device ─────────────────────────────────────────────────────────
   Copy and window side by side. The window takes the bigger column and its
   width is WHATEVER THE COLUMN GIVES IT — the previous rule set its height to
   40cqw, which at ~1100px made a Mac window half the card with empty space
   beside it. Width-driven, it fills the column at every size. ── */
.showcase-card--solo {
  /* The copy needs ~14rem; everything else is the window's. */
  grid-template-columns: minmax(12rem, 1fr) minmax(0, 3fr);
  align-items: center;
}

.showcase-card--solo .showcase-copy {
  padding-bottom: var(--space-lg);
}

.showcase-card--solo .showcase-art {
  justify-content: flex-end;
}

.showcase-card--solo .showcase-shot {
  flex: 1 1 auto;
  width: 100%;
}


/* ── Slim: one portrait device, half width ────────────────────────────────────
   Copy above, the phone or iPad bottom-right, tucked into the fold. Half the
   showcase wide, so two mobile-only features share a row. ── */
.showcase-card--slim {
  grid-column: span 1;
  grid-template-columns: minmax(0, 1fr);
  align-content: start;
}

/* Centred under the copy, not pushed to a corner — at half width there's no
   diagonal to make, so a corner just reads as off-balance. */
.showcase-card--slim .showcase-art {
  justify-content: center;
}

.showcase-card--slim .showcase-shot {
  flex: 0 1 auto;
}

/* ── Pair: two devices ────────────────────────────────────────────────────────
   Copy left, the two windows stacked on a shared baseline to the right. Widths
   proportional to shape (flex-grow IS the aspect ratio) so both land on one
   height, and the pair right-aligned so the gap sits under the copy. ── */
.showcase-card--pair {
  grid-template-columns: minmax(0, 3fr) minmax(0, 7fr);
  /* Copy reads from the top, like every other card — bottom-aligned it sat in
     the corner under a column of empty colour. The art still sits on the fold. */
  align-items: start;
}

.showcase-card--pair .showcase-art {
  align-self: end;
}

.showcase-card--pair .showcase-copy {
  padding-bottom: var(--space-xl);
}

.showcase-card--pair .showcase-art {
  justify-content: flex-end;
}

/* Basis by aspect ratio so a row shares one height and each shape takes the
   width it needs. */
.showcase-card--pair .showcase-shot {
  flex: var(--ar, 1) 1 0;
}

/* ── Trio: three devices ──────────────────────────────────────────────────────
   Mac, iPad and phone need the whole width, so the copy goes ABOVE and the row
   runs across the full card on one baseline. ── */
.showcase-card--trio {
  grid-template-columns: minmax(0, 1fr);
}

.showcase-card--trio .showcase-shot {
  flex: var(--ar, 1) 1 0;
}

/* ── Narrow: every variant stacks ─────────────────────────────────────────────
   Below ~900px there's no room for a side-by-side; copy above, art below, and
   a lone Mac window is full width — it was half-width with space beside it. ── */
@media (max-width: 899px) {
  .showcase-card,
  .showcase-card--solo,
  .showcase-card--pair {
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
  }

  .showcase-card--solo .showcase-copy,
  .showcase-card--pair .showcase-copy {
    padding-bottom: 0;
  }

}

@media (max-width: 767px) {
  .showcase {
    width: auto;
    margin-inline: 0;
    grid-template-columns: minmax(0, 1fr);
  }

  /* The desktop rhythm reads as dead air in a phone-width column, where the
     stacked field and button already take two rows: pull the CTA up under the
     line it belongs to and tighten the rows to a tap target rather than a
     tap target plus change. */
  .showcase-cta {
    margin-top: var(--space-sm);
  }

  .signup-input,
  .signup-button {
    padding-block: calc(var(--space-sm) * 0.8);
  }

  /* Two stacked rows in one narrow column: the desktop row gap between the
     copy and the artwork reads as a hole here. */
  .showcase-card.showcase-card--hero {
    gap: var(--space-md);
  }

  /* The feature-page hero is a direct child of .product-main, which has no
     horizontal padding — margin-inline: 0 ran it edge-to-edge on a phone.
     Same gutter as .container, so it aligns with the content card below. */
  .showcase.showcase--hero {
    margin-inline: var(--space-md);
  }

  .showcase-card,
  .showcase-card--slim {
    grid-column: span 1;
  }

  .showcase-card {
    padding: var(--space-md) var(--space-md) 0;
  }

  .showcase-card--trio .showcase-art,
  .showcase-card--pair .showcase-art {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-sm);
  }

  /* The cqw ceilings above are desktop card-rhythm tuning: they keep neighbouring
     cards the same height on a wide screen. On a phone there are no neighbours,
     and they starve the art — a Mac window came out 217px inside a 336px card,
     and a portrait phone shot barely a fifth of it. Fill the card instead, still
     never past 1:1 so nothing blurs. */
  .showcase-card--trio .showcase-shot,
  .showcase-card--pair .showcase-shot,
  .showcase-card--solo .showcase-shot {
    max-width: min(100%, var(--native, 100%));
  }

  /* Portrait art is the exception: full width would make a phone screenshot
     twice as tall as the text it illustrates. */
  .showcase-card--slim .showcase-shot,
  .showcase-card--portrait .showcase-shot {
    max-width: min(66%, var(--native, 100%));
    margin-inline: auto;
  }
}

/* ── Feature cards ────────────────────────────────────────────────────────────
   Scannable cards instead of a wall of prose. The surface is the accent shifted
   AWAY from the page's text colour — darker under the usual near-white text,
   lighter under dark text on a light accent — so the tint raises contrast
   instead of eroding it. --card-shift comes from ProductPage#card_shift and is
   verified against WCAG AA for every shipping accent. ── */
.features {
  margin: var(--space-lg) 0;
}

.features-title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  color: var(--page-text, var(--text));
  margin: 0 0 var(--space-md);
}

.features-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-sm);
}

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

.feature-card {
  /* Span set per card by Components::FeatureGrid so every row is full. */
  grid-column: span var(--cols, 4);
  background: var(--card-surface);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  /* A hairline of the page text at low alpha, so the card still has an edge on
     accents where the tint lands close to the background. */
  box-shadow: inset 0 0 0 1px oklch(from var(--page-text, #fff) l c h / 0.10);
}

.feature-icon {
  display: block;
  width: 1.6rem;
  height: 1.6rem;
  color: var(--page-text, var(--text));
  opacity: 0.9;
  margin-bottom: var(--space-sm);
}

.feature-name {
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: var(--tracking-snug);
  color: var(--page-text, var(--text));
  margin: 0 0 0.25em;
}

.feature-body {
  font-size: var(--text-sm);
  line-height: 1.4;
  /* Full-strength page text, not the secondary tone: this is the copy the
     cards exist to make readable. */
  color: var(--page-text, var(--text));
  margin: 0;
}

/* The features block spans the body's full width rather than joining the
   two-column prose flow. */
@media (min-width: 1000px) {
  .product-page .features { column-span: all; }
}

/* ---- Brand guidelines page ---- */
.brand-lede {
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--page-text, var(--text));
  margin: var(--space-md) 0 var(--space-lg);
}

.brand-logo-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  margin: var(--space-md) 0 var(--space-lg);
}
.brand-logo-cell {
  flex: 1 1 220px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  min-height: 140px;
  padding: var(--space-lg);
  border: 1px solid var(--border);
  border-radius: 12px;
}
.brand-wordmark {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--text-lg);
  letter-spacing: 0.02em;
  color: var(--text);
}
.brand-cap {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
}
/* Fixed light/dark lockup tiles: show the wordmark + B mark on each
   background regardless of the viewer's theme, so both treatments are
   always visible on the branding page. */
.brand-logo-cell--light {
  background: #ffffff;
  border-color: rgba(0, 0, 0, 0.15);
}
.brand-logo-cell--light .brand-wordmark { color: #000000; }
.brand-logo-cell--light .brand-cap { color: rgba(0, 0, 0, 0.6); }
.brand-logo-cell--dark {
  background: #000000;
  border-color: rgba(255, 255, 255, 0.15);
}
.brand-logo-cell--dark .brand-wordmark { color: #ffffff; }
.brand-logo-cell--dark .brand-cap { color: rgba(255, 255, 255, 0.65); }

.swatch-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--space-sm);
  margin: var(--space-md) 0 var(--space-lg);
}
.swatch-grid-neutral { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.swatch {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 12px;
  overflow: hidden;
  text-decoration: none;
  color: var(--text);
}
.swatch-chip {
  height: 88px;
  background: var(--sw);
}
.swatch-chip-bordered { box-shadow: inset 0 0 0 1px var(--border); }
.swatch-name {
  font-weight: 700;
  font-size: var(--text-sm);
  padding: var(--space-sm) var(--space-sm) 0;
}
.swatch-hex {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  padding: 0 var(--space-sm) var(--space-sm);
}

.type-specimens { margin: var(--space-md) 0 var(--space-lg); }
.type-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  border-bottom: 1px solid var(--border);
}
.type-sample { color: var(--text); line-height: 1.15; }
.type-meta {
  flex: none;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  white-space: nowrap;
}

.space-scale { margin: var(--space-md) 0 var(--space-lg); }
.space-row {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-xs) 0;
}
.space-bar {
  height: 16px;
  background: var(--text);
  border-radius: 3px;
  flex: none;
}

.brand-principles {
  margin: var(--space-md) 0 var(--space-md);
  padding-left: 1.1em;
}
.brand-principles li { margin: var(--space-xs) 0; }

.brand-tokens {
  background: var(--code-bg, rgba(127,127,127,0.08));
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: var(--space-md);
  overflow-x: auto;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text);
}

/* ── Backstage ────────────────────────────────────────────────────────────────
   Brand collateral, not a page for visitors: no nav, no footer, no accent
   flood. A plain sheet so the artwork is the only colour on it. ── */
.backstage {
  max-width: var(--container-xl);
  margin: 0 auto;
  padding: var(--space-lg) var(--space-md) var(--space-xl);
  color: var(--text);
}

.backstage-head {
  margin-bottom: var(--space-lg);
}

.backstage-head h1 {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  margin: 0;
}

.backstage-head p {
  color: var(--text-secondary);
  margin: 0.25em 0 0;
}

.backstage-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 10rem), 1fr));
  gap: var(--space-md);
}

.backstage-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: var(--space-md) 0.5rem;
  border-radius: var(--radius-md);
  text-decoration: none;
  color: inherit;
  transition: transform var(--transition-fast), background var(--transition-fast);
}

.backstage-tile:hover {
  transform: translateY(-2px);
  background: rgba(127, 127, 127, 0.08);
}

.backstage-tile-icon {
  width: 96px;
  height: 96px;
  border-radius: 22%;
}

.backstage-tile-name {
  font-weight: 700;
  text-align: center;
}

.backstage-tile-meta,
.backstage-meta {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.backstage-app-head {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

.backstage-app-head h1 {
  margin: 0;
}

.backstage-icon {
  border-radius: 22%;
}

.backstage-section {
  margin-top: var(--space-lg);
}

.backstage-section h2 {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-sm);
}

.backstage-icons {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
}

.backstage-icon-swatch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  text-decoration: none;
}

.backstage-icon-swatch img {
  border-radius: 22%;
}

.backstage-note {
  margin: var(--space-sm) 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.backstage-shots {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
  gap: var(--space-md);
}

/* Light and dark side by side, on the accent, which is where both are meant to
   be seen. Any difference between them is the point of this page. */
.backstage-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
  background: var(--accent, #333);
  border-radius: var(--radius-md);
  overflow: hidden;
  padding: 0.5rem;
}

.backstage-frame {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  align-items: center;
  min-width: 0;
}

.backstage-frame img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 4px;
}

.backstage-frame figcaption {
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
}

.backstage-frame--missing {
  display: grid;
  place-items: center;
  min-height: 6rem;
  font-size: var(--text-sm);
  color: rgba(255, 255, 255, 0.55);
  text-align: center;
}

.backstage-shot {
  min-width: 0;
}

.backstage-shot-sub {
  margin: 0.1rem 0 0.35rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* The listing copy, collapsed by default: it is long, and most visits here are
   to look at pictures. */
.backstage-listing {
  margin: 0 0 var(--space-md);
  font-size: var(--text-sm);
}

.backstage-listing summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--text-secondary);
}

.backstage-listing dl {
  margin: var(--space-sm) 0 0;
  display: grid;
  grid-template-columns: minmax(8rem, max-content) minmax(0, 1fr);
  gap: 0.4rem var(--space-md);
  align-items: baseline;
}

.backstage-listing dt {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  font-weight: 600;
  text-transform: capitalize;
  color: var(--text-secondary);
}

.backstage-listing dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}

.backstage-longform {
  white-space: pre-wrap;
  max-height: 12rem;
  overflow: auto;
}

.backstage-shot-name {
  margin: 0.5rem 0 0.15rem;
  font-weight: 600;
}

.backstage-flags {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.flag {
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: 0.15em 0.45em;
  border-radius: 4px;
  background: rgba(127, 127, 127, 0.16);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The whole reason the page exists: something that needs looking at. */
.flag--warn {
  background: #b91c1c;
  color: #fff;
}

.backstage-empty {
  color: var(--text-secondary);
}

/* Backstage, part two: enough structure to read as a tool. Hairline rules
   instead of boxes, monospace for anything you would paste, and a switcher
   pinned to the top so moving between apps is one click. */
.backstage-bar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  padding: 0.9rem 0;
  margin-bottom: var(--space-lg);
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.backstage-bar-title p {
  margin: 0.1rem 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.backstage-switcher {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  justify-content: flex-end;
  list-style: none;
  margin: 0;
  padding: 0;
}

.backstage-switcher-home {
  display: inline-block;
  font-size: var(--text-md);
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: var(--tracking-tight);
  text-decoration: none;
  color: var(--text);
}

.backstage-switcher-app {
  display: block;
  line-height: 0;
  border-radius: 8px;
  padding: 3px;
  opacity: 0.45;
  transition: opacity var(--transition-fast);
}

.backstage-switcher-app:hover {
  opacity: 1;
}

.backstage-switcher-app.is-current {
  opacity: 1;
  outline: 2px solid var(--text);
  outline-offset: 1px;
}

.backstage-switcher-app img {
  border-radius: 6px;
}

.backstage-tagline {
  margin: 0.1rem 0 0.3rem;
  color: var(--text-secondary);
}

.backstage-swatch {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  border-radius: 3px;
  margin-right: 0.4em;
  vertical-align: -0.05em;
}

.backstage-links {
  list-style: none;
  margin: var(--space-md) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.backstage-link {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
  color: var(--text);
}

.backstage-link:hover {
  background: rgba(127, 127, 127, 0.1);
}

.backstage-link--muted {
  color: var(--text-secondary);
  font-weight: 400;
}

.backstage-section h2 {
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--border);
}

.backstage-accent {
  display: flex;
  gap: var(--space-md);
  align-items: flex-start;
  flex-wrap: wrap;
}

.backstage-accent-swatch {
  width: 7rem;
  height: 7rem;
  border-radius: var(--radius-md);
  flex: none;
}

.backstage-formats {
  margin: 0;
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 0.3rem var(--space-md);
  align-items: baseline;
  font-size: var(--text-sm);
}

.backstage-formats dt {
  color: var(--text-secondary);
  font-weight: 600;
}

.backstage-formats dd {
  margin: 0;
  min-width: 0;
}

/* Everything here exists to be pasted somewhere else. */
.backstage-copy {
  font: inherit;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  background: none;
  border: 0;
  padding: 0.1rem 0.3rem;
  margin-left: -0.3rem;
  border-radius: 4px;
  color: inherit;
  cursor: copy;
  text-align: left;
}

.backstage-copy:hover {
  background: rgba(127, 127, 127, 0.14);
}

.backstage-copy.is-copied {
  background: #15803d;
  color: #fff;
}

.backstage-listing h3 {
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
  margin: 0 0 var(--space-sm);
}

.backstage-listing dd {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.86rem;
}

/* ── Backstage on a phone ─────────────────────────────────────────────────────
   The desktop layout is two-column everywhere: label beside value. At 390px
   that leaves the value about five characters wide, so the listing copy wrapped
   two words per line and was unreadable — which is the one thing this page is
   for. Everything stacks below 700px, and the switcher becomes a scrolling
   strip rather than three rows of small targets. ── */
@media (max-width: 699px) {
  .backstage {
    padding-left: var(--space-sm);
    padding-right: var(--space-sm);
  }

  /* One line, scrolled sideways, with targets big enough to hit. */
  .backstage-bar {
    flex-direction: column;
    align-items: stretch;
    gap: 0.6rem;
    padding: 0.7rem 0;
  }

  .backstage-switcher {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 2px;
    margin-right: calc(var(--space-sm) * -1);
  }

  .backstage-switcher::-webkit-scrollbar {
    display: none;
  }

  .backstage-switcher-app {
    padding: 4px;
  }

  .backstage-switcher-app img {
    width: 36px;
    height: 36px;
  }

  .backstage-app-head {
    gap: var(--space-sm);
  }

  .backstage-icon {
    width: 64px;
    height: 64px;
  }

  .backstage-links {
    gap: 0.4rem;
  }

  .backstage-link {
    padding: 0.45rem 0.8rem;
  }

  /* Label above value, value full width: the only way the copy is legible. */
  .backstage-listing dl,
  .backstage-formats {
    display: block;
  }

  .backstage-listing dt,
  .backstage-formats dt {
    margin-top: 0.9rem;
    font-size: 0.7rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
  }

  .backstage-listing dt:first-child,
  .backstage-formats dt:first-child {
    margin-top: 0;
  }

  .backstage-listing dd,
  .backstage-formats dd {
    margin-top: 0.15rem;
  }

  .backstage-longform {
    max-height: none;
  }

  .backstage-accent {
    gap: var(--space-sm);
  }

  .backstage-accent-swatch {
    width: 100%;
    height: 5rem;
  }

  .backstage-copy {
    display: block;
    width: 100%;
    padding: 0.35rem 0.4rem;
    margin-left: -0.4rem;
    background: rgba(127, 127, 127, 0.08);
  }

  /* The icon sizes are a reference strip, not a gallery. */
  .backstage-icons {
    gap: var(--space-sm);
    overflow-x: auto;
    scrollbar-width: none;
  }

  .backstage-icons::-webkit-scrollbar {
    display: none;
  }

  .backstage-icons li {
    flex: none;
  }

  .backstage-section {
    margin-top: var(--space-md);
  }

  /* Light and dark stay side by side: comparing them is the whole point, and
     a phone screenshot at half of 390px is still legible enough to spot a
     cream slab where a dark one belongs. */
  .backstage-pair {
    padding: 0.35rem;
    gap: 0.35rem;
  }

  .backstage-grid {
    grid-template-columns: repeat(auto-fill, minmax(min(50%, 8rem), 1fr));
    gap: var(--space-sm);
  }

  .backstage-tile-icon {
    width: 72px;
    height: 72px;
  }
}

/* The links list: every URL the app has, each one ready to send. */
.backstage-urls {
  list-style: none;
  margin: 0;
  padding: 0;
}

.backstage-url {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
}

.backstage-url-text {
  min-width: 0;
}

.backstage-url-name {
  display: block;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
}

.backstage-url-name:hover {
  text-decoration: underline;
}

.backstage-url-note {
  margin-left: 0.45rem;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.backstage-url-path {
  display: block;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.backstage-url-actions {
  display: flex;
  gap: 0.35rem;
  flex: none;
}

.backstage-share,
.backstage-copy--compact {
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  font-family: inherit;
  padding: 0.35rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: none;
  color: var(--text);
  cursor: pointer;
  margin-left: 0;
}

.backstage-share:hover,
.backstage-copy--compact:hover {
  background: rgba(127, 127, 127, 0.1);
}

.backstage-share.is-copied,
.backstage-copy--compact.is-copied {
  background: #15803d;
  border-color: #15803d;
  color: #fff;
}

@media (max-width: 699px) {
  /* Thumb-sized targets, and the path under the name rather than beside it. */
  .backstage-url {
    padding: 0.7rem 0;
  }

  .backstage-share,
  .backstage-copy--compact {
    padding: 0.5rem 0.9rem;
  }
}
