/* ============================================================
   PHPColor stylesheet orchestrator.
   Declares the cascade layer order, then imports the system.
   Each foundation file owns its own @layer declarations.
   Components and layout are placed via layer() in @import.
   ============================================================ */

@layer reset, tokens, base, typography, accents, components, layout, page;

/* ============================================================
   FOUNDATION
   Tokens, reset, base elements, typography, accent system.
   The stable core. Changes here cascade to every page.
   ============================================================ */


/* ─────────────────────────────────────────────────────────────
   TOKENS
   ───────────────────────────────────────────────────────────── */
@layer tokens {
  @property --hue-brand {
    syntax: "<number>";
    inherits: true;
    initial-value: 239;
  }

  :root {
    color-scheme: dark;

    /* Brand */
    --hue-brand: 239;
    --brand: oklch(0.585 0.233 var(--hue-brand));
    --rainbow-rose:   oklch(0.78 0.22 350);
    --rainbow-orange: oklch(0.80 0.20 45);
    --rainbow-amber:  oklch(0.84 0.18 85);
    --rainbow-green:  oklch(0.78 0.20 145);
    --rainbow-blue:   oklch(0.76 0.20 220);
    --rainbow-violet: oklch(0.76 0.22 300);
    --scale-green-accent: oklch(0.70 0.172807 149.57933);
    --brand-soft: color-mix(in oklch, var(--brand) 12%, transparent);
    --brand-glow: color-mix(in oklch, var(--brand) 35%, transparent);

    /* Surfaces */
    --surface-0: oklch(0.06 0.004 var(--hue-brand));
    --surface-1: oklch(0.09 0.006 var(--hue-brand));
    --surface-2: oklch(0.12 0.008 var(--hue-brand));
    --surface-3: oklch(0.15 0.010 var(--hue-brand));
    --surface-void: oklch(0 0 0);

    /* Text tiers */
    --text-0: oklch(0.97 0.005 var(--hue-brand));
    --text-1: oklch(0.82 0.015 var(--hue-brand));
    --text-2: oklch(0.68 0.015 var(--hue-brand));
    --text-3: oklch(0.55 0.012 var(--hue-brand));

    /* Borders */
    --border-subtle: color-mix(in oklch, var(--text-0) 6%, transparent);
    --border-default: color-mix(in oklch, var(--text-0) 10%, transparent);

    /* Chrome glass: shared recipe for the fixed/sticky bars
       (.site-nav, .subnav) so they always read as one unit. */
    --glass-bg: color-mix(in oklch, var(--surface-0) 70%, transparent);
    --glass-blur: blur(20px) saturate(1.4);

    /* Fonts */
    --font-body: 'Outfit', system-ui, sans-serif;
    --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
    /* Interface mono (file trees, TOCs, filters): the system mono
       first for a quieter, native feel; code blocks keep --font-mono. */
    --font-mono-ui: ui-monospace, 'Roboto Mono', 'SF Mono', Menlo, monospace;

    /* Weight scale. Dark-on-dark thickens strokes optically: prefer
       one step lighter than a light-theme instinct would pick (body
       copy and link titles sit at regular, not medium). */
    --weight-thin:       100;
    --weight-extralight: 200;
    --weight-light:      300;
    --weight-regular:    400;
    --weight-medium:     500;
    --weight-semibold:   600;
    --weight-bold:       700;

    /* Type scale (modular ~1.25) */
    --text-xs:   0.8125rem;
    --text-sm:   0.9375rem;
    --text-base: 1.0625rem;
    --text-lg:   1.1875rem;
    --text-xl:   1.3125rem;
    --text-2xl:  1.625rem;
    --text-3xl:  2rem;
    --text-4xl:  2.375rem;
    --text-5xl:  3.125rem;
    --text-6xl:  3.75rem;
    --text-7xl:  4.5rem;
    --text-8xl:  6rem;

    /* Spacing scale */
    --space-1:  0.25rem;
    --space-2:  0.5rem;
    --space-3:  0.75rem;
    --space-4:  1rem;
    --space-5:  1.25rem;
    --space-6:  1.5rem;
    --space-7:  1.75rem;
    --space-8:  2rem;
    --space-10: 2.5rem;
    --space-12: 3rem;
    --space-16: 4rem;
    --space-20: 5rem;
    --space-24: 6rem;
    --space-32: 8rem;
    --space-40: 10rem;

    /* Radius */
    --radius-sm:   0.375rem;
    --radius-md:   0.5rem;
    --radius-lg:   0.75rem;
    --radius-xl:   1rem;
    --radius-full: 9999px;

    /* Motion */
    --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
    --duration-fast:   150ms;
    --duration-normal: 350ms;
    --duration-slow:   600ms;

    /* Layout */
    --nav-h: 56px;
    --gutter: clamp(1.5rem, 5vw, 4rem);
    --layout-sidebar-width: 20rem;
    --layout-sidebar-width-compact: 18.75rem;
    --layout-rail-width-compact: 14.375rem;
    --layout-guide-measure: 46rem;

    /* Accent (the source color for accent scoping) */
    --accent: oklch(0.60 0.22 var(--hue-brand));
  }

  /* Accent derivations.
     Declared on :root AND on every accent scope so they
     re-compute when --accent is overridden in the cascade. */
  :root,
  [class*="accent-"],
  body[class*="part-"] {
    --accent-soft:          oklch(from var(--accent) l c h / 0.12);
    --accent-border:        oklch(from var(--accent) l c h / 0.30);
    --accent-text:          oklch(from var(--accent) calc(l + 0.18) calc(c * 0.82) h);
    --accent-surface:       oklch(from var(--accent) 0.14 0.03 h);
    --accent-surface-hover: oklch(from var(--accent) 0.18 0.04 h);
    --accent-glow:          oklch(from var(--accent) l c h / 0.35);
    --accent-bg-subtle:     oklch(from var(--accent) l c h / 0.05);
  }
}


/* ─────────────────────────────────────────────────────────────
   RESET
   ───────────────────────────────────────────────────────────── */
@layer reset {
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  html {
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    scroll-behavior: smooth;
  }

  ul, ol { list-style: none; }
  img, svg, video { display: block; max-width: 100%; }
  button { background: none; border: none; cursor: pointer; font: inherit; color: inherit; }

  ::selection {
    background: color-mix(in oklch, var(--brand) 25%, transparent);
    color: var(--text-0);
  }
}


/* ─────────────────────────────────────────────────────────────
   BASE ELEMENTS
   ───────────────────────────────────────────────────────────── */
@layer base {
  body {
    font-family: var(--font-body);
    font-weight: 300;
    color: var(--text-1);
    background: var(--surface-0);
    font-size: var(--text-base);
    line-height: 1.7;
    overflow-x: hidden;
  }

  code {
    font-family: var(--font-mono);
    font-size: 0.9em;
  }

  /* Inline code inside running text: a whisper of the section accent
     as background, the code itself light and vibrant in the same hue.
     Scoped to prose containers so data-mono usages (color cards, API
     signature lines) keep their bare treatment. */
  :is(p, li, dd, td) > code {
    display: inline-flex;
    padding: 0.125rem 0.25rem;
    margin-inline-end: 0.125rem;
    border-radius: var(--radius-sm);
    background: var(--accent-soft);
    color: oklch(from var(--accent) 0.82 c h);
    font-size: 0.82em;
  }

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

  /* Skip link: off-screen until focused, then anchored over the nav.
     Clipped rather than display:none so it stays in the tab order. */
  .skip-link {
    position: absolute;
    inset-inline-start: var(--space-4);
    inset-block-start: var(--space-4);
    z-index: 200;
    padding: var(--space-3) var(--space-5);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    color: var(--text-0);
    font-size: var(--text-sm);
    font-weight: 500;
    clip-path: inset(50%);
    transform: translateY(-150%);
  }

  .skip-link:focus-visible {
    clip-path: none;
    transform: none;
    outline: 2px solid var(--brand);
    outline-offset: 2px;
  }

  ::-webkit-scrollbar { width: 5px; height: 5px; }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb {
    background: var(--surface-2);
    border-radius: 3px;
  }
  ::-webkit-scrollbar-thumb:hover { background: var(--surface-3); }
}


/* ─────────────────────────────────────────────────────────────
   TYPOGRAPHY
   Semantic type classes. Role-based, not element-based.
   Display > Title > Headline > Body > Eyebrow > Caption > Label > Mono.
   ───────────────────────────────────────────────────────────── */
@layer typography {

  /* Display: hero-scale, one per page. */
  .display-xl {
    font-family: var(--font-body);
    font-size: clamp(2.75rem, 7vw, 5rem);
    font-weight: 100;
    letter-spacing: -0.05em;
    line-height: 0.95;
    color: var(--text-0);
  }
  .display-lg {
    font-family: var(--font-body);
    font-size: clamp(2.5rem, 6vw, 4rem);
    font-weight: 100;
    letter-spacing: -0.04em;
    line-height: 1;
    color: var(--text-0);
  }
  .display-md {
    font-family: var(--font-body);
    font-size: clamp(2rem, 4vw, 3rem);
    font-weight: 100;
    letter-spacing: -0.03em;
    line-height: 1.05;
    color: var(--text-0);
  }
  .display-sm {
    font-family: var(--font-body);
    font-size: clamp(1.5rem, 3vw, 2.25rem);
    font-weight: 200;
    letter-spacing: -0.02em;
    line-height: 1.1;
    color: var(--text-0);
  }

  /* Title: section headings. */
  .title-lg {
    font-family: var(--font-body);
    font-size: clamp(var(--text-3xl), 3.4vw, var(--text-5xl));
    font-weight: 200;
    letter-spacing: -0.02em;
    line-height: 1.2;
    color: var(--text-0);
  }
  .title-md {
    font-family: var(--font-body);
    font-size: var(--text-2xl);
    font-weight: 300;
    letter-spacing: -0.01em;
    line-height: 1.25;
    color: var(--text-0);
  }
  .title-sm {
    font-family: var(--font-body);
    font-size: var(--text-xl);
    font-weight: 400;
    line-height: 1.3;
    color: var(--text-0);
  }

  /* Headline: component-level headings. */
  .headline {
    font-family: var(--font-body);
    font-size: var(--text-lg);
    font-weight: 400;
    line-height: 1.35;
    color: var(--text-0);
  }
  .headline-sm {
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-weight: 500;
    line-height: 1.4;
    color: var(--text-0);
  }

  /* Body: prose. Carries its own measure (max-width). */
  .body-lg {
    font-family: var(--font-body);
    font-size: var(--text-lg);
    font-weight: 300;
    line-height: 1.75;
    color: var(--text-1);
    max-width: 60ch;
  }
  .body {
    font-family: var(--font-body);
    font-size: var(--text-base);
    font-weight: 300;
    line-height: 1.7;
    color: var(--text-1);
    max-width: 55ch;
  }
  .body-sm {
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 300;
    line-height: 1.65;
    color: var(--text-1);
    max-width: 55ch;
  }

  /* Eyebrow: pre-heading label. Mono, uppercase, accent-colored. */
  .eyebrow {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent-text);
    line-height: 1;

    .separator { color: var(--text-0); }
  }
  .eyebrow-sm {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--accent-text);
    line-height: 1;
  }

  /* Caption: supporting text below media or in fine print. */
  .caption {
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 300;
    color: var(--text-2);
    line-height: 1.5;
  }
  .caption-sm {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    font-weight: 400;
    color: var(--text-2);
    line-height: 1.5;
  }

  /* Label: UI chrome (form labels, button text, table headers). */
  .label {
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--text-1);
    line-height: 1.4;
  }
  .label-sm {
    font-family: var(--font-body);
    font-size: var(--text-xs);
    font-weight: 500;
    color: var(--text-2);
    line-height: 1.4;
    text-transform: uppercase;
    letter-spacing: 0.08em;
  }

  /* Mono: code, technical identifiers, version numbers. */
  .mono {
    font-family: var(--font-mono);
    font-size: 0.92em;
    font-weight: 400;
    color: var(--text-1);
  }
  .mono-sm {
    font-family: var(--font-mono);
    font-size: 0.82em;
    font-weight: 400;
    color: var(--text-2);
  }

  /* Emphasis rules, scoped to type tiers via :where for low specificity. */
  :where(.display-xl, .display-lg, .display-md, .display-sm,
         .title-lg, .title-md, .title-sm) {
    strong {
      font-weight: 600;
      color: var(--text-0);
    }
    em {
      font-style: normal;
      color: var(--accent-text);
    }
  }

  :where(.body-lg, .body, .body-sm) {
    strong {
      font-weight: 500;
      color: var(--text-0);
    }
    a {
      color: var(--accent-text);
      text-decoration: underline;
      text-decoration-color: var(--accent-border);
      text-underline-offset: 0.15em;
      transition: text-decoration-color var(--duration-fast);
    }
    a:hover {
      text-decoration-color: var(--accent-text);
    }
    code {
      background: var(--surface-1);
      padding: 0.15em 0.4em;
      border-radius: var(--radius-sm);
      font-weight: 400;
    }
  }
}


/* ─────────────────────────────────────────────────────────────
   ACCENT SCOPES
   One class on a container sets --accent (a full oklch color).
   The derivations declared in @layer tokens re-compute via the
   [class*="accent-"] selector. Children inherit automatically.
   ───────────────────────────────────────────────────────────── */
@layer accents {
  .accent-brand   { --accent: oklch(0.60 0.22 var(--hue-brand)); }
  .accent-rose    { --accent: oklch(0.60 0.18 350); }
  .accent-pink    { --accent: oklch(0.60 0.18 330); }
  .accent-green   { --accent: oklch(0.60 0.18 145); }
  .accent-lime    { --accent: oklch(0.60 0.18 125); }
  .accent-blue    { --accent: oklch(0.60 0.18 220); }
  .accent-cyan    { --accent: oklch(0.60 0.18 195); }
  .accent-violet  { --accent: oklch(0.60 0.18 300); }
  .accent-indigo  { --accent: oklch(0.60 0.18 270); }
  .accent-amber   { --accent: oklch(0.60 0.18 85); }
  .accent-orange  { --accent: oklch(0.60 0.18 45); }

  /* ── Section accents ──
     One accent per header destination, taken from the scale above so a
     section's colour is the same tone as the tool cards that use it:
     the nav link renders --accent-text, oklch(0.78 0.1476 h), which is
     exactly the colour of a "Launch" link on /tools.

     The earlier values sat at oklch(0.72 0.25 h) and derived to
     oklch(0.90 0.205 h), which read washed out next to those links.
     Matching the scale, rather than the logo petals, is what makes the
     menu and the cards agree.

     Applied on <body> from active_nav, so it colours the chrome --
     nav, sidebars, footer -- while any .accent-* container inside a
     page still overrides it locally.

     Contrast of the rendered link runs 9.80 to 11.01 on --surface-0. */
  body.part-php        { --accent: oklch(0.60 0.18 300); } /* violet */
  body.part-color      { --accent: oklch(0.60 0.18 45); }  /* orange */
  body.part-docs       { --accent: oklch(0.60 0.22 var(--hue-brand)); }
  /* Tools and Playground sit on the brand blue: the per-section hue
     experiment read as a mistake there, the product areas carry the
     brand instead. */
  body.part-tools      { --accent: oklch(0.60 0.22 var(--hue-brand)); }
  body.part-playground { --accent: oklch(0.60 0.22 var(--hue-brand)); }
  body.part-blog       { --accent: oklch(0.60 0.18 330); } /* pink   */
}

@layer components {
/* ============================================================
   LOGO
   PHPColor brand logo, rendered from an inline SVG sprite defined
   once in base.html.twig. Three variants share a single component:

     <twig:Logo />                  -> default 'lockup' (mark + wordmark)
     <twig:Logo variant="mark" />   -> the OKLCH flower only
     <twig:Logo variant="wordmark" /> -> the "phpcolor" letters only

   Sizing is height-driven. The component accepts a `size` attribute
   that maps to `style="height: ..."`. Width follows from each
   variant's aspect-ratio.

   Color: the mark carries fixed OKLCH fills per petal (brand-locked).
   The wordmark uses `fill="currentColor"` and inherits the parent's
   CSS color. The lockup combines both — the wordmark portion of
   the lockup also inherits currentColor.
   ============================================================ */

/* The sprite host element must not render. The <symbol> children
   never render anyway, but we strip the container too so it does
   not contribute to layout. */
.logo-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.logo {
  display: inline-block;
  vertical-align: middle;
  color: var(--text-0);
}

/* Mark only -- square. Multicolor (brand-fixed). */
.logo-mark {
  height: 1.5rem;
  width: 1.5rem;
  flex-shrink: 0;
}

/* Wordmark only -- 791:107 aspect, inherits currentColor. */
.logo-wordmark {
  height: 1.125rem;
  width: auto;
  aspect-ratio: 791 / 107;
}

/* Lockup -- mark + wordmark with brand spacing. 917:107 aspect. */
.logo-lockup {
  height: 1.5rem;
  width: auto;
  aspect-ratio: 917 / 107;
}
}
@layer components {
/* ============================================================
   SITE NAV
   One nav, used everywhere. 56px height, three zones:
   logo / links / right-side (version + GitHub).

   The default is fixed + glassmorphic, used by marketing and
   article pages. Inside an app shell (.api-shell, .docs-shell,
   .tool-shell) or the playground body, the same nav switches
   to in-flow positioning automatically -- no modifier prop on
   the component itself.
   ============================================================ */

.site-nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-inline: var(--space-6);

  /* Glass lives on a pseudo-element so .site-nav itself does NOT become the
     containing block for the fixed mobile overlay. A backdrop-filter on the
     element traps position:fixed descendants (.nav-links) inside the 56px bar
     instead of letting them fill the viewport. */
  &::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: var(--glass-bg);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
  }
}

/* In-flow inside an app shell: relative position, hairline border,
   and (for grid shells) span every column so it spans full width
   like the topbars it replaces. */
.api-shell > .site-nav,
.docs-shell > .site-nav,
.tool-shell > .site-nav,
body.ctx-playground > .site-nav {
  position: relative;
  inset: auto;
  border-bottom: 1px solid var(--border-subtle);

  &::before {
    background: color-mix(in oklch, var(--surface-0) 90%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }
}
.api-shell > .site-nav,
.docs-shell > .site-nav,
.tool-shell > .site-nav {
  grid-column: 1 / -1;
}

.nav-logo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-0);
  text-decoration: none;

  /* Tighter sizing inside the nav: smaller wordmark, mark stays a
     touch bigger than cap-height so the flower reads first. */
  & .logo-mark     { height: 1.25rem; width: 1.25rem; }
  & .logo-wordmark { height: 0.95rem; }
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-4); /* 1rem gap */
}

/* One colour for every destination, separated by weight of presence
   rather than hue: resting links sit at 0.85, the hovered one and the
   current one come to full. The section accent still drives the rest of
   the chrome, it just does not tint the menu. */
/* Entries that only exist in the full-screen overlay, not in the bar. */
.nav-link--menu-only {
  display: none;
}

.nav-link {
  display: inline-block;
  padding-inline: var(--space-4); /* 1rem horizontal padding */
  font-size: var(--text-base);
  font-weight: 300;
  color: var(--text-0);
  opacity: 0.85;
  transition: opacity var(--duration-fast);
  text-decoration: none;

  /* Holds the width the label needs at weight 500, so going bold on hover
     or on the current page never reflows the row. The bar is centred and
     Turbo navigations are client-side, so without this the whole menu
     shifts under the pointer and again on every page change. A zero-height
     block child still counts toward the shrink-to-fit width. */
  &::after {
    content: attr(data-label);
    display: block;
    height: 0;
    overflow: hidden;
    visibility: hidden;
    font-weight: 500;
  }

  &:hover,
  &[aria-current="page"],
  &.active {
    opacity: 1;
    font-weight: 500;
  }
}

/* Once a destination is current, its siblings step further back so the
   contrast between them widens. Without a current link every item stays
   at the resting 0.85, so pages outside the menu are not dimmed for no
   reason. Hover is excluded from the dim rather than fought with a
   higher-specificity override: a hovered sibling stops matching this
   selector, and the 1 above applies again. */
.nav-links:has(.nav-link[aria-current="page"], .nav-link.active)
  .nav-link:not([aria-current="page"], .active, :hover) {
  opacity: 0.7;
}

.nav-side {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

/* Square icon-only links in the nav (GitHub, X, sponsor heart). All
   three share the same shape so the right-hand cluster reads as a
   single icon group. Default state is muted; hover lifts the color
   to text-0 and applies a subtle scale-up. */
.nav-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  color: var(--text-3);
  transition: color var(--duration-fast),
              background var(--duration-fast),
              transform var(--duration-fast) var(--ease-out-expo);

  svg { display: block; }

  &:hover {
    color: oklch(1 0 0);
    background: var(--surface-2);
    transform: scale(1.08);
  }
  &:active { transform: scale(1); }
  &:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }
}

/* Sponsor heart: pink stroke-only outline at rest. Hover swaps the
   outline for a solid pink fill -- a small "favourite" gesture that
   reads as approval / endorsement. Distinct from GitHub/X so the
   call-to-sponsor reads immediately. */
.nav-icon--heart {
  color: oklch(0.68 0.18 0);

  svg path {
    fill: none;
    transition: fill var(--duration-fast);
  }

  &:hover {
    color: oklch(0.78 0.22 0);
    background: transparent;

    svg path { fill: currentColor; }
  }
}

/* ============================================================
   MOBILE: collapse to a full-screen glass overlay.

   Below 768px the inline clusters hide behind .nav-burger.
   Opening (.is-open on the nav) turns .nav-links into a
   viewport-filling glass layer that reuses the bar's exact
   material; .nav-side rides on top near the bottom. Only
   .nav-burger is added markup -- the links are not duplicated.
   ============================================================ */

.nav-burger {
  display: none; /* desktop: hidden. Shown in the media query below. */
}

@media (width <= 768px) {
  .nav-burger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
    z-index: 110; /* above the overlay so the X stays tappable */
    width: 40px;
    height: 40px;
    margin-inline-start: auto;
    padding: 0;
    color: var(--text-1);
    background: none;
    border: 0;
    cursor: pointer;

    &:focus-visible {
      outline: 2px solid var(--accent, var(--text-0));
      outline-offset: 2px;
      border-radius: var(--radius-sm);
    }
  }

  .nav-burger-box {
    position: relative;
    width: 20px;
    height: 14px;
  }

  .nav-burger-line {
    position: absolute;
    left: 0;
    width: 100%;
    height: 2px;
    background: currentColor;
    border-radius: var(--radius-full);
    transition: top var(--duration-fast) var(--ease-out-expo),
                transform var(--duration-fast) var(--ease-out-expo),
                opacity var(--duration-fast) var(--ease-out-expo);
  }
  .nav-burger-line:nth-child(1) { top: 0; }
  .nav-burger-line:nth-child(2) { top: 6px; }
  .nav-burger-line:nth-child(3) { top: 12px; }

  /* burger -> X */
  .site-nav.is-open .nav-burger-line:nth-child(1) { top: 6px; transform: rotate(45deg); }
  .site-nav.is-open .nav-burger-line:nth-child(2) { opacity: 0; }
  .site-nav.is-open .nav-burger-line:nth-child(3) { top: 6px; transform: rotate(-45deg); }

  /* .nav-links -> full-screen glass overlay, hidden until open.
     Sits just under the bar so the two glass layers tile the
     viewport with one continuous frosted surface. */
  .nav-links {
    position: fixed;
    inset: var(--nav-h) 0 0 0;
    flex-direction: column;
    align-items: center;
    /* Anchored to the top rather than centred: the list starts right
       under the bar instead of floating mid-screen. */
    justify-content: flex-start;
    gap: var(--space-6);
    padding: var(--space-10) var(--space-6) var(--space-8);
    background: var(--glass-bg);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);

    opacity: 0;
    visibility: hidden;
    transform: scale(0.98) translateY(-8px);
    pointer-events: none;
    transition: opacity var(--duration-normal) var(--ease-out-expo),
                transform var(--duration-normal) var(--ease-out-expo),
                visibility 0s linear var(--duration-normal);
  }

  .site-nav.is-open .nav-links {
    opacity: 1;
    visibility: visible;
    transform: none;
    pointer-events: auto;
    transition: opacity var(--duration-normal) var(--ease-out-expo),
                transform var(--duration-normal) var(--ease-out-expo),
                visibility 0s;
  }

  .nav-link {
    font-size: var(--text-2xl);
    padding-block: var(--space-2);
  }

  .nav-link--menu-only {
    display: block;
  }

  /* .nav-side (social icons) rides on top, near the bottom, with
     finger-sized targets. */
  .nav-side {
    position: fixed;
    inset: auto 0 calc(var(--space-8) + var(--space-4)) 0;
    justify-content: center;
    gap: var(--space-6);

    opacity: 0;
    visibility: hidden;
    transform: translateY(8px);
    pointer-events: none;
    transition: opacity var(--duration-normal) var(--ease-out-expo),
                transform var(--duration-normal) var(--ease-out-expo),
                visibility 0s linear var(--duration-normal);
  }

  .nav-icon {
    width: 48px;
    height: 48px;

    svg {
      inline-size: 26px;
      block-size: 26px;
    }
  }

  .site-nav.is-open .nav-side {
    opacity: 1;
    visibility: visible;
    transform: none;
    pointer-events: auto;
    transition: opacity var(--duration-normal) var(--ease-out-expo),
                transform var(--duration-normal) var(--ease-out-expo),
                visibility 0s;
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav-burger-line,
  .nav-links,
  .nav-side {
    transition-duration: 0s;
  }
}
}
@layer components {
/* ============================================================
   SUBNAV
   Section-level secondary navigation (About, later others).
   Same visual language as .tool-tabs: compact bar, pill links,
   accent pill on the current page. Borderless: the shared
   chrome glass does the separation.

   Sticky right under the fixed SiteNav on every page of the
   section. On overflow the bar scrolls horizontally (no wrap);
   the `subnav` Stimulus controller centres the current link.
   ============================================================ */

.subnav {
  position: sticky;
  top: var(--nav-h);
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: safe center;
  /* No gap: the slash between links is the separator. */
  gap: 0;
  /* A secondary bar under a 56px primary one. The bar itself adds no height
     beyond its links, which carry their own padding so the tap target stays
     usable. Previously the two bars stacked to 106px of chrome before any
     page content, and the subnav read as a second primary bar. */
  padding-inline: var(--space-4);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  overflow-x: auto;
  max-width: 100%;
  min-width: 0;
  scrollbar-width: none;

  &::-webkit-scrollbar { width: 0; height: 0; background: transparent; }

  &.has-overflow.at-start:not(.at-end) {
    mask-image: linear-gradient(to right, currentColor 0, currentColor calc(100% - var(--space-12)), transparent 100%);
  }

  &.has-overflow.at-end:not(.at-start) {
    mask-image: linear-gradient(to right, transparent 0, currentColor var(--space-12), currentColor 100%);
  }

  &.has-overflow:not(.at-start, .at-end) {
    mask-image: linear-gradient(to right, transparent 0, currentColor var(--space-12), currentColor calc(100% - var(--space-12)), transparent 100%);
  }
}

/* The fixed nav is out of flow: start below the bar. */
.site-nav + .subnav {
  margin-top: var(--nav-h);
}

/* App shells keep both navigation bars inside their grid. */
.api-shell > .subnav,
.docs-shell > .subnav {
  grid-column: 1 / -1;
  margin-top: 0;
}

/* The API browser is the one place the bars carry borders, matching
   the tool and playground shells: the shell is an app frame, not a
   glass overlay, so the separation is drawn rather than blurred. */
.api-shell > .subnav {
  position: relative;
  top: auto;
  border-bottom: 1px solid var(--border-subtle);
}

.docs-shell > .site-nav {
  position: sticky;
  top: 0;
}

/* Mono, uppercase and small: the same voice the eyebrows use, which is what
   reads as subordinate to the main navigation without a second bar of
   chrome. */
.subnav-link {
  position: relative;
  /* line-height 1 sets the type; the vertical padding is what keeps the tap
     target usable. At 13px type a padding-free link would be a 13px tall
     target on a bar that scrolls horizontally under a thumb. */
  padding: var(--space-2) var(--space-5);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--duration-fast);

  &:hover {
    color: var(--text-0);
  }

  /* Colour alone marks the current page. Adding weight on top states it
     twice and makes that one label sit heavier than its neighbours, which
     the accent already handles. */
  &[aria-current] {
    color: var(--accent-text);
  }
}

/* Sits on the boundary between two links, so it belongs to neither. */
.subnav-link + .subnav-link::before {
  content: "/";
  position: absolute;
  top: 50%;
  left: 0;
  translate: -50% -50%;
  opacity: 0.4;
  pointer-events: none;
}
}
@layer components {
/* ============================================================
   SEARCH
   Three states for the nav field: an icon at rest, a preview on
   hover, the full-width field once open (.site-nav.is-search).
   Results live in a fixed overlay under the bar, on the shared
   glass material. Behavior: assets/controllers/search_controller.js.
   ============================================================ */

/* The form and the overlay participate directly in the nav's flex
   layout; the wrapper only exists to scope the Stimulus controller. */
.search-root {
  display: contents;
}

/* With the field as a fourth flex child, space-between would drag the
   menu off-center. Auto margins on the links restore true centering and
   glue the field to the icon cluster. */
.site-nav:has(.search-root) {
  justify-content: flex-start;
  gap: var(--space-4);

  & .nav-links { margin-inline: auto; }
}

.search-field {
  position: relative;
  z-index: 1; /* own stacking context: the hover pill overlays neighbors */
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px; /* matches .nav-icon so the rest state joins the icon group */
  height: 32px;
  border-radius: var(--radius-full);
  color: var(--text-3);
  cursor: text;
  transition: color var(--duration-fast);

  /* The hover pill is an overlay anchored to the icon, growing leftwards
     over empty bar space: the field's in-flow box never changes, so the
     centered menu does not move. Only focus (is-search) expands for real. */
  &::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    z-index: -1;
    width: 100%;
    border-radius: var(--radius-full);
    background: transparent;
    transition: width 0.28s var(--ease-out-expo),
                background var(--duration-fast);
  }

  & > svg {
    flex: none;
    width: 15px;
    height: 15px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.8;
    stroke-linecap: round;
  }

  & input {
    position: absolute;
    inset-block: 0;
    inset-inline-end: 100%; /* hugs the left edge of the icon */
    width: 0;
    min-width: 0;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-0);
    font: inherit;
    font-size: var(--text-sm);
    opacity: 0;
    transition: opacity var(--duration-fast);

    &::placeholder { color: var(--text-3); }
    &:focus { outline: none; }
    &::-webkit-search-cancel-button { display: none; }
  }

  /* hover: the pill fades and slides out, the icon stays put */
  &:hover {
    color: var(--text-1);

    &::before {
      width: 11rem;
      background: var(--surface-2);
    }

    & input {
      width: calc(11rem - 32px - var(--space-2));
      padding-inline-start: var(--space-3);
      opacity: 1;
    }
  }
}

.search-field-esc {
  display: none;
  flex: none;
  margin-inline-start: auto;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  color: var(--text-3);
  cursor: pointer;

  &:hover { color: var(--text-0); }
}

/* Open: the menu steps aside, the field spans from the logo to the
   surviving GitHub icon. */
.site-nav.is-search {
  & .nav-links { display: none; }

  /* The open field floats centered over the bar, capped to the width
     of the results pane below so the two read as one column. A notch
     above surface-2 keeps it visible against the glass. */
  & .search-field {
    position: absolute;
    inset: 0;
    margin: auto;
    width: min(44rem, calc(100% - 22rem));
    height: fit-content;
    justify-content: flex-start;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    background: color-mix(in oklch, var(--surface-2) 55%, var(--surface-3));
    color: var(--text-2);

    &::before { display: none; }
    & input { position: static; flex: 1; width: auto; padding: 0; opacity: 1; }
  }

  & .search-field-esc { display: block; }

  /* only the field and GitHub survive in the open bar; the field is out
     of flow, so the icon needs its own push to the right edge */
  & .nav-side { margin-inline-start: auto; }
  & .nav-side .nav-icon:not(:last-child) { display: none; }
}

/* --- the overlay under the bar --- */
.search-overlay {
  position: fixed;
  inset: var(--nav-h) 0 0;
  overflow-y: auto;
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);

  &[hidden] { display: none; }
}

.search-pane {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 44rem;
  margin-inline: auto;
  padding: var(--space-6) var(--space-4) var(--space-10);
}

.search-count,
.search-hint {
  padding-inline: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-3);
}

.search-count { text-transform: uppercase; }

.search-results {
  display: grid;
  gap: var(--space-4);
}

.search-group {
  display: grid;
  gap: var(--space-1);
}

.search-group-title {
  margin: 0;
  padding: var(--space-2) var(--space-3) 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 400;
  text-transform: uppercase;
  color: var(--accent-text);
}

.search-group-items {
  display: grid;
  gap: 2px;
}

.search-hit {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "title arrow"
    "path arrow";
  gap: var(--space-1) var(--space-4);
  align-items: center;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  text-decoration: none;
  transition: background var(--duration-fast);

  &:hover,
  &.is-active {
    background: var(--surface-2);

    .search-hit-title { color: var(--accent-text); }
    .search-hit-arrow { color: var(--accent-text); translate: 2px 0; }
  }

  & mark {
    background: none;
    color: var(--accent-text);
  }
}

.search-hit-title {
  grid-area: title;
  font-size: var(--text-base);
  line-height: 1.25;
  color: var(--text-0);
  text-wrap: pretty;
  transition: color var(--duration-fast);
}

.search-hit-path {
  grid-area: path;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-3);
}

.search-hit-arrow {
  grid-area: arrow;
  color: var(--text-3);
  transition: color var(--duration-fast), translate var(--duration-fast);
}

.search-empty {
  padding: var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-3);
}

/* --- mobile: the bar becomes the search --- */
@media (width <= 768px) {
  /* the icon anchors the right cluster, ahead of the burger */
  .search-field { margin-inline-start: auto; }
  .site-nav:has(.search-root) .nav-burger {
    order: 1;
    margin-inline-start: 0;
  }

  /* rest: a plain icon next to the burger, no hover preview */
  .search-field,
  .search-field:hover {
    width: 40px;
    height: 40px;
    justify-content: center;
    padding: 0;
    background: none;

    &::before { display: none; }
    & input { width: 0; padding: 0; opacity: 0; }
  }

  .site-nav.is-search {
    & :is(.nav-logo, .nav-burger, .nav-side) { display: none; }

    & .search-field {
      position: static;
      flex: 1;
      width: auto;
      height: auto;
      justify-content: flex-start;
      gap: var(--space-2);
      margin: 0;
      padding: var(--space-2) var(--space-4);
      background: color-mix(in oklch, var(--surface-2) 55%, var(--surface-3));

      & input { position: static; flex: 1; width: auto; opacity: 1; }
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  .search-field,
  .search-field::before,
  .search-field input {
    transition: none;
  }
}
}
@layer components {
/* ============================================================
   SITE FOOTER
   Multi-column footer with brand + link columns + bottom row.
   Add .minimal to render only the bottom row (landing pages).
   The .footer-glow sibling element is an optional decoration
   placed BEFORE the footer, not inside it.
   ============================================================ */

.site-footer {
  padding: var(--space-16) var(--gutter) var(--space-8);
  border-top: 1px solid var(--border-subtle);

  /* Variants */
  &.minimal {
    .footer-top { display: none; }
    .footer-bottom {
      border-top: none;
      padding-top: 0;
    }
  }

  /* Flush: drop the outer block padding AND the inner max-width so the
     footer spans the viewport edge-to-edge. Used on app-style pages
     (API browser, playground) where the surrounding chrome already
     provides vertical spacing and the page is full-bleed. */
  &.flush {
    padding-block: 0;
    padding-inline: var(--space-6); /* aligns with .site-nav's gutters */
  }
  &.flush .footer-bottom {
    padding-block: var(--space-3);
    max-width: none;
    margin-inline: 0;
  }
}

/* Columns flow via the .grid-auto primitive (auto-fit minmax), knobs
   set in the template: --grid-min 10rem, --grid-gap space-8. Column
   count degrades continuously with the viewport, no breakpoints. */
.footer-top {
  max-width: 76rem;
  margin: 0 auto var(--space-16);
}

/* Below the columns, above the legal row. */
.footer-brand {
  max-width: 76rem;
  margin: 0 auto var(--space-12);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  text-align: center;
}

.footer-logo {
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--text-0);
}

.footer-tagline {
  font-size: var(--text-sm);
  font-weight: 300;
  color: var(--text-1);
  max-width: 28rem;
  line-height: 1.6;
}

.footer-col {
  h4 {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--text-1);
    margin-bottom: var(--space-4);
  }

  ul {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
  }

  a {
    font-size: var(--text-sm);
    font-weight: 300;
    color: var(--text-1);
    transition: color var(--duration-fast);

    &:hover { color: var(--text-0); }
  }
}

.footer-bottom {
  max-width: 76rem;
  margin: 0 auto;
  padding-top: var(--space-8);
  border-top: 1px solid var(--border-subtle);
}

.footer-copy {
  font-size: var(--text-xs);
  font-weight: 200;
  color: var(--text-2);

  a {
    color: var(--text-1);
    transition: color var(--duration-fast);

    &:hover { color: var(--text-0); }
  }
}

.footer-legal {
  a {
    font-size: var(--text-xs);
    color: var(--text-2);
    transition: color var(--duration-fast);

    &:hover { color: var(--text-0); }
  }
}

/* Optional decorative glow line. Place as a sibling
   immediately before .site-footer. */
.footer-glow {
  height: 1px;
  background: linear-gradient(90deg,
    transparent 0%,
    oklch(0.55 0.24 var(--hue-brand)) 15%,
    oklch(0.60 0.22 calc(var(--hue-brand) +  40)) 30%,
    oklch(0.65 0.25 calc(var(--hue-brand) +  80)) 50%,
    oklch(0.60 0.22 calc(var(--hue-brand) + 120)) 70%,
    oklch(0.55 0.24 calc(var(--hue-brand) + 160)) 85%,
    transparent 100%
  );
  opacity: 0.6;
}
}
@layer components {
/* ============================================================
   BUTTON
   Primary actionable element. Three visual variants and three
   sizes. Variants and sizes are modifier classes on .btn.
   The accent scope of the parent colors the primary variant
   automatically via --accent.
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1;
  padding: var(--space-3) var(--space-6);
  border-radius: var(--radius-lg);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-0);
  cursor: pointer;
  transition:
    background var(--duration-fast),
    border-color var(--duration-fast),
    color var(--duration-fast),
    box-shadow var(--duration-fast),
    transform var(--duration-fast);

  /* Base states */
  &:active   { transform: scale(0.98); }
  &:disabled { opacity: 0.5; cursor: not-allowed; }
  &:focus-visible {
    outline: 2px solid var(--accent-text);
    outline-offset: 2px;
  }

  /* Variants */
  &.primary {
    background: var(--accent);
    color: var(--surface-0);

    &:hover {
      background: oklch(from var(--accent) calc(l + 0.05) c calc(h + 8));
    }
  }

  &.secondary {
    border-color: var(--accent-border);
    color: var(--accent-text);

    &:hover {
      background: var(--accent-soft);
      border-color: transparent;
    }
  }

  &.ghost {
    color: var(--text-1);

    &:hover {
      background: var(--surface-1);
      color: var(--text-0);
    }
  }

  /* Sizes */
  &.sm {
    font-size: var(--text-xs);
    padding: var(--space-2) var(--space-4);
    border-radius: var(--radius-md);
  }
  &.lg {
    font-size: var(--text-base);
    padding: var(--space-4) var(--space-8);
    border-radius: var(--radius-xl);
  }

  /* Shape and width modifiers */
  &.pill  { border-radius: var(--radius-full); }
  &.block { width: 100%; }

  /* Glow: hover lift + soft white glow. Use on hero CTAs. */
  &.glow:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 30px oklch(from var(--text-0) l c h / 0.12);
  }
}
}
@layer components {
/* Hero CTA with the site's complete accent palette as a stepped ring. */
.btn.rainbow-button {
  --rainbow-button-icon: var(--accent-text);

  position: relative;
  isolation: isolate;
  background: transparent;
  border: 2px solid var(--border-default);
  color: var(--text-0);
  box-shadow: none;

  &:hover {
    background: transparent;
  }

  &::before {
    content: "";
    position: absolute;
    inset: calc(var(--space-1) * -0.75);
    z-index: -2;
    border-radius: var(--radius-full);
    background: conic-gradient(
      oklch(0.70 0.20 45) 0deg 33deg,
      oklch(0.70 0.20 85) 33deg 65deg,
      oklch(0.70 0.20 125) 65deg 98deg,
      oklch(0.70 0.20 145) 98deg 131deg,
      oklch(0.70 0.20 195) 131deg 164deg,
      oklch(0.70 0.20 220) 164deg 196deg,
      oklch(0.70 0.24 239) 196deg 229deg,
      oklch(0.70 0.20 270) 229deg 262deg,
      oklch(0.70 0.20 300) 262deg 295deg,
      oklch(0.70 0.20 330) 295deg 327deg,
      oklch(0.70 0.20 350) 327deg 360deg
    );
  }

  &::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: var(--radius-full);
    background: var(--surface-0);
  }
}

.rainbow-button__arrow {
  color: var(--rainbow-button-icon);
}

.rainbow-button__icon {
  width: var(--space-5);
  height: var(--space-5);
  fill: none;
  stroke: var(--rainbow-button-icon);
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.rainbow-button__icon-dot {
  fill: var(--rainbow-button-icon);
  stroke: none;
}

.home-hero .rainbow-button {
  --rainbow-button-icon: var(--hero-accent);
}
}
@layer components {
/* Semantic navigation card rendered by <twig:Card>. The scope keeps
   its internal composition independent from page-level card families. */
@scope (.content-card) {
  :scope {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    overflow: clip;
    background: var(--surface-1);
    border-radius: var(--radius-lg);
  }

  :scope.is-bordered {
    border: 1px solid var(--border-subtle);
  }

  :scope > .media {
    > :only-child { margin: 0; }
  }

  :scope > .content {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-6);
  }

  .title {
    margin: 0;
    font-size: var(--text-2xl);

    &.is-compact { font-size: var(--text-xl); }

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

  .description {
    margin: 0;
  }

  .action {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    gap: var(--space-2);
    margin-block-start: auto;
    padding-block-start: var(--space-3);
    color: var(--text-1);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);

    > .arrow {
      color: var(--accent-text);
      transition: translate var(--duration-fast) var(--ease-out-expo);
    }
  }

  :scope:is(:hover, :focus-within) .action > .arrow {
    translate: var(--space-1) 0;
  }

  :scope > .arrow {
    position: absolute;
    inset-inline-end: var(--space-6);
    inset-block-end: var(--space-5);
    color: var(--accent-text);
    font-size: var(--text-lg);
    opacity: 0;
    pointer-events: none;
    translate: calc(var(--space-2) * -1) 0;
    transition:
      opacity var(--duration-fast),
      translate var(--duration-fast) var(--ease-out-expo);
  }

  :scope:is(:hover, :focus-within) > .arrow {
    opacity: 1;
    translate: 0 0;
  }

  :scope:has(.card-link:focus-visible) {
    outline: 2px solid var(--accent-text);
    outline-offset: var(--space-1);
  }

  .card-link:focus-visible { outline: none; }
}
}
@layer components {
/* ============================================================
   CODE BLOCK
   Displayed code with optional header (language tag, copy button).
   Highlights values (.t-val), dims property names (.t-kw), and
   uses calm blue for functions (.t-fn). Comments italicized.

   <div class="code-block">
     <div class="code-header">
       <span class="code-lang">php</span>
       <button class="code-copy" data-controller="clipboard">Copy</button>
     </div>
     <pre><code>...</code></pre>
   </div>
   ============================================================ */

.code-block {
  /* Deeper than the page, held by a subtle accent border: the block
     reads as a recessed panel in the section's color. */
  /* No overflow: hidden here -- it would clip the scroll-driven
     spectrum ring below at the padding box. The pre rounds itself. */
  position: relative;
  background: oklch(from var(--surface-0) calc(l - 0.015) calc(c * 1.5) h);
  border: 1px solid color-mix(in oklch, var(--accent) 30%, transparent);
  border-radius: var(--radius-lg);
  margin-block: var(--space-6);

  pre {
    margin: 0;
    padding: var(--space-6);
    overflow-x: auto;
    line-height: 1.7;
    font-size: var(--text-base);
    border-radius: inherit;

    code {
      font-family: var(--font-mono);
      font-weight: 400;
      color: var(--text-1);
      background: none;
      padding: 0;
    }
  }
}

/* The header floats in the top-right corner instead of reserving a
   full row: the language tag and copy button are metadata, not
   content, so they share the block's own padding. */
.code-header {
  position: absolute;
  inset-block-start: var(--space-2);
  inset-inline-end: var(--space-2);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.code-copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1);
  border-radius: var(--radius-sm);
  color: var(--text-3);
  transition: color var(--duration-fast), background var(--duration-fast);

  &:hover {
    color: var(--text-0);
    background: var(--surface-2);
  }
}

/* The clipboard controller stamps the state on the block root. */
.code-block[data-clipboard-state="copied"] .code-copy {
  color: var(--accent-text);
}

/* --- Console blocks -----------------------------------------------
   Shell commands read as a prompt, not as highlighted source: flat
   text, a non-selectable "$ " prompt, and the copy button centered
   vertically on the right. The clipboard source never includes the
   prompt -- it copies the raw fence. */
.code-block:is([data-lang="bash"], [data-lang="shell"], [data-lang="sh"]) {
  pre code,
  pre code * {
    color: var(--text-0);
    font-weight: var(--weight-regular);
    font-style: normal;
  }

  .code-header {
    inset-block-start: 50%;
    inset-inline-end: var(--space-3);
    translate: 0 -50%;
  }
}

/* One span per command line; the prompt is a pseudo-element, so it
   is never selected nor copied. Blank lines get no prompt. */
.console-line {
  display: block;

  &:not(:empty)::before {
    content: "$ ";
    color: var(--text-3);
    user-select: none;
  }
}

/* --- Scroll-driven spectrum ring ----------------------------------
   As a block climbs past 15vh from the bottom of the viewport, its
   accent border dissolves into the PHPColor spectrum (the rainbow
   button's conic), fully in place as it nears the center. The conic
   slowly rotates with the scroll, and the ring masks itself away
   right before the block slides under the fixed nav. Browsers
   without view() support, and readers who prefer reduced motion,
   keep the plain accent border. */
@property --ring-angle {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .code-block::after {
      content: "";
      position: absolute;
      inset: -1px;
      border: 1px solid transparent;
      border-radius: var(--radius-lg);
      background: conic-gradient(
        from var(--ring-angle),
        oklch(0.70 0.20 45) 0deg 33deg,
        oklch(0.70 0.20 85) 33deg 65deg,
        oklch(0.70 0.20 125) 65deg 98deg,
        oklch(0.70 0.20 145) 98deg 131deg,
        oklch(0.70 0.20 195) 131deg 164deg,
        oklch(0.70 0.20 220) 164deg 196deg,
        oklch(0.70 0.24 239) 196deg 229deg,
        oklch(0.70 0.20 270) 229deg 262deg,
        oklch(0.70 0.20 300) 262deg 295deg,
        oklch(0.70 0.20 330) 295deg 327deg,
        oklch(0.70 0.20 350) 327deg 360deg
      ) border-box;
      mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0);
      mask-composite: exclude;
      pointer-events: none;
      opacity: 0;
      /* Order matters: the exit fade must win over the entry fade, so
         it comes later; `forwards` only, or its backwards fill would
         pin the ring visible before its range. */
      animation:
        code-rainbow linear both,
        code-ring-out linear forwards,
        code-ring-spin linear both;
      animation-timeline: view(), view(), view();
      /* Chrome does not resolve var()/calc() inside animation-range:
         the exit offsets are literal, derived from --nav-h (56px).
         -116px = nav + 60 of approach, -40px = 16px under the nav. */
      animation-range:
        cover 15vh cover 45vh,
        exit -116px exit -40px,
        cover;
    }
  }
}

/* The spectrum stays a whisper: a thin veil over the accent border,
   which remains in place underneath at all times. */
@keyframes code-rainbow {
  to { opacity: 0.5; }
}

@keyframes code-ring-out {
  from { opacity: 0.5; }
  to { opacity: 0; }
}

/* 540deg over the full cover: the ring is only visible for ~60% of
   the traversal (15vh to the nav), which leaves roughly a full turn
   on screen. */
@keyframes code-ring-spin {
  to { --ring-angle: 540deg; }
}

/* Legacy hand-authored token classes (still used by a handful of
   inline snippets). Keep until they're all migrated to alto. */
.t-kw  { color: var(--text-3); }
.t-val { color: var(--text-0); font-weight: 500; }
.t-fn  { color: oklch(0.82 0.20 200); font-weight: 400; }
.t-cm  { color: oklch(0.38 0.02 var(--hue-brand)); font-style: italic; font-size: 0.92em; }
.t-str { color: oklch(0.78 0.16 145); }
.t-num { color: oklch(0.78 0.18 50); }


/* alto/code-highlight token classes. The library's own theme writes
   inline styles -- we override here so highlighting picks up the
   project's design tokens (hue-brand surfaces, OKLCH accent set). */
.code-block .alto-highlight {
  background: transparent;
  margin: 0;
}
.alto-keyword         { color: oklch(0.78 0.18 320); font-weight: 500; }
.alto-keyword-control { color: oklch(0.78 0.18 320); font-weight: 500; }
.alto-keyword-modifier{ color: oklch(0.78 0.18 320); font-weight: 500; }

.alto-variable        { color: oklch(0.85 0.06 var(--hue-brand)); }
.alto-variable-special{ color: oklch(0.85 0.06 var(--hue-brand)); font-style: italic; }
.alto-property        { color: oklch(0.82 0.10 220); }
.alto-parameter       { color: oklch(0.85 0.06 var(--hue-brand)); }

.alto-function        { color: oklch(0.78 0.16 220); }
.alto-method          { color: oklch(0.78 0.16 220); }
.alto-function-builtin{ color: oklch(0.80 0.16 200); }

.alto-class           { color: oklch(0.84 0.10 195); }
.alto-class-builtin   { color: oklch(0.84 0.10 195); }
.alto-namespace       { color: oklch(0.78 0.08 195); }
.alto-type            { color: oklch(0.82 0.09 210); }
.alto-type-builtin    { color: oklch(0.82 0.09 210); }
.alto-constant        { color: oklch(0.82 0.12 350); }
.alto-constant-builtin{ color: oklch(0.82 0.12 350); }
.alto-attribute       { color: oklch(0.80 0.10 300); }

.alto-string          { color: oklch(0.80 0.16 145); }
.alto-string-special  { color: oklch(0.80 0.16 145); }
.alto-number          { color: oklch(0.86 0.11 90); }
.alto-boolean         { color: oklch(0.86 0.11 90); }
.alto-null            { color: oklch(0.78 0.18 320); font-style: italic; }

.alto-operator        { color: var(--text-2); }
.alto-punctuation     { color: var(--text-2); }
.alto-bracket         { color: var(--text-2); }
.alto-tag             { color: oklch(0.78 0.18 320); }
.alto-tag-php         { color: oklch(0.65 0.10 var(--hue-brand)); }

.alto-comment         { color: oklch(0.55 0.02 var(--hue-brand)); font-style: italic; }
.alto-comment-doc     { color: oklch(0.60 0.04 var(--hue-brand)); font-style: italic; }
.alto-comment-doc-tag { color: oklch(0.70 0.08 220); font-style: italic; font-weight: 500; }

.alto-error           { color: oklch(0.70 0.22 28); text-decoration: underline wavy; }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Inline code variant: when wrapped in prose. */
.code-inline {
  font-family: var(--font-mono);
  font-size: 0.88em;
  color: var(--accent-text);
  background: var(--accent-soft);
  padding: 2px 7px;
  border-radius: var(--radius-sm);
}
}
@layer components {
/* ============================================================
   SWATCH
   Color display tile with metadata. The PHPColor signature
   component since the entire site is about colors.

   Each swatch gets its own --ph (palette hue) custom property,
   set inline or via accent scope. The fill is computed from it.

   <div class="swatch" style="--ph: 180;">
     <div class="swatch-fill"></div>
     <div class="swatch-info">
       <p class="swatch-name">Teal 500</p>
       <p class="swatch-value">oklch(0.65 0.18 180)</p>
     </div>
   </div>
   ============================================================ */

@property --ph {
  syntax: "<number>";
  inherits: true;
  initial-value: 239;
}

.swatch {
  background: var(--surface-1);
  border-radius: var(--radius-md);
  overflow: hidden;
  transition: transform var(--duration-normal) var(--ease-out-expo);
}
.swatch:hover { transform: translateY(-2px); }

.swatch-fill {
  height: 100px;
  background: oklch(0.65 0.20 var(--ph));
  display: flex;
  align-items: flex-end;
  padding: var(--space-3);
}

.swatch-info {
  padding: var(--space-3) var(--space-4);
}

.swatch-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-0);
  margin-bottom: var(--space-1);
}

.swatch-value {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 300;
  color: var(--text-2);
}

/* Grid container for collections of swatches. */
.swatch-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: var(--space-4);
}

/* Spectrum mark variant: tiny inline color dots. */
.swatch-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: oklch(0.65 0.20 var(--ph));
  margin-right: var(--space-2);
  vertical-align: middle;
}

/* Border-left accent variant: text with a colored side rule. */
.swatch-bar {
  border-left: 3px solid oklch(0.60 0.18 var(--ph));
  padding-left: var(--space-4);
}
}
@layer components {
/* ============================================================
   BADGE
   Small inline indicator. Used for tags, labels, status,
   counts. Mono font, pill shape, accent-scoped color.

   <span class="badge">v2.1</span>
   <span class="badge solid">stable</span>
   <span class="badge dot">live</span>
   ============================================================ */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.05em;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  border: 1px solid var(--border-default);
  color: var(--text-1);
  background: transparent;
  line-height: 1;

  /* Variants */
  /* Solid: filled with accent color. */
  &.solid {
    background: var(--accent-soft);
    border-color: transparent;
    color: var(--accent-text);
  }

  /* Strong: full accent fill, used for primary status. */
  &.strong {
    background: var(--accent);
    border-color: transparent;
    color: var(--surface-0);
  }

  /* Dot: pulsing indicator dot before the label. */
  &.dot::before {
    content: "";
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 8px var(--accent);
    animation: badge-pulse 2s ease-in-out infinite;
  }

  /* Sizes */
  &.sm {
    font-size: 0.65rem;
    padding: 2px var(--space-2);
  }
}

@keyframes badge-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}
}
@layer components {
/* Tag: uppercase mono accent-colored label.
   Inherits accent scope via var(--accent-text), like .eyebrow. */
.tag {
  font-family: var(--font-mono);
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent-text);
  margin-bottom: var(--space-3);

  &.sm {
    font-size: 0.6rem;
    margin-bottom: var(--space-2);
  }
}
}
@layer components {
/* ============================================================
   SPONSOR BAR
   Horizontal CTA strip with color signature, message, and
   sponsor button. Sits between content and footer.
   ============================================================ */

.sponsor-bar {
  padding: var(--space-6) var(--gutter);
  position: relative;
  overflow: hidden;

  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 20%;
    width: 60%;
    height: 1px;
    background: linear-gradient(90deg, transparent,
      oklch(0.60 0.24 var(--hue-brand)),
      oklch(0.65 0.22 calc(var(--hue-brand) + 80)),
      transparent);
  }
}

.sponsor-bar-inner {
  max-width: 64rem;
  margin-inline: auto;
}

/* Color signature -- vertical bars */
.sponsor-sig {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex-shrink: 0;

  span {
    width: 3px;
    border-radius: 2px;

    &:nth-child(1) { height: 10px; background: oklch(0.65 0.24 var(--hue-brand)); }
    &:nth-child(2) { height: 16px; background: oklch(0.68 0.22 calc(var(--hue-brand) + 50)); }
    &:nth-child(3) { height: 20px; background: oklch(0.72 0.25 calc(var(--hue-brand) + 100)); }
    &:nth-child(4) { height: 16px; background: oklch(0.68 0.22 calc(var(--hue-brand) + 150)); }
    &:nth-child(5) { height: 10px; background: oklch(0.65 0.24 calc(var(--hue-brand) + 200)); }
  }
}

.sponsor-bar-text {
  h3 {
    font-size: var(--text-sm);
    font-weight: 400;
    color: var(--text-0);

    strong {
      font-weight: 400;
      background: linear-gradient(135deg,
        oklch(0.70 0.24 var(--hue-brand)),
        oklch(0.72 0.22 calc(var(--hue-brand) + 60)));
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
      background-clip: text;
    }
  }

  p {
    font-size: var(--text-xs);
    color: var(--text-2);
    font-weight: 200;
  }
}

/* CTA */
.sponsor-bar-cta {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-6);
  border: 1px solid var(--border-subtle);
  border-radius: 9999px;
  color: var(--text-1);
  font-size: var(--text-xs);
  font-weight: 300;
  flex-shrink: 0;
  transition: all var(--duration-normal) var(--ease-out-expo);
  position: relative;
  overflow: hidden;

  &::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(135deg,
      oklch(0.50 0.20 var(--hue-brand) / 0.12),
      oklch(0.50 0.20 calc(var(--hue-brand) + 60) / 0.12));
    opacity: 0;
    transition: opacity var(--duration-normal);
  }

  &:hover {
    border-color: var(--brand);
    color: var(--text-0);
    box-shadow: 0 0 24px oklch(0.40 0.15 var(--hue-brand) / 0.25);
  }
  &:hover::before { opacity: 1; }

  svg {
    width: 14px;
    height: 14px;
    fill: oklch(0.70 0.24 350);
    position: relative;
    z-index: 1;
    transition: transform var(--duration-fast);
  }

  span {
    position: relative;
    z-index: 1;
  }

  &:hover svg {
    animation: sponsor-heartbeat 0.5s ease-in-out;
  }
}

@keyframes sponsor-heartbeat {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.2); }
}

@media (width <= 600px) {
  .sponsor-bar-inner { flex-direction: column; text-align: center; }
  .sponsor-bar-left { flex-direction: column; }
  .sponsor-sig { flex-direction: row; }
  .sponsor-sig span { width: auto; height: 3px; }
  .sponsor-sig span:nth-child(1) { width: 16px; height: 3px; }
  .sponsor-sig span:nth-child(2) { width: 24px; height: 3px; }
  .sponsor-sig span:nth-child(3) { width: 32px; height: 3px; }
  .sponsor-sig span:nth-child(4) { width: 24px; height: 3px; }
  .sponsor-sig span:nth-child(5) { width: 16px; height: 3px; }
}
}
@layer components {
.install-bar {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-base);
  font-weight: 300;
  color: var(--text-1);
  padding: var(--space-4) var(--space-8);
  background: color-mix(in oklch, var(--text-0) 5%, transparent);
  border-radius: var(--radius-lg);
  transition: background var(--duration-normal);

  &:hover { background: color-mix(in oklch, var(--text-0) 10%, transparent); }

  .install-bar-prompt { color: oklch(0.80 0.26 145); font-weight: 600; }
}
}
@layer components {
/* ============================================================
   CARD LINK
   Stretched-link pattern. Lets a single anchor inside a card
   capture clicks across the whole card area without wrapping
   the entire card in <a>.
   Usage:
     <article class="blog-card">          <- needs position:relative
       <h2><a class="card-link" href="...">Title</a></h2>
       <p>Body</p>
       <a class="card-link-above" ...>secondary link</a>
     </article>
   ============================================================ */

.card-link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  /* Transparent overlay -- captures clicks, no visual effect. */
}

/* Sibling links / buttons inside a card need to sit above the
   cover so they remain clickable. Use the explicit modifier or
   rely on the descendant rule below. */
.card-link-above {
  position: relative;
  z-index: 2;
}

/* Any anchor / button that follows the card-link inside the same
   card host gets lifted above the cover automatically -- saves
   tagging every secondary CTA. */
.card-link ~ a,
.card-link ~ button,
.card-link ~ * a,
.card-link ~ * button {
  position: relative;
  z-index: 2;
}
}
@layer components {
/* ============================================================
   FEATURE TRIO
   Three centred columns of mark + title + 1-line description.
   The staple "three reasons" landing primitive.

   Section padding is built in -- drop into a page directly,
   no `.sec-features` wrapper required. Sits on the page surface
   (no card chrome).

   Marks are typography-only (mono uppercase on the page surface).
   No colored backgrounds, no colored borders, no chips. The
   design system's "no color on color" rule applies.

   Usage:
     <section class="feature-trio">
       <article class="feature">
         <span class="feature-mark">01</span>
         <h3 class="feature-title">Parse</h3>
         <p class="feature-desc">Hex, rgb, hsl, oklch, lab.</p>
       </article>
       ...
     </section>
   ============================================================ */

/* Inner content -- always lives inside a `.sec-*` wrapper that
   provides the section padding. No internal padding-block here
   so trios compose with the surrounding section's rhythm. */
.feature-trio {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 4vw, 4rem);
  max-width: 80rem;
  margin: 0 auto;
  padding-inline: var(--gutter);
}

.feature {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-4);
  max-width: 24rem;
  margin-inline: auto;
}

/* Small mark above the title -- mono, monochromatic, no fill.
   Holds a numeral (01) or a short uppercase token (PARSE / FMT). */
.feature-mark {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text-2);
}

.feature-icon {
  width: var(--space-12);
  height: var(--space-12);
  color: var(--accent-text);
}

.feature-title {
  font-size: var(--text-xl);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text-0);
  margin: 0;
  text-wrap: balance;

  /* Optional link inside the title -- inherit colour. */
  a {
    color: inherit;
    text-decoration: none;
    transition: color var(--duration-fast);
  }
  a:hover { color: var(--text-1); }
}

.feature-desc {
  font-size: var(--text-base);
  font-weight: 300;
  line-height: 1.55;
  color: var(--text-1);
  margin: 0;
  /* Balance, not pretty: these run to two lines in a narrow column, where
     pretty only guards the last line and still leaves the first one long
     and the second one short. */
  text-wrap: balance;
}

/* Stack on narrow viewports. */
@media (width <= 768px) {
  .feature-trio { grid-template-columns: 1fr; }
}
}
@layer components {
/* ============================================================
   METRIC STRIP
   A horizontal band of big numerals + tiny labels. Anchors a
   page in concrete numbers without dedicating a whole section.

   Usage:
     <section class="metric-strip">
       <div class="metric">
         <span class="metric-value">147</span>
         <span class="metric-label">Named colors</span>
       </div>
       <div class="metric">
         <span class="metric-value">0</span>
         <span class="metric-label">Dependencies</span>
       </div>
       ...
     </section>

   The band is full-bleed within the section it sits in; vertical
   rules between metrics are drawn via border-inline-start so they
   collapse cleanly when the grid wraps to fewer columns.
   ============================================================ */

.metric-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--space-1);
  max-width: 80rem;
  margin: 0 auto;
  padding: clamp(2rem, 5vh, 5rem) var(--gutter);
}

.metric {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  text-align: center;
  padding-inline: var(--space-4);
  border-inline-start: 1px solid var(--border-subtle);

  /* The first metric in any wrapped row sits flush -- drop the
     leading rule by clearing it on the first column. */
  &:nth-child(1),
  &:nth-child(4n + 1) {
    border-inline-start: none;
  }
}

.metric-value {
  font-family: var(--font-display, var(--font-body));
  font-size: clamp(2.5rem, 5vw, 4rem);
  font-weight: 200;
  letter-spacing: -0.04em;
  line-height: 1;
  color: var(--text-0);
}

.metric-label {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-2);
}

/* Optional accent on the number -- echoes the surrounding section. */
.metric.accent .metric-value {
  color: var(--accent-text, oklch(0.78 0.22 var(--hue-brand)));
}

/* On narrow viewports we drop dividers entirely and rely on the
   wrap to separate metrics. */
@media (width <= 768px) {
  .metric { border-inline-start: none; }
}
}
@layer components {
/* ============================================================
   STEP DIAGRAM
   Ordered numerals + title + description for "do X, then Y, then
   Z" sequences. Distinct from feature-trio because the step-num
   carries the sequence implicitly.

   Inner content -- lives inside a `.sec-*` wrapper (which
   provides the section padding-block). The diagram composes
   under arbitrary column counts: override
   `grid-template-columns` inline if you need 4 steps.

   No decorative connector lines, no colored chips, no border
   accents. Numbers + typography are the whole signal.

   Usage:
     <section class="sec-features">
       <p class="eyebrow">...</p>
       <h2 class="title-lg">...</h2>
       <section class="step-diagram">
         <article class="step">
           <span class="step-num">01</span>
           <h3 class="step-title">Install</h3>
           <p class="step-desc">...</p>
         </article>
         ...
       </section>
     </section>
   ============================================================ */

.step-diagram {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(2rem, 4vw, 3rem);
  max-width: 64rem;
  margin: 0 auto;
  padding-inline: var(--gutter);
}

.step {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-3);
}

.step-num {
  font-family: var(--font-mono);
  font-size: var(--text-2xl);
  font-weight: 200;
  letter-spacing: 0.04em;
  color: var(--text-2);
  line-height: 1;
}

.step-title {
  font-size: var(--text-lg);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--text-0);
  margin: 0;
}

.step-desc {
  font-size: var(--text-sm);
  font-weight: 300;
  line-height: 1.55;
  color: var(--text-1);
  margin: 0;
  text-wrap: pretty;
  max-width: 18rem;
}

/* Optional code chip under a step. Mono on the next-up surface
   so it reads as a quoted snippet, not as a colored badge. */
.step-code {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 400;
  padding: var(--space-2) var(--space-3);
  background: var(--surface-2);
  color: var(--text-1);
  border-radius: var(--radius-sm);
  word-break: break-all;
}

/* Stacked layout on narrow screens. */
@media (width <= 768px) {
  .step-diagram { grid-template-columns: 1fr; }
}
}
@layer components {
/* ============================================================
   COMPARE TABLE
   Tight feature × library / option table. First column is the
   feature name; subsequent columns are the things being compared.
   The `.yes` / `.partial` / `.no` cell modifiers carry the
   strength signal through *typography* (weight + text colour),
   not pass/fail colour coding -- this design system does not
   rely on green-good / red-bad semantics.

   Usage:
     <table class="compare-table">
       <thead>
         <tr><th>Feature</th><th>PHPColor</th><th>Library X</th></tr>
       </thead>
       <tbody>
         <tr>
           <th>OKLCH manipulation</th>
           <td class="yes">Native</td>
           <td class="no">No</td>
         </tr>
         ...
       </tbody>
     </table>
   ============================================================ */

.compare-table {
  width: 100%;
  max-width: 64rem;
  margin: 0 auto;
  border-collapse: collapse;
  font-size: var(--text-sm);
  table-layout: fixed;

  th, td {
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border-subtle);
    vertical-align: top;
    text-align: left;
  }

  /* Header row: monospace, uppercase. */
  thead th {
    font-family: var(--font-mono);
    font-size: 0.7rem;
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-2);
    border-bottom: 1px solid var(--border-default);
  }

  /* First column = row label; the rest are values. */
  tbody th {
    font-weight: 500;
    color: var(--text-0);
  }
  tbody td {
    color: var(--text-1);
  }

  tbody tr:hover {
    background: var(--surface-1);
  }

  /* Cell value modifiers -- strength = relevance.
     Bold + text-0 for "yes", text-1 for "partial",
     barely-there text-3 for "no". No green/red. */
  .yes     { color: var(--text-0); font-weight: 500; }
  .partial { color: var(--text-1); }
  .no      { color: var(--text-3); }
}

/* Container scrolls horizontally on narrow screens so the table
   never breaks the layout. */
.compare-table-wrap {
  overflow-x: auto;
  max-width: 100%;
  margin: 0 auto;
  padding-block: var(--space-4);
}
}
@layer components {
/* ============================================================
   PULL QUOTE
   Magazine-style oversized text block, centred. Used to break up
   long landing scrolls and emphasize a single sentence.

   Usage:
     <figure class="pull-quote">
       <blockquote>
         "OKLCH is the future of color on the web."
       </blockquote>
       <figcaption class="pull-quote-attribution">
         &mdash; Bjorn Ottosson, originator of Oklab
       </figcaption>
     </figure>

   The opening quotation mark is a decorative `::before` so it
   stays large and accent-coloured without affecting reading flow.
   ============================================================ */

.pull-quote {
  position: relative;
  max-width: 50rem;
  margin: clamp(3rem, 8vh, 8rem) auto;
  padding: var(--space-6) var(--gutter) 0;
  text-align: center;

  &::before {
    content: '\201C';
    position: absolute;
    top: -0.5rem;
    left: 50%;
    transform: translateX(-50%);
    font-family: var(--font-display, var(--font-body));
    font-size: clamp(4rem, 8vw, 6rem);
    line-height: 1;
    color: oklch(from var(--accent, oklch(0.65 0.20 260)) l c h / 0.5);
    pointer-events: none;
  }

  blockquote {
    font-size: clamp(1.4rem, 2.5vw, 2.2rem);
    font-weight: 200;
    line-height: 1.35;
    letter-spacing: -0.02em;
    color: var(--text-0);
    margin: 0;
    text-wrap: balance;

    /* The quote text comes with literal " characters in the
       markup; the decorative one above is a pure visual accent. */
  }
}

.pull-quote-attribution {
  margin-top: var(--space-6);
  font-family: var(--font-mono);
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-2);
}
}
@layer components {
/* Callout: inline aside for note/tip/warn/info hints in docs.
   Type-specific colour comes from the accent class set by the
   component (.accent-cyan, .accent-green, .accent-amber, .accent-blue),
   which exposes --accent-text. No coloured borders or filled
   backgrounds — the type signal lives in the title alone. */
.callout {
    margin: var(--space-6) 0;
    padding: var(--space-5) var(--space-6);
    background: var(--surface-1);
    border-radius: var(--radius-lg);

    & .callout-title {
        margin: 0 0 var(--space-2);
        font-size: var(--text-sm);
        font-weight: 600;
        letter-spacing: 0.02em;
        text-transform: uppercase;
        color: var(--accent-text);
    }

    & .callout-body {
        color: var(--text-1);

        & > :first-child {
            margin-top: 0;
        }

        & > :last-child {
            margin-bottom: 0;
        }
    }
}
}
@layer components {
/* Inline colour chip for prose. The fill is a square that scales
   with surrounding font-size; the label sits next to it in a
   monospaced face so colour values read as data, not running text.
   Pass the colour through `--c`. Override `--chip-size` to change
   the swatch width without editing the surrounding text size. */
.color-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    vertical-align: -0.2em;

    & .color-chip-fill {
        flex: none;
        display: inline-block;
        background: var(--c, var(--surface-2));
        border-radius: var(--chip-radius, var(--radius-sm));
        width: var(--chip-size, 1.25em);
        height: var(--chip-size, 1.25em);
    }

    & .color-chip-label {
        font-family: var(--font-mono);
        font-size: 0.95em;
        color: var(--text-1);
    }
}

.color-chip-sm {
    --chip-size: 0.9em;
}

.color-chip-lg {
    --chip-size: 1.75em;
}
}
@layer components {
/* ============================================================
   COLOR STRIP
   Visual feedback after a code sample: the colors the code just
   produced. Two renderings share the markup. "dots" is the quiet
   default -- circle + value pairs on one centered row. "blocks"
   is the assertive one -- equal tiles, three per row at most.
   Values flow through `--c`.
   ============================================================ */

.color-strip {
  margin-block: var(--space-2) var(--space-8);
}

.color-strip-item {
  margin: 0;

  figcaption {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-2);
  }
}

/* --- dots: a quiet grid, three per row at most, dots flush left --- */
.color-strip.dots {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: var(--space-6);
  row-gap: var(--space-3);

  /* fewer items, fewer columns: no floating cells */
  &:has(> :last-child:nth-child(1)) { grid-template-columns: 1fr; }
  &:has(> :last-child:nth-child(2)) { grid-template-columns: repeat(2, 1fr); }

  .color-strip-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
  }

  .color-strip-fill {
    flex: none;
    width: 1.1rem;
    height: 1.1rem;
    border-radius: var(--radius-full);
    background: var(--c, var(--surface-2));
  }
}

/* --- blocks: equal tiles, max 3 per row --- */
.color-strip.blocks {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-5);

  .color-strip-item {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);

    figcaption { text-align: center; }
  }

  .color-strip-fill {
    height: var(--strip-height, 3.5rem);
    border-radius: var(--radius-md);
    background: var(--c, var(--surface-2));
  }
}

@media (width <= 640px) {
  .color-strip.blocks { grid-template-columns: repeat(2, 1fr); }
  .color-strip.dots { grid-template-columns: 1fr; }
}
}
@layer components {
/* ============================================================
   BREADCRUMB
   One trail component for every depth > 1 page. Subtle, no
   borders, sits above the page title.
   ============================================================ */

.breadcrumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  line-height: 1.4;
  margin-bottom: var(--space-6);
}

.breadcrumb.centered {
  justify-content: center;
  margin-bottom: var(--space-4);
}

.breadcrumb.eyebrow {
  font-size: var(--text-xs);
  line-height: 1;

  :is(.breadcrumb-link, .breadcrumb-sep, .breadcrumb-current) {
    color: inherit;
  }

  .breadcrumb-link:hover {
    color: var(--text-0);
  }
}

.breadcrumb-link {
  color: var(--text-2);
  text-decoration: none;
  transition: color var(--duration-fast);

  &:hover { color: var(--text-0); }
}

.breadcrumb-sep {
  color: var(--text-3);
  user-select: none;
}

.breadcrumb.strong-separator .breadcrumb-sep {
  color: var(--text-0);
}

.breadcrumb-current {
  color: var(--text-1);
}

.breadcrumb-rainbow {
  span:nth-child(1) { color: var(--rainbow-rose); }
  span:nth-child(2) { color: var(--rainbow-orange); }
  span:nth-child(3) { color: var(--rainbow-amber); }
  span:nth-child(4) { color: var(--rainbow-green); }
  span:nth-child(5) { color: var(--rainbow-blue); }
  span:nth-child(6) { color: var(--rainbow-violet); }
}
}
@layer components {
/* ============================================================
   PAGER (prev / next)
   Sequential navigation for guides, reference, recipes, blog.
   Two slots: prev pinned left, next pinned right. No borders.
   ============================================================ */

.pager {
  display: flex;
  gap: var(--space-4);
  margin-top: var(--space-12);
}

.pager-slot {
  flex: 1;
  min-width: 0;
}

.pager-end {
  display: flex;
  justify-content: flex-end;
  text-align: right;
}

.pager-link {
  display: inline-flex;
  flex-direction: column;
  gap: var(--space-1);
  max-width: 100%;
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-md);
  background: var(--surface-1);
  text-decoration: none;
  transition: background var(--duration-fast);

  &:hover { background: var(--surface-2); }
}

.pager-kicker {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.pager-prev .pager-kicker::before { content: "\2190  "; }
.pager-next .pager-kicker::after { content: "  \2192"; }

.pager-title {
  font-size: var(--text-base);
  font-weight: var(--weight-regular);
  color: var(--text-1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pager-link:hover .pager-title { color: var(--text-0); }

/* Tour variant: no cards. Previous is a lone arrow, next carries the
   progress kicker and the destination title, arrow trailing right.
   The arrow slot shrinks to its content; the next link gets the room. */
.pager.large {
  align-items: center;
  gap: var(--space-6);
  margin-top: var(--space-16);

  .pager-slot { flex: none; }
  .pager-slot.pager-end { flex: 1; }

  .pager-kicker { white-space: nowrap; }

  .pager-link {
    background: none;
    padding: var(--space-2);

    &:hover { background: none; }
  }

  .pager-prev {
    font-size: var(--text-2xl);
    color: var(--text-3);

    &:hover { color: var(--text-0); }
  }

  /* Next reads as one line: "Step 3 . Title ->", common baseline. */
  .pager-next {
    flex-direction: row;
    align-items: baseline;
    gap: var(--space-3);

    .pager-kicker {
      font-size: var(--text-sm);

      &::after { content: none; }
    }

    .pager-sep { color: var(--text-3); }

    .pager-title {
      font-size: var(--text-xl);
      white-space: normal;
    }

    .pager-arrow {
      display: inline-block;
      align-self: center;
      font-size: var(--text-xl);
      color: var(--text-2);
      transition: translate var(--duration-fast) var(--ease-out-expo), color var(--duration-fast);
    }

    &:hover .pager-arrow {
      translate: 3px 0;
      color: var(--text-0);
    }
  }
}

@media (width <= 768px) {
  .pager.large {
    flex-direction: column;

    .pager-end {
      text-align: left;
    }
  }
}
}
@layer components {
/* ============================================================
   CTA BANNER
   Contained terminal call-to-action for landing / resource
   pages. Centered, accent glow, no borders. Lighter than the
   full-screen feat-cta hero.
   ============================================================ */

.cta-banner {
  position: relative;
  overflow: hidden;
  padding: clamp(4rem, 10vh, 8rem) var(--gutter);
  text-align: center;
}

.cta-banner-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(
    ellipse at center,
    color-mix(in oklch, var(--accent) 5.33%, transparent),
    transparent 70%
  );
}

.cta-banner-inner {
  position: relative;
  max-width: 44rem;
  margin-inline: auto;
  --stack-gap: var(--space-5);
}

.cta-banner-text {
  color: var(--text-2);
  text-wrap: balance;
}

.page-cta--compact {
  padding-block: var(--space-16);
}

.page-cta--install {
  background: var(--surface-1);
}

.page-cta-actions {
  --cluster-gap: var(--space-8);
  margin-block-start: var(--space-3);
}

.page-cta-secondary {
  color: var(--text-1);
  font-weight: var(--weight-medium);
  text-decoration-color: var(--accent-border);
  text-underline-offset: var(--space-1);
  transition: color var(--duration-fast), text-decoration-color var(--duration-fast);

  span {
    display: inline-block;
    color: var(--accent-text);
    transition: transform var(--duration-fast) var(--ease-out-expo);
  }

  &:hover {
    color: var(--accent-text);
    text-decoration-color: var(--accent-text);

    span { transform: translateX(var(--space-1)); }
  }

  &:focus-visible {
    outline: 2px solid var(--accent-text);
    outline-offset: var(--space-2);
    border-radius: var(--radius-sm);
  }
}

@media (width <= 560px) {
  .page-cta-actions {
    flex-direction: column;
  }
}
}
@layer components {
/* Derived-facts panel (nearest CSS name, accessible text, harmony).
   Rendered by templates/components/ColorFacts.html.twig. */
.color-facts__swatch {
  display: inline-block;
  inline-size: 2.25rem;
  block-size: 2.25rem;
  border-radius: var(--radius-md);
  flex-shrink: 0;
}
}
@layer components {
/* Factual proof strip (ProofChips component): plain type, wide
   tracking, no chips, no borders. Values in text-1, quiet labels. */
.proof-chips {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3) var(--space-8);

  li {
    font-family: var(--font-mono-ui);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-3);

    strong {
      font-weight: var(--weight-regular);
      color: var(--text-1);
    }
  }
}
}
@layer components {
/* ============================================================
   COLOR CURVE
   A band of high-chroma color revealed beneath a shallow curve
   cut into the page. Used to close the home page, and available
   anywhere a section needs a colored sign-off.

   The band is full bleed on purpose: every other block on the
   page is capped at 88rem, so running edge to edge is what makes
   it read as the page opening up rather than as one more section.

   The field sweeps the whole hue circle. A sweep that covers only
   the cool half looks arbitrary here, and on a color library it
   also misrepresents what the engine reaches.
   ============================================================ */

.color-curve {
  /* Nothing to lay out: the shape is entirely in the clip path,
     which the template sets per instance so the partial can be
     included more than once on a page. */
  line-height: 0;
}

.color-curve__field {
  /* Tall enough for the belly to have somewhere to hang. Below roughly 8rem
     the bend has no room and the boundary flattens into a diagonal. */
  height: clamp(8rem, 17vh, 12rem);

  /* The curve opens the band; this closes it. Cut square at the bottom, the
     colour reads as a bar laid over the footer instead of as the page
     opening up, and the hard line competes with the curve above it.
     Independent of the clip path, which only shapes the top edge. */
  mask-image: linear-gradient(to bottom, #000 0%, #000 58%, transparent 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 58%, transparent 100%);

  /* Eight blooms over a base sweep. A single linear gradient reads
     flat at this height; the blooms give the band places where two
     hues cross, which is the part worth looking at.

     Lightness tracks hue rather than staying constant: yellow needs
     ~0.89 and red ~0.66 to hold this chroma without clipping out of
     gamut. A flat lightness would band the warm end. */
  background:
    radial-gradient(50% 130% at 6% 62%, oklch(0.60 0.30 318) 0%, transparent 60%),
    radial-gradient(46% 125% at 20% 32%, oklch(0.64 0.28 278) 0%, transparent 58%),
    radial-gradient(46% 130% at 34% 66%, oklch(0.72 0.22 228) 0%, transparent 58%),
    radial-gradient(46% 125% at 48% 32%, oklch(0.80 0.20 190) 0%, transparent 58%),
    radial-gradient(46% 130% at 61% 66%, oklch(0.85 0.24 150) 0%, transparent 58%),
    radial-gradient(46% 125% at 73% 34%, oklch(0.90 0.20 100) 0%, transparent 58%),
    radial-gradient(46% 130% at 85% 64%, oklch(0.78 0.20 58) 0%, transparent 58%),
    radial-gradient(50% 130% at 97% 36%, oklch(0.66 0.25 26) 0%, transparent 60%),
    linear-gradient(100deg,
      oklch(0.55 0.30 320), oklch(0.62 0.26 268), oklch(0.72 0.21 212),
      oklch(0.82 0.22 158), oklch(0.89 0.20 100), oklch(0.78 0.20 58),
      oklch(0.66 0.25 26));
}
}
@layer components {
/* ============================================================
   COLOR HOLES
   A high-chroma field seen through shapes cut into the page.
   Reusable anywhere a section wants colour without a slab of it.

   The element is only as wide as its shapes, so the field spans
   the first hole to the last one. Sized to the container instead,
   neighbouring holes fall a few degrees of hue apart and the run
   looks washed out.

   `dots` and `dissolve` get their geometry from the component and
   arrive as inline masks. `bar` and `hexagons` are static enough
   to live here.
   ============================================================ */

/* Centres one run on the page with room around it. The band clips rather
   than letting the run shrink: the hole positions are percentages of the
   element while their radii are pixels, so a narrower element would slide
   them into each other. Seeing part of the run on a phone is the intended
   outcome. */
.color-holes-band {
  display: flex;
  justify-content: center;
  padding-block: clamp(var(--space-16), 9vw, calc(var(--space-24) + var(--space-4)));
  overflow: hidden;
}

.color-holes {
  position: relative;
  flex: none;
  width: var(--holes-width, 600px);
  height: var(--holes-height, 100px);
}

.color-holes__field,
.color-holes__rim {
  position: absolute;
  inset: 0;
  mask-repeat: no-repeat;
  -webkit-mask-repeat: no-repeat;
}

/* Same sweep as .color-curve: the whole hue circle, with lightness
   tracking hue so the warm end holds its chroma inside gamut. */
.color-holes__field {
  background:
    radial-gradient(50% 150% at 6% 62%, oklch(0.60 0.30 318) 0%, transparent 60%),
    radial-gradient(46% 145% at 20% 32%, oklch(0.64 0.28 278) 0%, transparent 58%),
    radial-gradient(46% 150% at 34% 66%, oklch(0.72 0.22 228) 0%, transparent 58%),
    radial-gradient(46% 145% at 48% 32%, oklch(0.80 0.20 190) 0%, transparent 58%),
    radial-gradient(46% 150% at 61% 66%, oklch(0.85 0.24 150) 0%, transparent 58%),
    radial-gradient(46% 145% at 73% 34%, oklch(0.90 0.20 100) 0%, transparent 58%),
    radial-gradient(46% 150% at 85% 64%, oklch(0.78 0.20 58) 0%, transparent 58%),
    radial-gradient(50% 150% at 97% 36%, oklch(0.66 0.25 26) 0%, transparent 60%),
    linear-gradient(100deg,
      oklch(0.55 0.30 320), oklch(0.62 0.26 268), oklch(0.72 0.21 212),
      oklch(0.82 0.22 158), oklch(0.89 0.20 100), oklch(0.78 0.20 58),
      oklch(0.66 0.25 26));
}

/* The depth cue. `box-shadow: inset` cannot do this job here: the mask
   clips the shadow along with everything else, so the rim has to be
   painted as a background on a layer wearing the same mask. */
.color-holes__rim {
  background-repeat: no-repeat;
}

/* --- bar --------------------------------------------------- */

.color-holes--bar .color-holes__field,
.color-holes--bar .color-holes__rim {
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 40'%3E%3Crect width='400' height='40' rx='20' fill='%23000'/%3E%3C/svg%3E");
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 40'%3E%3Crect width='400' height='40' rx='20' fill='%23000'/%3E%3C/svg%3E");
  mask-size: 100% 22px;
  -webkit-mask-size: 100% 22px;
  mask-position: 0 50%;
  -webkit-mask-position: 0 50%;
}

.color-holes--bar .color-holes__rim {
  background-image: linear-gradient(oklch(0 0 0 / 0.5), transparent 45%, transparent 55%, oklch(0 0 0 / 0.5));
  background-size: 100% 22px;
  background-position: 0 50%;
}

/* --- hexagons ----------------------------------------------- */

/* The gap is built into the tile: a 34-wide hexagon centred in a
   58-wide viewBox. Widening mask-size would stretch the shape
   instead of spacing the row. */
.color-holes--hexagons .color-holes__field,
.color-holes--hexagons .color-holes__rim {
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 58 32'%3E%3Cpolygon points='20.5,2 37.5,2 46,16 37.5,30 20.5,30 12,16' fill='%23000'/%3E%3C/svg%3E");
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 58 32'%3E%3Cpolygon points='20.5,2 37.5,2 46,16 37.5,30 20.5,30 12,16' fill='%23000'/%3E%3C/svg%3E");
  mask-repeat: repeat-x;
  -webkit-mask-repeat: repeat-x;
  mask-size: 58px 32px;
  -webkit-mask-size: 58px 32px;
  mask-position: 0 50%;
  -webkit-mask-position: 0 50%;
}

.color-holes--hexagons .color-holes__rim {
  background-image: radial-gradient(closest-side, transparent 55%, oklch(0 0 0 / 0.55) 100%);
  background-repeat: repeat-x;
  background-size: 58px 32px;
  background-position: 0 50%;
}
}

@layer layout {
/* ============================================================
   LAYOUT
   Page-level structure and section primitives.

   Three families of classes:
     .page-wrap       - basic bounded content container
     .sec-*           - section patterns (composable building blocks)
     .ctx-*           - body-level context (marketing, docs, tool)

   Variants are plain modifier classes on the same element:
     .sec-split.reverse, .sec-cards.compact, .sec-hero.compact

   The sec- prefix prevents collision with component classes.
   ============================================================ */


/* ─────────────────────────────────────────────────────────────
   PAGE WRAP
   The default content container. Bounded width, gutter padding.
   Accounts for fixed nav height on top.
   ───────────────────────────────────────────────────────────── */
.page-wrap {
  max-width: 72rem;
  margin-inline: auto;
  padding: calc(var(--nav-h) + var(--space-12)) var(--gutter) var(--space-16);
}
.page-wrap.narrow { max-width: 48rem; }
.page-wrap.wide   { max-width: 88rem; }
.page-wrap.full   { max-width: none; padding-inline: 0; }


/* ─────────────────────────────────────────────────────────────
   MICRO LAYOUT
   Composable inline (.cluster), block (.stack), and auto-fit
   (.grid-auto) primitives. Use INSIDE the .sec-* primitives,
   not as a replacement for them.

   Knobs (set via inline style or scoped CSS):
     --cluster-gap   default var(--space-3)
     --stack-gap     default var(--space-3)
     --grid-gap      default var(--space-6)
     --grid-min      default 16rem
   ───────────────────────────────────────────────────────────── */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cluster-gap, var(--space-3));
}
.cluster.between  { justify-content: space-between; }
.cluster.center   { justify-content: center; }
.cluster.end      { justify-content: flex-end; }
.cluster.top      { align-items: flex-start; }
.cluster.baseline { align-items: baseline; }

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap, var(--space-3));
}
.stack.center { align-items: center; }

.grid-auto {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--grid-min, 16rem), 1fr));
  gap: var(--grid-gap, var(--space-6));
}


/* ─────────────────────────────────────────────────────────────
   CONTEXTS
   Body-level scopes that adjust the entire page mood.
   Apply on <body> or a high-level wrapper.
   ───────────────────────────────────────────────────────────── */
body.ctx-marketing {
  --surface-0: oklch(0.06 0.004 var(--hue-brand));
}

body.ctx-docs {
  --surface-0: oklch(0.07 0.005 var(--hue-brand));
  font-size: var(--text-base);
  line-height: 1.75;
}

body.ctx-tool {
  --surface-0: oklch(0.13 0.008 var(--hue-brand));
  --surface-1: oklch(0.16 0.010 var(--hue-brand));
}

/* Minimal context (legal pages, 404, etc.): short content
   pages. Use a flex column so the footer sticks to the bottom
   when the content doesn't fill the viewport, rather than
   floating in the middle of the page. */
body.ctx-minimal {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
body.ctx-minimal .site-footer { margin-top: auto; }


/* ─────────────────────────────────────────────────────────────
   HERO
   First section of a page. Accounts for fixed nav height.
   Hosts a glow as positioned child. Centers its content
   when .centered modifier is added.
   ───────────────────────────────────────────────────────────── */
.sec-hero {
  position: relative;
  padding: calc(var(--nav-h) + var(--space-16)) var(--gutter) var(--space-12);
  overflow: hidden;
}
.sec-hero.compact { padding-block: calc(var(--nav-h) + var(--space-16)) var(--space-12); }

/* Short variant: still a hero, same centered composition, just not a
   full landing statement. Reduced padding; pair it with a smaller title
   at the page level. For hub pages whose value sits in the content
   below the fold. */
.sec-hero.short {
  padding-block: calc(var(--nav-h) + var(--space-12)) var(--space-10);
}
/* Large variant: a page-opening statement with more presence than the
   shared short hub hero, without taking over the full viewport. */
/* No min-height. Paired with flex-start it did nothing but dump its
   leftover at the bottom, and since the leftover is whatever the title did
   not use, the gap changed size from page to page: 300px on /news, 124px on
   /docs. Padding gives the variant its presence and stays the same
   everywhere. */
.sec-hero.large {
  padding-block: calc(var(--nav-h) + var(--space-16)) var(--space-12);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;

  > .eyebrow { margin-block-end: var(--space-6); }
  > :is(h1, h2, h3) {
    max-width: 9ch;
    margin-block: 0;
    margin-inline: auto;
  }
  > .display-xl {
    font-size: clamp(var(--text-6xl), 8vw, var(--text-8xl));

    strong { font-weight: 500; }
  }
  > .body-lg {
    max-width: min(48ch, 80%);
    margin-block-start: var(--space-8);
    text-wrap: balance;
  }
  > .hero-actions { margin-block-start: var(--space-8); }
}
.hero-title-emphasis { font-weight: 200; }
.hero-title-lead { font-size: larger; }
.hero-title-line { display: block; }
.sec-hero.large > .hero-title-wide { max-width: 15ch; }
/* A section subnav preceding the hero has already cleared the fixed nav, so
   the hero keeps only its own breathing room.

   These used to be written `.subnav + .sec-hero`, which never matched: the
   subnav sits next to <main>, not next to the hero inside it. Every page
   carrying a subnav therefore paid a second nav-height of top padding to
   clear a bar the subnav had already cleared. */
.subnav ~ main > .sec-hero,
.subnav ~ main > .sec-hero.short {
  padding-block-start: var(--space-8);
}
.subnav ~ main > .sec-hero.large {
  padding-block: var(--space-10) var(--space-10);
}
.sec-hero.topic,
.sec-hero.guide-section {
  min-height: calc(100dvh - var(--nav-h));
  padding-block: var(--space-20) var(--space-16);
  display: flex;
  flex-direction: column;
  justify-content: center;

  .breadcrumb { margin-block-end: var(--space-8); }

  > h1 {
    max-width: 13ch;
    margin-block: 0 var(--space-6);
    font-size: clamp(var(--text-5xl), 7vw, var(--text-8xl));
    font-weight: var(--weight-light);
    line-height: 0.96;
    letter-spacing: -0.04em;
  }

  > .body-lg {
    max-width: 46ch;
    color: var(--text-1);
  }
}
.sec-hero.guide-section {
  min-height: 0;
  padding-block: var(--space-10) var(--space-12);
  justify-content: flex-start;

  > .eyebrow { margin-block-end: var(--space-6); }

  > h1 em { color: var(--accent-text); }
}
.sec-hero.centered {
  text-align: center;
  > * { margin-inline: auto; }
  /* Lead paragraphs stay balanced and never wider than the actual
     reading measure below the hero (38rem = the gs-article text
     width), nor 80% of the hero on narrow screens. Inline styles
     may narrow, not widen. */
  > p {
    max-width: min(38rem, 80%);
    text-wrap: balance;
  }
  > h1, > h2, > h3 { text-wrap: balance; }

  /* Article-hero headline (pages/docs/_hero.html.twig). */
  > .hero-headline {
    max-width: 14ch;
    margin-block: 0 var(--space-6);

    &.wide { max-width: 18ch; }
  }
}

/* Compact orientation strip shared by topic and subsection heroes. */
.hero-proof {
  width: min(100%, 52rem);
  margin-block-start: var(--space-12);
  padding: var(--space-4) var(--space-6);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-6);
  background: var(--surface-1);
  border-radius: var(--radius-lg);
  text-align: start;

  > div { min-width: 0; }

  strong {
    display: block;
    margin-block-end: var(--space-1);
    color: var(--text-0);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
  }

  span {
    display: block;
    color: var(--text-2);
    font-size: var(--text-xs);
    line-height: 1.45;
  }
}

/* Reusable radial glow. Positioned inside a sec-hero or any
   relatively-positioned container. Color reads from --accent. */
.hero-glow {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: -100px;
  translate: -50% 0;
  width: 700px;
  height: 500px;
  background:
    radial-gradient(circle at 60% 40%, oklch(from var(--accent) l c h / 0.10) 0%, transparent 60%),
    radial-gradient(circle at 30% 60%, oklch(from var(--accent) l calc(c * 0.8) calc(h + 60) / 0.05) 0%, transparent 50%);
  filter: blur(80px);
  pointer-events: none;
  opacity: 0.6;

  /* Soft-fade the entire glow rectangle so the surrounding
     overflow:hidden never clips a visible halo edge. Without this,
     the blur(80px) halo around the colored region produces a hard
     colored line wherever the hero's clip box meets it. */
  mask-image: radial-gradient(ellipse 75% 70% at 50% 45%, black 25%, transparent 85%);
}
.hero-glow.sm {
  width: 500px;
  height: 400px;
  inset-block-start: -60px;
}

/* Optional multi-accent rule for product and section heroes. Color comes
   from the accent system on each segment, so templates never duplicate
   the spectrum markup or its color values. */
.hero-spectrum {
  display: flex;
  width: min(70%, calc(var(--space-24) * 5));
  height: var(--space-1);
  gap: var(--space-1);
  margin-block-start: var(--space-12);

  > span {
    flex: 1;
    background: oklch(from var(--accent) calc(l + 0.14) calc(c * 1.08) h);
    border-radius: var(--radius-full);
  }
}

/* On the About cluster the spectrum is followed immediately by a trio of
   cards, and at the hero's own bottom padding the bar sits too close to
   them to read as the end of the hero. Scoped by the title class rather
   than applied to every spectrum: elsewhere the hero already has room. */
.sec-hero:has(.about-hero-title) .hero-spectrum {
  margin-block-end: var(--space-16);
}

@media (width <= 600px) {
  .sec-hero.large {
    min-height: calc(var(--space-24) * 5);
  }

  .hero-spectrum {
    width: 100%;
  }
}

@media (width <= 768px) {
  .sec-hero.topic,
  .sec-hero.guide-section {
    min-height: 0;
    padding-block: var(--space-16) var(--space-12);

    > h1 { font-size: var(--text-5xl); }
  }

  .sec-hero.guide-section { padding-block-start: var(--space-10); }

  .hero-proof {
    grid-template-columns: 1fr;
    gap: var(--space-4);
  }
}


/* ─────────────────────────────────────────────────────────────
   STATEMENT
   Centered, focused narrative. One claim per section.
   ───────────────────────────────────────────────────────────── */
.sec-statement {
  padding: var(--space-24) var(--gutter);
  text-align: center;
  max-width: 64rem;
  margin-inline: auto;

  > * { margin-inline: auto; }
  > p { text-wrap: balance; }
  > h1, > h2, > h3 { text-wrap: balance; }
}
.sec-statement.compact { padding-block: var(--space-16); }


/* ─────────────────────────────────────────────────────────────
   SPLIT
   Two-column layout. Add .reverse to flip visual order.
   Each child is a column.
   ───────────────────────────────────────────────────────────── */
.sec-split {
  padding: var(--space-24) var(--gutter);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-16);
  align-items: center;
  max-width: 80rem;
  margin-inline: auto;
}
.sec-split > * { min-width: 0; }
.sec-split.reverse > :first-child { order: 2; }
.sec-split.compact { padding-block: var(--space-16); gap: var(--space-12); }


/* ─────────────────────────────────────────────────────────────
   FEATURES
   Grid of feature items. Auto-fit columns by minimum width.
   Default 3 columns at desktop.
   ───────────────────────────────────────────────────────────── */
.sec-features {
  padding: var(--space-24) var(--gutter);
  max-width: 80rem;
  margin-inline: auto;

  > p[style*="text-align: center"] { text-wrap: balance; }
  > h2[style*="text-align: center"] { text-wrap: balance; }
}
.sec-features-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--space-8);
  margin-top: var(--space-12);
}
.sec-features.compact { padding-block: var(--space-16); }


/* ─────────────────────────────────────────────────────────────
   CARDS
   Grid of cards (uses .card component). Same auto-fit pattern
   but with larger min-width for fuller content.
   ───────────────────────────────────────────────────────────── */
.sec-cards {
  padding: var(--space-24) var(--gutter);
  max-width: 80rem;
  margin-inline: auto;

  > p[style*="text-align: center"] { text-wrap: balance; }
  > h2[style*="text-align: center"] { text-wrap: balance; }
}
.sec-cards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--space-6);
  margin-top: var(--space-12);
}
.sec-cards.compact .sec-cards-grid {
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--space-4);
}


/* ─────────────────────────────────────────────────────────────
   PROSE
   Article body. Single column, generous measure.
   Children inherit good defaults from typography classes.
   ───────────────────────────────────────────────────────────── */
.sec-prose {
  padding: var(--space-16) var(--gutter);
  max-width: 44rem;
  margin-inline: auto;

  /* Default vertical rhythm for arbitrary children. */
  > * + * { margin-top: var(--space-6); }
  > h2 + *,
  > h3 + * { margin-top: var(--space-3); }
  > h2 { margin-top: var(--space-12); }
  > h3 { margin-top: var(--space-8); }

  /* Section headings: match the brand's thin-display register.
     Browser default for <h2>/<h3> is bold, which clashes with the
     thin-weight headings used everywhere else on the site. */
  h2 {
    font-family: var(--font-body);
    font-size: var(--text-xl);
    font-weight: 300;
    letter-spacing: -0.01em;
    line-height: 1.25;
    color: var(--text-0);
  }
  h3 {
    font-family: var(--font-body);
    font-size: var(--text-lg);
    font-weight: 400;
    line-height: 1.3;
    color: var(--text-0);
  }
  p { color: var(--text-1); line-height: 1.7; }
  a {
    color: var(--text-0);
    text-decoration: underline;
    text-decoration-color: var(--border-default);
    text-underline-offset: 0.15em;
    transition: text-decoration-color var(--duration-fast);
  }
  a:hover { text-decoration-color: var(--text-2); }
  .prose-meta {
    font-size: var(--text-sm);
    color: var(--text-2);
  }
}


/* ─────────────────────────────────────────────────────────────
   BAND
   Full-bleed callout strip. Uses accent-surface for fill.
   Usually contains a CTA or a single statement.
   ───────────────────────────────────────────────────────────── */
.sec-band {
  padding: var(--space-16) var(--gutter);
  background: var(--accent-surface);
  border-block: 1px solid var(--accent-border);
}
.sec-band.bordered {
  background: transparent;
  border-block-color: var(--border-subtle);
}
.sec-band-inner {
  max-width: 72rem;
  margin-inline: auto;
}


/* ─────────────────────────────────────────────────────────────
   CTA
   End-of-page conversion section. Centered, with glow.
   ───────────────────────────────────────────────────────────── */
.sec-cta {
  position: relative;
  padding: var(--space-24) var(--gutter);
  text-align: center;
  overflow: hidden;
  max-width: 56rem;
  margin-inline: auto;

  > * { position: relative; margin-inline: auto; }
}


/* ─────────────────────────────────────────────────────────────
   APP LAYOUTS
   Two structural layouts for app-style pages (docs, playground).
   Use on the body or a top-level wrapper.
   ───────────────────────────────────────────────────────────── */
.layout-rail {
  display: grid;
  grid-template-columns: 280px 1fr;
  min-height: 100vh;

  > nav { grid-column: 1 / -1; }
  > aside { border-right: 1px solid var(--border-subtle); overflow-y: auto; }
  > main { overflow-y: auto; }
  > footer { grid-column: 1 / -1; }
}

.layout-app {
  display: grid;
  grid-template-columns: 280px 1fr;
  grid-template-rows: var(--nav-h) 1fr;
  height: 100vh;

  > nav { grid-column: 1 / -1; }
  > aside { overflow-y: auto; border-right: 1px solid var(--border-subtle); }
  > main { overflow-y: auto; }
}


/* ─────────────────────────────────────────────────────────────
   RESPONSIVE
   Single breakpoint. Below 768px, multi-column layouts collapse.
   ───────────────────────────────────────────────────────────── */
@media (width <= 768px) {
  .sec-split { grid-template-columns: 1fr; gap: var(--space-8); }
  .sec-split.reverse > :first-child { order: 0; }

  .layout-rail,
  .layout-app {
    grid-template-columns: 1fr;
  }
  .layout-rail > aside,
  .layout-app > aside {
    display: none;
  }
}
}
