/* ==========================================================================
   PeptideAI design system
   Implements DESIGN.md. Every value here has a section number next to it.
   If you change a value, change DESIGN.md too, or that file becomes a lie.
   ========================================================================== */

/* --- Faces (DESIGN.md §4) ------------------------------------------------ */

@font-face {
  font-family: 'Newsreader';
  font-style: normal;
  font-weight: 200 700;
  font-display: swap;
  src: url(fonts/newsreader-var-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Geist Mono';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(fonts/geist-mono-var-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Caveat';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url(fonts/caveat-var-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* --- Tokens -------------------------------------------------------------- */

:root {
  /* Colour (§3). ACCENT is sampled from the Play Store screenshot so the
     site and the app agree. It is never type on light: 1.55:1. On BAND it
     measures 10.68:1, which is why it can be type there. */
  --pd-bg:        #f4f3ef;
  --pd-ink:       #191919;
  --pd-muted:     #6b6b64;   /* 4.83:1 on --pd-bg */
  --pd-band:      #141413;
  --pd-card-ink:  #f2f1ec;
  --pd-accent:    #00e0a0;

  /* Grid (§1). One dash definition, so verticals, rules and crosshairs
     cannot drift apart. */
  --pd-cols:      8;
  --pd-dash-on:   6px;
  --pd-dash-off:  11px;
  --pd-line:      rgba(25, 25, 25, 0.22);
  --pd-band-line: rgba(242, 241, 236, 0.20);

  /* Crosshairs (§2) */
  --pd-dot:       rgba(25, 25, 25, 0.45);
  --pd-band-dot:  rgba(242, 241, 236, 0.45);
  --pd-dot-box:   11px;

  /* The dash, defined once. Five copies of these four stops existed. Dark
     surfaces re-point the two tokens and everything inside them inherits,
     the same trick as --pd-focus-gap / --pd-focus-ring. */
  --pd-dash-v: repeating-linear-gradient(to bottom,
    var(--pd-line) 0, var(--pd-line) var(--pd-dash-on),
    transparent var(--pd-dash-on),
    transparent calc(var(--pd-dash-on) + var(--pd-dash-off)));
  --pd-dash-h: repeating-linear-gradient(to right,
    var(--pd-line) 0, var(--pd-line) var(--pd-dash-on),
    transparent var(--pd-dash-on),
    transparent calc(var(--pd-dash-on) + var(--pd-dash-off)));
  --pd-dot-arm:   5.5px;

  /* Surfaces (§5) */
  --pd-edge:      16px;   /* near-edge inset for Wide surfaces */
  --pd-r-card:    26px;   /* hero, spotlight, runner band */
  --pd-r-feature: 16px;   /* feature cards */
  --pd-nest:      rgba(242, 241, 236, 0.07);
  --pd-feature-fill:   rgba(242, 241, 236, 0.04);
  --pd-feature-stroke: rgba(242, 241, 236, 0.10);

  /* Motion (§6) */
  --pd-enter:     520ms cubic-bezier(0.2, 0, 0, 1);
  --pd-quick:     150ms ease-out;
  --pd-card-hov:  160ms ease-out;

  /* Faces (§4) */
  --pd-display: 'Newsreader', 'Iowan Old Style', Georgia, serif;
  --pd-body:    'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --pd-mono:    'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --pd-hand:    'Caveat', cursive;
}

/* --- Reset + root -------------------------------------------------------- */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--pd-bg);
  color: var(--pd-ink);
  font-family: var(--pd-body);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* The edge crosshairs are centred on the 0% and 100% gridlines, so half of
   each hangs past the viewport. clip, not hidden: hidden on body alone still
   let the document scroll 5.5px sideways, and clip does not create a scroll
   container, which would break every view() timeline on the page. */
html, body { overflow-x: clip; }
/* The containing block for .pd-grid, so the lattice is document-tall.
   flow-root as well as relative: without a block formatting context the
   hero's 80px top margin collapses out through body, body's box starts 80px
   down, and the grid starts 80px late and ends 80px short. */
body { position: relative; display: flow-root; }

img, svg, video { display: block; max-width: 100%; }


/* The store menu under the nav button. Nested-card radii: 14 outer, 6 inset,
   so the rows are 8 and the gap stays constant through the corner. It scales
   from the trigger, never from its own centre. */
.pd-nav-menu {
  position: absolute;
  top: calc(100% + 8px);
  left: 50%;
  z-index: 70;
  display: none;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  border-radius: 14px;
  background: rgba(244, 243, 239, 0.92);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  backdrop-filter: blur(16px) saturate(140%);
  border: 1px solid rgba(25, 25, 25, 0.12);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5) inset,
              0 18px 40px rgba(0, 0, 0, 0.16),
              0 4px 12px rgba(0, 0, 0, 0.10);
  transform: translateX(-50%);
  transform-origin: top center;
}
.pd-nav-menu[data-open="true"] { display: flex; }
/* .pd-nav .pd-nav-menu a, not .pd-nav-menu a: the base .pd-nav a rule is the
   same (0,1,1) and was winning on order, which held these under the 44 they
   declare. Same trap as the nav CTA and the swap button before it. */
.pd-nav .pd-nav-menu a {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  /* flex: none, or the column shrinks them under the 44 they declare. */
  flex: none;
  height: 44px;                    /* explicit: min-height was resolving to 42 */
  padding: 0 16px;
  border-radius: 8px;              /* 14 outer minus the 6 inset */
  color: var(--pd-ink);
  font-family: var(--pd-mono);
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--pd-quick);
}
/* On a dark bar it takes the band's surface instead. */
.pd-nav--on-dark .pd-nav-menu {
  background: rgba(20, 20, 19, 0.86);
  border-color: rgba(242, 241, 236, 0.16);
  box-shadow: 0 1px 0 rgba(242, 241, 236, 0.10) inset,
              0 18px 40px rgba(0, 0, 0, 0.42);
}
.pd-nav--on-dark .pd-nav-menu a { color: var(--pd-card-ink); }

@media (prefers-reduced-motion: no-preference) {
  .pd-nav-menu[data-open="true"] { animation: pd-menu-in 160ms var(--pd-ease-out, cubic-bezier(0.23, 1, 0.32, 1)) both; }
}
@keyframes pd-menu-in {
  from { opacity: 0; transform: translateX(-50%) scale(0.96); }
  to   { opacity: 1; transform: translateX(-50%) scale(1); }
}

/* Below the breakpoint the centre cell is the wordmark alone and the drawer
   carries the stores, so neither the button nor its menu appear. */
@media (max-width: 900px) {
  .pd-nav .pd-nav-menu { display: none; }
}

/* The rule under the bar. The bar is fixed so it takes no flow space; this
   sits where the bar ends and everything below it measures from here, which
   is how the original stacks: nav, rule, then a 16px gap to the card. */
.pd-nav-rule {
  position: relative;
  z-index: 1;
  margin-top: 64px;
}
@media (max-width: 900px) {
  .pd-nav-rule { margin-top: 56px; }
  /* The rule's marks are already hidden at this width; the line stays. */
}

/* ==========================================================================
   §7b  Subpage hero
   Every page that is not the homepage opens on the same object the homepage
   does: a near-edge rounded card carrying the hero silk, with light type on
   it. There are three header shapes across the site, .page-hero on 23 pages,
   .cpd-head on 66 and .art-head on 14, and they all take one treatment, so a
   compound page, an article and a catalogue page open the same way.
   ========================================================================== */

.page-hero,
.cpd-head,
.art-head,
.legal-hero,
.dl-hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  overflow: clip;                       /* see .pd-card: never "hidden" */
  /* The nav rule above supplies the clearance now, so this is just the edge
     gap under it. */
  margin-block: var(--pd-edge) clamp(40px, 5vw, 64px);
  border-radius: var(--pd-r-card);
  background: #101010;
  color: var(--pd-card-ink);
  padding: clamp(88px, 9vw, 120px) clamp(28px, 5vw, 68px) clamp(40px, 5vw, 60px);
}

/* .page-hero is a child of <body> and goes straight to the near edge. The
   other two sit inside a 12.5% inset and are pulled back out to it:
   -16.6667% of a 75%-wide parent is exactly 12.5% of the viewport, which is
   the same trick .pd-rule already uses to reach the gridlines. */
.page-hero,
.legal-hero,
.dl-hero { margin-inline: var(--pd-edge); }
/* Inside a 12.5% inset, so they need the pull-out. .page-top is the four
   pages where the header had to be wrapped because it shared a section with
   the content grid. */
.cpd-head,
.art-head,
.page-top > .page-hero { margin-inline: calc(-16.6667% + var(--pd-edge)); }

/* The silk, then the tint that holds the type's contrast over it. Two
   pseudos rather than one gradient over a background-image, so the image can
   be swapped in one place. */
.page-hero::before,
.cpd-head::before,
.art-head::before,
.legal-hero::before,
.dl-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -2;
  background: #101010 url("hero-bg.webp") center / cover no-repeat;
}
.page-hero::after,
.cpd-head::after,
.art-head::after,
.legal-hero::after,
.dl-hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(180deg, rgba(10, 12, 11, 0.60) 0%, rgba(10, 12, 11, 0.32) 46%, rgba(10, 12, 11, 0.55) 100%);
}

/* Type on the card. One :is() list rather than five selectors per rule: the
   five hero shapes carry different markup for the same three jobs, a heading,
   a standfirst, and a row of small muted labels above them. */
:is(.page-hero, .cpd-head, .art-head, .legal-hero, .dl-hero) :is(h1, .page-h1, .cpd-title) {
  color: var(--pd-card-ink);
}
:is(.page-hero, .cpd-head, .art-head, .legal-hero, .dl-hero) :is(p, .page-sub, .section-lead, .cpd-lead, .art-dek) {
  color: rgba(242, 241, 236, 0.72);
}
:is(.page-hero, .cpd-head, .art-head, .legal-hero, .dl-hero)
  :is(.crumb, .crumb a, .breadcrumbs, .breadcrumbs a, .section-label, .hero-kicker,
      .art-kicker, .cpd-cat, .cpd-aka, .dl-trust) {
  color: rgba(242, 241, 236, 0.62);
}
:is(.page-hero, .cpd-head, .art-head, .legal-hero, .dl-hero)
  :is(.section-label, .hero-kicker)::before,
:is(.page-hero, .cpd-head, .art-head, .legal-hero, .dl-hero) .dl-trust span::before {
  background: rgba(242, 241, 236, 0.62);
}
/* 0.55, not 0.4. At 0.4 on the card it measured 3.5:1 at 11.84px, and a
   separator is still text. */
:is(.page-hero, .cpd-head, .art-head, .legal-hero, .dl-hero)
  :is(.crumb .sep, .breadcrumbs span) {
  color: rgba(242, 241, 236, 0.55);
}
/* The compound breadcrumb is the first thing in the card, so it needs the gap
   the .cpd-cat below it would otherwise have swallowed. */
.cpd-head .breadcrumbs { margin-bottom: 22px; }

/* The evidence tiers and the goal chips sit on this card too, so they take
   their dark-surface forms. */
.cpd-head .rd-badge, .cpd-head .rd-pill {
  border-color: rgba(242, 241, 236, 0.28);
  color: rgba(242, 241, 236, 0.72);
}
.cpd-head .rd-extensive {
  background: none;
  border-color: var(--pd-accent);
  color: var(--pd-accent);
}
.cpd-head .cpd-goal-link {
  border-color: rgba(242, 241, 236, 0.28);
  color: var(--pd-card-ink);
}

@media (max-width: 900px) {
  /* The insets drop to 24px at this width, so the pull-out is a pixel value
     rather than a percentage of a box that is no longer 75% wide. */
  .cpd-head,
  .art-head,
  .page-top > .page-hero { margin-inline: -12px; }
  .page-hero,
  .legal-hero,
  .dl-hero { margin-inline: 12px; }
  .page-hero,
  .cpd-head,
  .art-head,
  .legal-hero,
  .dl-hero {
    margin-top: 12px;
    padding-top: clamp(44px, 9vw, 64px);
  }
}

@media (hover: hover) {
  :is(.page-hero, .cpd-head, .art-head, .legal-hero, .dl-hero)
    :is(.crumb a, .breadcrumbs a):hover { color: var(--pd-card-ink); }
  .cpd-head .cpd-goal-link:hover { background: rgba(242, 241, 236, 0.10); }
}

/* Breadcrumbs. Here rather than in pages.css: 13 of the 19 pages that carry a
   .crumb load article.css instead, and were rendering the trail as full-size
   black 16px body type with no gaps. */
.crumb, .breadcrumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-bottom: 26px;
  font-family: var(--pd-mono);
  font-size: 0.74rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--pd-muted);
}
.crumb a, .breadcrumbs a { color: var(--pd-muted); text-decoration: none; }
.crumb .sep, .breadcrumbs .sep { opacity: 0.5; }

/* One focus ring, here rather than in pages.css: the 14 article pages and the
   password reset page do not load pages.css and had no ring at all. Two
   shadows, the first faking the gap so the ring follows the border radius,
   which an outline did not in Safari for years. Never animated in. Dark
   surfaces re-point the two tokens; everything inside them inherits. */
:root {
  --pd-focus-gap:  var(--pd-bg);
  --pd-focus-ring: var(--pd-ink);
}
.pd-band, .pd-card, .foot, .statement-box, .spotlight, .lattice, .endcta-media {
  --pd-focus-gap:  var(--pd-band);
  --pd-focus-ring: var(--pd-card-ink);
}
:focus-visible {
  transition: none;
  outline: none;
  box-shadow: 0 0 0 2px var(--pd-focus-gap), 0 0 0 4px var(--pd-focus-ring);
}

::selection { background: var(--pd-accent); color: var(--pd-band); }

/* ==========================================================================
   §1  The grid
   Columns 1 and 8 stay empty. Prose lives in 2 through 7.
   The 4/5 boundary is deleted: it lands dead centre and cuts through
   anything centred there, starting with the logo. Do not add it back.
   No max width anywhere: the grid, the inset track and the cards used to
   share one cap and were removed together so they stay aligned.
   ========================================================================== */

/* absolute against the body, not fixed against the viewport. Fixed, the
   vertical dashes did not move while the page scrolled under them, so every
   line drawn in document space beat against them: measured on one hairline
   across 9px of scroll it rendered 6-on/11-off, then 9/8, then 12/5, then
   6-on/2-off. The original anchors its grid to the document, and this is that.
   body carries position: relative so inset: 0 resolves against the document
   height rather than the viewport's. */
.pd-grid {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 0;
}

.pd-grid i {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background-image: var(--pd-dash-v);
}

/* Every column boundary from 0/8 to 8/8, minus the centre line at 4/8. The
   two page edges were missing, so the grid stopped one line short at each
   side and the outer columns had nothing bounding them. */
.pd-grid i:nth-child(1) { left: 0; }
.pd-grid i:nth-child(2) { left: 12.5%; }
.pd-grid i:nth-child(3) { left: 25%; }
.pd-grid i:nth-child(4) { left: 37.5%; }
.pd-grid i:nth-child(5) { left: 62.5%; }
.pd-grid i:nth-child(6) { left: 75%; }
.pd-grid i:nth-child(7) { left: 87.5%; }
.pd-grid i:nth-child(8) { left: auto; right: 0; }

/* Inset: columns 2 through 7. The default. Reach for this first. */
.pd-inset {
  position: relative;
  z-index: 1;
  margin-inline: 12.5%;
}

/* Wide: near-edge, 16px. Surfaces bleed, words do not. */
.pd-wide {
  position: relative;
  z-index: 1;
  margin-inline: var(--pd-edge);
}

@media (max-width: 900px) {
  /* Below this the outer gutter costs more than it proves. Hold the idea,
     shrink the reserve, and let the hairlines go. */
  .pd-inset { margin-inline: 24px; }
  .pd-wide  { margin-inline: 12px; }
  .pd-grid  { display: none; }
}

/* ==========================================================================
   §2  Crosshairs
   A crosshair marks a real crossing of two lines. Four lines meet -> full
   mark. Three -> half mark. Two (an L, a box corner) -> no mark.
   If you cannot name the two lines that cross at a mark, delete the mark.
   ========================================================================== */

.pd-rule {
  position: relative;
  height: 1px;
  border: 0;
  margin: 0;
  background-image: var(--pd-dash-h);
}

/* The rule spans full width and its crosshairs are children of the rule, so
   "top: 50%" resolves against the 1px line itself rather than against the
   section. Keep a ruled crosshair at the top level of a full-width section,
   never inside an Inset: an inset parent makes the marks box-relative and
   they drift off the verticals by half the overflow at every width but one. */
.pd-rule-marks {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 1px;
}

/* The +0.5px is not a fudge. A 1px hairline placed at `left: 25%` occupies
   the pixel [25%, 25%+1], so its optical centre is 25% + 0.5, not 25%. The
   mark's own box centre has to land there or the vertical arm sits half a
   pixel to the LEFT of every gridline it is supposed to be marking, which is
   what it was doing: measured, arm at [-0.5, 0.5] against a line at [0, 1] at
   every one of the eight positions. Vertically the same correction is already
   implicit: `top: 50%` of the 1px .pd-rule-marks resolves to 0.5px, which is
   the rule's centre. Two exceptions re-point this below, both of them lines
   drawn from a right edge inward: .pd-x-7 and .endcta-seam. */
.pd-x {
  position: absolute;
  top: 50%;
  width: var(--pd-dot-box);
  height: var(--pd-dot-box);
  margin-left: calc(var(--pd-dot-box) / -2 + 0.5px);
  margin-top: calc(var(--pd-dot-box) / -2);
}

.pd-x::before,
.pd-x::after {
  content: "";
  position: absolute;
  background: var(--pd-dot);
}

/* horizontal arm, always full width */
.pd-x::before {
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  transform: translateY(-0.5px);
}

/* vertical arm, trimmed by arms= */
.pd-x::after {
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  transform: translateX(-0.5px);
}

.pd-x[data-arms="up"]::after   { bottom: 50%; }
.pd-x[data-arms="down"]::after { top: 50%; }

/* Viewport-relative: for a rule that spans the full width. -0 and -7 are the
   two page edges. Both stay centred on their line like every other mark, so
   half of each hangs off the viewport; body already carries overflow-x: clip,
   so neither adds a scrollbar. */
.pd-x-0 { left: 0; }
.pd-x-1 { left: 12.5%; }
.pd-x-2 { left: 25%; }
.pd-x-3 { left: 37.5%; }
.pd-x-4 { left: 62.5%; }
.pd-x-5 { left: 75%; }
.pd-x-6 { left: 87.5%; }
/* The last gridline is `right: 0`, so it occupies [100%-1, 100%] and its
   centre is 0.5px to the LEFT of the edge, the mirror of every other line.
   Same formula, applied to margin-right, which moves the box the other way. */
.pd-x-7 { left: auto; right: 0; margin-left: 0; margin-right: calc(var(--pd-dot-box) / -2 + 0.5px); }

/* A rule that sits inside an Inset or a Wide surface is pulled back out to
   the viewport edges, so the percentages above keep meaning the same columns
   the verticals use. This is the whole geometry trap in one declaration. */
.pd-inset > .pd-rule,
.pd-inset > .pd-rule-holder {
  margin-inline: -16.6667%;   /* the inset track is 75% wide: 12.5/75 */
}
.pd-wide > .pd-rule,
.pd-wide > .pd-rule-holder {
  margin-inline: calc(var(--pd-edge) * -1);
}

/* The pull-out has to follow the inset down at the breakpoint. Left at
   -16.6667% it is a percentage of a 24px-inset box, which overshoots the
   viewport and gives the whole document a horizontal scrollbar. This has to
   sit AFTER the declaration above: same specificity, so source order decides. */
@media (max-width: 900px) {
  .pd-inset > .pd-rule,
  .pd-inset > .pd-rule-holder { margin-inline: -24px; }
  .pd-wide > .pd-rule,
  .pd-wide > .pd-rule-holder  { margin-inline: -12px; }
}

/* On dark surfaces the two dash tokens re-point, so .pd-rule needs no variant
   of its own; only the crosshair ink does. */
.pd-band, .pd-card, .foot, .statement-box, .spotlight, .lattice, .endcta-media, .pd-rule--band {
  --pd-dash-v: repeating-linear-gradient(to bottom,
    var(--pd-band-line) 0, var(--pd-band-line) var(--pd-dash-on),
    transparent var(--pd-dash-on),
    transparent calc(var(--pd-dash-on) + var(--pd-dash-off)));
  --pd-dash-h: repeating-linear-gradient(to right,
    var(--pd-band-line) 0, var(--pd-band-line) var(--pd-dash-on),
    transparent var(--pd-dash-on),
    transparent calc(var(--pd-dash-on) + var(--pd-dash-off)));
}
/* Same list as the dash re-point above, and for the same reason: a mark that
   lands on any of these surfaces needs the light ink, not the dark one. It
   used to name .pd-band only, so a mark placed inside a .pd-card or the
   footer would have been drawn in near-black on near-black. */
.pd-band .pd-x::before, .pd-band .pd-x::after,
.pd-card .pd-x::before, .pd-card .pd-x::after,
.foot .pd-x::before, .foot .pd-x::after,
.statement-box .pd-x::before, .statement-box .pd-x::after,
.spotlight .pd-x::before, .spotlight .pd-x::after,
.lattice .pd-x::before, .lattice .pd-x::after,
.endcta-media .pd-x::before, .endcta-media .pd-x::after,
.pd-rule--band .pd-x::before, .pd-rule--band .pd-x::after { background: var(--pd-band-dot); }

@media (max-width: 900px) { .pd-rule-marks { display: none; } }

/* ==========================================================================
   §4  Type
   The serif argues, the sans explains, the mono carries anything that is a
   measurement. Mono means "this is data": do not use it for prose.
   ========================================================================== */

.pd-display,
h1, h2, h3 {
  font-family: var(--pd-display);
  font-weight: 300;
  line-height: 1.06;
  /* normal. The original never touches tracking on a heading and neither
     does the house rule. */
  text-wrap: balance;
  margin: 0;
}

h1 { font-size: clamp(2.6rem, 6.2vw, 4.6rem); }
h2 { font-size: clamp(2.1rem, 4.2vw, 3.2rem); }
h3 { font-size: clamp(1.4rem, 2.2vw, 1.9rem); }

p { margin: 0 0 1em; text-wrap: pretty; }
p:last-child { margin-bottom: 0; }

.pd-lede {
  font-family: var(--pd-display);
  font-weight: 300;
  font-size: clamp(1.35rem, 2.5vw, 2rem);
  line-height: 1.32;
  text-wrap: pretty;
}

.pd-body  { font-family: var(--pd-body); font-size: 1rem; }
.pd-muted { color: var(--pd-muted); }

/* Mono is for measurements, metadata, captions and small labels. */
.pd-mono {
  font-family: var(--pd-mono);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
.pd-mono--up { text-transform: uppercase; }

/* Caveat is scoped to the two margin annotations beside the evidence table
   and nowhere else. It is a human hand pointing at a table. */
.pd-hand {
  font-family: var(--pd-hand);
  font-weight: 600;
  font-size: 1.35rem;
  color: var(--pd-muted);
}

/* Anything that counts. */
.pd-num {
  font-family: var(--pd-display);
  font-weight: 200;
  font-variant-numeric: tabular-nums;
  font-size: clamp(2.1rem, 3.6vw, 3rem);
  line-height: 1;
}

/* Multi-word technical terms that must not split at a hyphen. */
.pd-nowrap { white-space: nowrap; }

/* ==========================================================================
   §5  Surfaces
   Concentric radii: inner radius = outer radius - inset. A square parent
   gets square children. Elevation is layered transparent shadow plus a 1px
   inset highlight. Never a glow.
   ========================================================================== */

/* Near-edge rounded card: hero, spotlight, runner band. */
.pd-card {
  position: relative;
  border-radius: var(--pd-r-card);
  /* clip, NOT hidden. overflow:hidden makes this a scroll container, and
     every view() timeline inside then resolves against a box that never
     scrolls, freezing it on its start frame (DESIGN.md §6). overflow:clip
     clips without creating one. Browsers without clip fall back to hidden
     and land on the animation's end state, which is the correct fallback. */
  overflow: hidden;
  overflow: clip;
  background: var(--pd-band);
  color: var(--pd-card-ink);
}

/* Square box sitting on the grid, rules top and bottom. */
.pd-box {
  position: relative;
  border-radius: 0;
}

/* Nested block inside a square box: square, because its parent is. */
.pd-nested {
  background: var(--pd-nest);
  border-radius: 0;
  padding: 20px;
}

.pd-band {
  background: var(--pd-band);
  color: var(--pd-card-ink);
}
.pd-band .pd-muted { color: rgba(242, 241, 236, 0.62); }

.pd-elev {
  box-shadow:
    0 1px 0 rgba(242, 241, 236, 0.10) inset,
    0 10px 30px rgba(0, 0, 0, 0.34),
    0 2px 8px rgba(0, 0, 0, 0.22);
}

/* Glass, for the floating hero chips. */
.pd-glass {
  background: rgba(12, 18, 15, 0.34);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  backdrop-filter: blur(16px) saturate(150%);
  border: 1px solid rgba(242, 241, 236, 0.18);
  box-shadow:
    0 1px 0 rgba(242, 241, 236, 0.10) inset,
    0 10px 30px rgba(0, 0, 0, 0.34),
    0 2px 8px rgba(0, 0, 0, 0.22);
  border-radius: 12px;
  padding: 12px 16px;
  color: var(--pd-card-ink);
}

/* The compound table is a heavier, opaque variant on purpose. It carries
   data, so it reads as a panel, not as a floating chip. */
.pd-panel {
  background: rgba(20, 20, 19, 0.72);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
  border: 1px solid rgba(242, 241, 236, 0.12);
  border-radius: 0;
  color: var(--pd-card-ink);
}

/* Feature cards */
.pd-feature {
  border-radius: var(--pd-r-feature);
  background: var(--pd-feature-fill);
  border: 1px solid var(--pd-feature-stroke);
  padding: 28px;
  transition: background var(--pd-card-hov), border-color var(--pd-card-hov);
}

/* ==========================================================================
   §5  The device frame
   Never a raw screenshot with a border-radius on it. Every value scales off
   one width, so the proportions hold at any size. Set --w on .pd-device.
   ========================================================================== */

.pd-device {
  --w: 260px;
  position: relative;
  width: var(--w);
  padding: calc(var(--w) * 0.028);                       /* bezel */
  border-radius: calc(var(--w) * 0.135);                 /* outer R */
  /* the bezel: a lit top edge falling to a dark body, so it reads as a solid
     object rather than a flat rounded rectangle */
  background: linear-gradient(155deg, #3a3a37 0%, #1b1b19 26%, #0e0e0d 62%, #2b2b28 100%);
  /* Layered transparent shadow plus a 1px inset highlight. Never a glow (§5). */
  box-shadow:
    0 1px 0 rgba(242, 241, 236, 0.18) inset,
    0 0 0 1px rgba(0, 0, 0, 0.55),
    0 40px 80px rgba(0, 0, 0, 0.52),
    0 14px 34px rgba(0, 0, 0, 0.40),
    0 3px 10px rgba(0, 0, 0, 0.30);
}

.pd-device-screen {
  position: relative;
  overflow: hidden;
  /* inner R = outer R - bezel. Concentric, same rule as everything else. */
  border-radius: calc(var(--w) * 0.135 - var(--w) * 0.028);
  background: #0b0b0a;
  /* the real app-shot ratio, so nothing is cropped */
  aspect-ratio: 528 / 1149;
}
.pd-device-screen img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

.pd-device-island {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: calc(var(--w) * 0.022);
  width: calc(var(--w) * 0.30);
  height: calc(var(--w) * 0.085);
  border-radius: 999px;
  background: #080908;
  z-index: 2;
}

/* ==========================================================================
   §3  The accent, and the seven placements
   One accent, and every placement is on a list. Adding an eighth means
   removing one. Resting and hover colour live in the same rule: an inline
   style beats :hover, and the nav shipped broken that way twice.
   ========================================================================== */

/* 1. Hero primary CTA - fill, dark text on it */
.pd-cta {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 15px 28px;
  /* The one rounded thing on the page, and specifically asked for: the pill
     pair from the reference the hero was built against. */
  border-radius: 999px;
  background: var(--pd-accent);
  /* Deeper than --pd-band on the accent fill, same as .pd-chip-fill. */
  color: #04120c;
  font-family: var(--pd-body);
  font-weight: 500;
  font-size: 0.98rem;
  text-decoration: none;
  border: 0;
  cursor: pointer;
  transition: background var(--pd-quick), transform var(--pd-quick);
}
.pd-cta:active { transform: scale(0.96); }

/* Secondary sits next to it and takes no accent. */
/* Glass, not a flat tint: the hero CTA sits over the device, and at 10%
   opacity the screen behind it read straight through the button. */
/* The secondary beside the accent pill. A light tint of card ink, not a dark
   glass panel: measured on the original, rgba(242,241,236,0.14) in both the
   hero and the footer. It reads as the same object held back, where a dark
   fill reads as a different control. No backdrop-filter either, which was
   blurring the hero silk behind a button that sits on it. */
.pd-cta--ghost {
  background: rgba(242, 241, 236, 0.14);
  color: var(--pd-card-ink);
  border: 0;
}

/* Ink and page background, not band and card ink. This pair stands on the
   light page, so it takes the light page's two colours. */
.pd-cta--ink { background: var(--pd-ink); color: var(--pd-bg); }

/* The inverse of --ink, for the primary of a pair standing on a dark band.
   The footer had two ghosts side by side and no primary at all. Card ink
   rather than the accent: the accent list is closed at seven. */
.pd-cta--card { background: var(--pd-card-ink); color: #07110d; }

/* The secondary of a pair standing on the page. --ghost is built for dark
   surfaces; on cream it reads as a second heavy black pill with no hierarchy. */
.pd-cta--soft { background: rgba(25, 25, 25, 0.07); color: var(--pd-ink); }

/* 2. Hero ON CYCLE tag - type on dark */
.pd-tag-accent { color: var(--pd-accent); }

/* 3. Spotlight Insights chip - 1px border + type on dark */
.pd-chip-accent {
  display: inline-block;
  padding: 5px 12px;
  border: 1px solid var(--pd-accent);
  color: var(--pd-accent);
  font-family: var(--pd-mono);
  font-size: 0.78rem;
}

/* 4. Evidence "Extensive research" chip - fill, dark text on it */
.pd-chip-fill {
  display: inline-block;
  padding: 4px 8px;
  background: var(--pd-accent);
  /* Darker than --pd-band on the accent: this is the one place a fill carries
     type, so it takes the deepest ink the palette has. */
  color: #04120c;
  font-family: var(--pd-mono);
  font-size: 0.75rem;
  line-height: 1.5;
}

/* 5. Feature card Premium badge - 1px border + type on dark */
.pd-badge {
  display: inline-block;
  padding: 4px 10px;
  line-height: 1.5;
  border: 1px solid var(--pd-accent);
  color: var(--pd-accent);
  font-family: var(--pd-mono);
  font-size: 0.6875rem;
}

/* 6. Runner band opening hairline - 32x1px rule */
.pd-hairline {
  width: 32px;
  height: 1px;
  background: var(--pd-accent);
  border: 0;
  margin: 0 0 20px;
}

/* 7. Compound table, extensive tier - name + grade, type on dark */
.pd-tier-extensive { color: var(--pd-accent); }

/* ==========================================================================
   §7  Hit areas
   44x44 on touch, 40x40 on dense desktop. Where a control is visually
   smaller, extend it with .pd-tap rather than making it look bigger.
   ========================================================================== */

.pd-tap { position: relative; }
.pd-tap::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  min-width: 44px;
  min-height: 44px;
  width: 100%;
  height: 100%;
}
@media (pointer: fine) {
  .pd-tap::after { min-width: 40px; min-height: 40px; }
}

/* ==========================================================================
   §6  Motion
   Transform, opacity, clip-path and colour only. Never transition: all.
   Nothing that triggers layout. Everything under 200ms except entrances,
   which get ~520ms. Idle: none. Nothing loops off-screen.
   ========================================================================== */

.pd-press { transition: transform var(--pd-quick); }
.pd-press:active { transform: scale(0.96); }

/* Entrance, scroll-driven, no JavaScript. */
@supports (animation-timeline: view()) {
  .pd-enter {
    animation-name: pd-rise;
    animation-duration: auto;          /* NOT the shorthand's implicit 0s */
    animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 10% entry 62%;
  }
}
@keyframes pd-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* Word-by-word reveal in the statement. The paragraph must NOT be
   display:flex: the spans are inline and need natural whitespace for word
   spacing. Start the range around 34%, not 0%, or the first words darken
   while the paragraph is still off screen. */
/* --pd-muted is documented as 4.83:1 on --pd-bg, the light page. This
   paragraph sits on --pd-band, where the same grey measures 3.5:1. The
   resting colour is card ink held back instead, which is the same family as
   the text it darkens into. */
.pd-reveal { color: rgba(242, 241, 236, 0.42); }
.pd-reveal span { color: inherit; }

/* Deliberately NOT inside @supports. Where animation-timeline is unsupported
   the animation still applies, animation-duration: auto resolves to 0s and
   fill-mode lands the end state, so the paragraph simply reads fully dark.
   Gating it on @supports is what left it frozen on its start colour forever
   in every browser without scroll timelines. */
.pd-reveal span {
  animation-name: pd-reveal-ink;
  animation-duration: auto;          /* NOT the shorthand's implicit 0s */
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-timeline: view();
  animation-range:
    cover calc(34% + var(--i) * 1.15%)
    cover calc(47% + var(--i) * 1.15%);
}
@keyframes pd-reveal-ink {
  to { color: var(--pd-card-ink); }
}
/* Reduce turns the ramp off by landing the end state, never by leaving the
   paragraph on the resting colour. */
@media (prefers-reduced-motion: reduce) {
  .pd-reveal, .pd-reveal span { animation: none; color: var(--pd-card-ink); }
}

/* The statement card opening from inset() to full width. No overflow-hidden
   on anything wrapping this: it would make that element a scroll container
   and every view() timeline inside would resolve against a box that never
   scrolls. clip-path already does the clipping. */
@supports (animation-timeline: view()) {
  .pd-open {
    animation-name: pd-open-card;
    animation-duration: auto;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;
  }
}
@keyframes pd-open-card {
  from { clip-path: inset(0 10% 0 10%); }
  to   { clip-path: inset(0 0 0 0); }
}

/* Parallax drift on the image behind the statement. */
@supports (animation-timeline: view()) {
  .pd-drift {
    animation-name: pd-drift-y;
    animation-duration: auto;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }
}
@keyframes pd-drift-y {
  from { transform: translateY(-5%) scale(1.12); }
  to   { transform: translateY(5%)  scale(1.12); }
}

/* The store-button ring. Hover spins a conic gradient once around the
   border, neutral rather than accent so it stays an affordance. Masked to
   its own border with mask-composite: exclude so it sits ON the button.
   The usual z-index:-1 version disappears behind the fill. */
.pd-ring { position: relative; }
.pd-ring::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: conic-gradient(
    from var(--pd-ring-a, 0deg),
    transparent 0deg,
    rgba(242, 241, 236, 0.75) 40deg,
    transparent 110deg,
    transparent 360deg
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  /* 280ms, not the 150ms quick. The conic angle resets the instant the pointer
     leaves, and a slower fade is what covers that reset. */
  transition: opacity 280ms ease;
}
@property --pd-ring-a {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}
@keyframes pd-spin {
  to { --pd-ring-a: 360deg; }
}

/* Global reduce backstop. Where scroll timelines are unsupported each
   animation lands on its end state immediately: the paragraph reads fully
   dark, the card reads full width. That is the correct fallback. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  .pd-ring:hover::before { animation: none; opacity: 0.5; }
  .pd-reveal span { color: var(--pd-card-ink); }
}

/* ==========================================================================
   Nav (§7: links occupy whole grid cells, so the hover target is the cell,
   not the text). Transparent and floating.
   ========================================================================== */

.pd-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 60;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  align-items: stretch;
  height: 64px;
  background: transparent;
}

/* The bar starts transparent and earns a glass surface once the page has moved.
   design.js still flips its ink independently, so it reads on the light page and
   on every dark card passing beneath it.

   The surface lives on a pseudo-element and fades by OPACITY. Transitioning
   backdrop-filter directly would animate the blur radius, which repaints the
   whole stacking context every frame. The radius here is constant; only the
   layer's opacity moves. translateZ(0) is the iOS Safari fix: backdrop-filter on
   a fixed element flickers during momentum scroll without it. */
.pd-nav[data-stuck="true"] { background: transparent; }

.pd-nav::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  opacity: 0;
  pointer-events: none;
  background: rgba(244, 243, 239, 0.72);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  transform: translateZ(0);
  border-bottom: 1px solid var(--pd-line);
}
.pd-nav--on-dark::before {
  background: rgba(20, 20, 19, 0.55);
  border-bottom-color: var(--pd-band-line);
}
.pd-nav[data-scrolled="true"]::before { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  .pd-nav::before { transition: opacity var(--pd-quick); }
}

.pd-nav a {
  display: flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  /* Sits at 0.78 rather than the muted token: over the hero these were
     reading as background texture instead of navigation. Present, not loud. */
  color: rgba(25, 25, 25, 0.78);
  font-family: var(--pd-mono);
  font-weight: 500;
  font-size: 0.8rem;
  letter-spacing: 0.055em;
  text-transform: uppercase;
  /* resting and hover in the same rule, per §3 */
  transition: background var(--pd-quick), color var(--pd-quick);
}

/* Columns 2, 3, 6 and 7, placed rather than auto-flowed. Left to flow, the
   first link lands in column 1, which is the one column that has to stay
   empty, and it is the page's first element. */
.pd-nav .pd-nav-link:nth-of-type(1) { grid-column: 2; }
.pd-nav .pd-nav-link:nth-of-type(2) { grid-column: 3; }
.pd-nav .pd-nav-link:nth-of-type(3) { grid-column: 6; }
.pd-nav .pd-nav-link:nth-of-type(4) { grid-column: 7; }

/* The centre cell. Two things live in it, stacked in one grid area rather
   than absolutely positioned, so the taller of the two sets the height and
   neither can drift off centre. */
.pd-nav-centre {
  position: relative;
  grid-column: 4 / 6;
  display: grid;
  place-items: center;
}
/* The wordmark and the button share one cell; the menu hangs below it. */
.pd-nav-centre > .pd-nav-brand,
.pd-nav-centre > .pd-nav-swap { grid-area: 1 / 1; }

.pd-nav-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  text-decoration: none;
  color: var(--pd-ink);
}

/* The button the wordmark turns into. Wears what the nav CTA used to wear,
   so the bar keeps one button shape. */
.pd-nav-swap {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 18px;
  /* 40, the dense-desktop floor. It measured 34. */
  min-height: 40px;
  border: 1px solid rgba(25, 25, 25, 0.3);
  /* A pill, like the CTAs. It was square and read as a hard chip in the one
     place on the page that is otherwise all type. */
  border-radius: 999px;
  background: none;
  cursor: pointer;
  color: var(--pd-ink);
  font-family: var(--pd-mono);
  font-weight: 500;
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
.pd-nav--on-dark .pd-nav-swap { border-color: rgba(242, 241, 236, 0.34); color: var(--pd-card-ink); }

/* The swap. data-scrolled is already set by design.js off a 1px sentinel, so
   this is CSS only. Desktop only: below 901 the bar is the wordmark and a
   hamburger, and taking the wordmark away would leave nothing identifying the
   site. The drawer carries the Discord link at that width instead. */
@media (max-width: 900px) {
  /* .pd-nav .pd-nav-swap, not .pd-nav-swap: the base .pd-nav a rule is (0,1,1)
     and beats a bare class whatever the order. Same trap as the old nav CTA. */
  .pd-nav .pd-nav-swap { display: none; }
}
@media (min-width: 901px) {
  .pd-nav-swap { opacity: 0; visibility: hidden; }
  .pd-nav[data-scrolled="true"] .pd-nav-brand { opacity: 0; visibility: hidden; }
  .pd-nav[data-scrolled="true"] .pd-nav-swap { opacity: 1; visibility: visible; }
}

/* Motion is opt-in, so the swap is a hard cut for anyone who has not asked
   for animation, and for any browser that does not understand the query.
   A cross-fade with a little scale and blur, which is what reads as one thing
   changing rather than two things overlapping. Transitions, not keyframes:
   scrolling back over the threshold retargets from wherever it currently is
   instead of restarting. visibility transitions discretely, so it flips at
   the end of the fade out and at the start of the fade in, which is exactly
   when the element should leave and rejoin the tab order. */
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) {
  .pd-nav-brand, .pd-nav-swap {
    transition:
      opacity 200ms cubic-bezier(0.2, 0, 0, 1),
      transform 200ms cubic-bezier(0.2, 0, 0, 1),
      filter 200ms cubic-bezier(0.2, 0, 0, 1),
      visibility 200ms;
  }
  .pd-nav-swap { transform: scale(0.94); filter: blur(4px); }
  .pd-nav[data-scrolled="true"] .pd-nav-swap { transform: none; filter: blur(0); }
  .pd-nav[data-scrolled="true"] .pd-nav-brand { transform: scale(0.94); filter: blur(4px); }
}
.pd-nav-brand span {
  font-family: var(--pd-display);
  font-weight: 300;
  font-size: 1.32rem;
  letter-spacing: 0.01em;
  /* the bar is uppercase mono; the wordmark is not */
  text-transform: none;
}
.pd-nav-brand svg { width: 22px; height: 22px; }

/* On a dark hero the bar sits over BAND, so it flips to card ink. */
.pd-nav--on-dark a {
  color: rgba(242, 241, 236, 0.88);
  /* the hero silk is busy; a hairline of shadow keeps the type crisp on it
     without adding a background to the bar */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.32);
}
.pd-nav--on-dark .pd-nav-brand { color: var(--pd-card-ink); }

/* Mobile: the 8-cell bar collapses to brand + a toggle. The links move into
   a drawer rather than disappearing. */
.pd-nav-toggle {
  display: none;
  grid-column: 3;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  padding-right: 18px;
  background: none;
  border: 0;
  cursor: pointer;
  color: inherit;
}
.pd-nav-toggle span {
  display: block;
  width: 20px;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--pd-quick), opacity var(--pd-quick);
}
.pd-nav-toggle span + span { margin-top: 5px; }
.pd-nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.pd-nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.pd-nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.pd-drawer {
  position: fixed;
  inset: 56px 0 auto 0;
  z-index: 55;
  display: none;
  flex-direction: column;
  background: rgba(244, 243, 239, 0.96);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  backdrop-filter: blur(16px) saturate(140%);
  border-bottom: 1px solid rgba(25, 25, 25, 0.12);
  padding: 8px 0 14px;
}
.pd-drawer[data-open="true"] { display: flex; }
.pd-drawer a {
  padding: 14px 24px;
  text-decoration: none;
  color: var(--pd-ink);
  font-family: var(--pd-mono);
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transition: background var(--pd-quick);
}

@media (max-width: 900px) {
  .pd-nav {
    grid-template-columns: 1fr auto 1fr;
    height: 56px;
  }
  /* The drawer needs an opaque bar to hang off at every scroll position, so on
     mobile the glass is pinned on rather than earned. Same single mechanism as
     desktop — the pseudo — instead of a second background declaration here. */
  .pd-nav::before { opacity: 1; }
  .pd-nav-centre { grid-column: 2; }
  .pd-nav .pd-nav-link { display: none; }
  .pd-nav-toggle { display: flex; }
}
@media (min-width: 901px) {
  .pd-drawer { display: none !important; }
}

/* ==========================================================================
   Shared page chrome.
   The section rules, the bottom CTA and the footer band are injected into
   every page by scripts/migrate-to-v3.py, so their styles belong here in the
   system file. They lived in home.css until 2026-08-26, which meant 100 pages
   rendered the footer with unstyled anchors: default UA blue on a dark band.
   ========================================================================== */
/* --- Section rhythm ------------------------------------------------------
   Every boxed section gets a rule above it (DESIGN.md §10.4). That rule, and
   the crosshairs where it crosses the verticals, are what stop the page
   background reading as empty space between floating slabs. It is also why
   there is no fade anywhere: a soft gradient against a flat page is a smear
   (§9), and the discipline everywhere else is a hairline and a hard edge.
   ------------------------------------------------------------------------- */

.section-rule {
  position: relative;
  z-index: 1;
  margin-top: 96px;
  margin-bottom: 96px;
}

/* The section rhythm collapses on a phone. This lives here, not in home.css:
   .section-rule and .endcta are on every page, and home.css only loads on the
   homepage, so interior pages kept desktop spacing at mobile widths. */
@media (max-width: 900px) {
  .section-rule { margin-top: 64px; margin-bottom: 64px; }
  .endcta { margin-top: 64px; }
}

/* --- Bottom CTA (a Ruled split) ------------------------------------------ */

.endcta {
  position: relative;
  margin-top: 96px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 0;
}
/* Opaque, and carrying two of the block's three verticals: its own left edge
   and the seam it shares with the media. Opaque because the page grid was
   running straight through the display serif of the heading, and because the
   rules above and below are half marks only if something hides the grid
   between them. */
.endcta-copy {
  position: relative;
  z-index: 1;
  /* stretch, not the grid's align-items: center. Centred, the copy is however
     tall its content is and the row is however tall the media is, and at most
     widths those differ by a rounding pixel: measured at 1367px the row was
     407.016 and the copy 406.008, so the copy floated with a 0.5px gap at each
     end and the seam it draws stopped half a pixel short of both rules. The
     two seam crosshairs mark exactly those junctions, so the line has to reach
     them. Stretching costs nothing: the copy is the taller of the two at every
     width that matters, which is why centring was a no-op to begin with. */
  align-self: stretch;
  padding: clamp(44px, 6vw, 88px) clamp(24px, 4vw, 56px) clamp(44px, 6vw, 88px) clamp(24px, 4vw, 56px);
  background-color: var(--pd-bg);
  background-image: var(--pd-dash-v), var(--pd-dash-v);
  background-repeat: no-repeat;
  background-size: 1px 100%, 1px 100%;
  background-position: left top, right top;
}
.endcta-copy h2 { margin: 14px 0 26px; }
.endcta-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.endcta-fine { margin-top: 22px; max-width: 36ch; font-size: 0.94rem; }

/* After the base rule above, not before it: at equal specificity source order
   decides, and up in the section-rhythm block this spent its life losing to
   .endcta's own margin-top: 96px. */
@media (max-width: 900px) {
  .endcta { margin-top: 64px; }
}

/* The seam is the only real junction inside the box: the outer corners are Ls,
   and the rules above and below already mark where they cross the gridlines.

   Half marks, both of them. The page grid has no line at 50% (§1 deletes it),
   so nothing crosses the rule here from the other side: at the top seam three
   lines meet, the rule going left, the rule going right, and the seam going
   down. Same at the bottom, pointing up. The arms are set in the markup.

   The three 0.5px terms are the same correction as .pd-x: the seam is drawn by
   endcta-copy's `background-position: right top`, so it occupies the pixel
   ending at the copy's right edge and its centre is 0.5px inside, and the two
   rules occupy [copyTop-1, copyTop] and [copyBottom, copyBottom+1]. Measured
   before the fix: seam arm at [639.5, 640.5] against a line at [639, 640]. */
.endcta-seam { left: 100%; margin-left: calc(var(--pd-dot-box) / -2 - 0.5px); }
.endcta-seam--t { top: -0.5px; }
.endcta-seam--b { top: calc(100% + 0.5px); }

.endcta-media {
  position: relative;
  align-self: stretch;
  min-height: 300px;
  overflow: hidden;
  display: grid;
  /* start, not centre. Centred, the device was cut at BOTH ends once it
     outgrew the box: a flat slice across the top corner of a tilted phone
     reads as a rendering fault. Pinned at the top it runs out of the bottom
     only, which is the original's "cut flush by the container's bottom edge". */
  align-items: start;
  padding-top: 32px;
  /* off-centre, so the device sits beside the figure in the video rather
     than on top of him */
  justify-items: start;
  padding-left: clamp(20px, 4vw, 56px);
  background-color: #101010;
  /* the block's third vertical, its right edge */
  background-image: var(--pd-dash-v);
  background-repeat: no-repeat;
  background-size: 1px 100%;
  background-position: right top;
}
.endcta-media .pd-device {
  position: relative;
  z-index: 2;
  --w: clamp(150px, 13vw, 196px);
  transform: perspective(1200px) rotateY(-12deg) rotateZ(5deg);
}

@media (max-width: 900px) {
  /* Stacked: the seam and its two crosshairs have nothing to sit on. */
  .endcta { grid-template-columns: 1fr; }
  .endcta-copy { padding: 44px 24px; background-image: none; }
  .endcta-seam { display: none; }
  .endcta-media { min-height: 260px; justify-items: center; padding-left: 0; background-image: none; }
}

/* --- Footer band --------------------------------------------------------- */

/* Image-backed band, not a gradient. It reuses the hero's silk so the page
   opens and closes on the same surface, and the radial glow that used to sit
   here is gone: it was a soft fade under a comment saying there wasn't one. */
.foot {
  position: relative;
  isolation: isolate;
  /* Its own radius, deliberately not --pd-r-card. This is one big band meeting
     the page, not a card in the stack. */
  border-radius: 64px 64px 0 0;
  overflow: hidden;
  overflow: clip;
  background: #0d0d0c;
  color: var(--pd-card-ink);
  padding: clamp(44px, 5vw, 72px) clamp(24px, 4vw, 60px) 0;
}
/* Blurred well past legibility and scaled up, so the blur's soft edges fall
   outside the band rather than showing as a light rim inside the radius. */
.foot::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: url("footer-bg.webp") center / cover no-repeat;
  filter: blur(10px);
  transform: scale(1.06);
}
.foot::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: rgba(6, 10, 8, 0.42);
}
.foot-grid {
  display: grid;
  grid-template-columns: 1.6fr repeat(3, 1fr);
  gap: 40px;
}
.foot-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }
.foot-brand span { font-family: var(--pd-display); font-weight: 300; font-size: 1.45rem; }
.foot-brand img { height: 26px; width: auto; }
.foot p { color: rgba(242, 241, 236, 0.62); max-width: 34ch; font-size: 0.94rem; }
.foot h4 { font-family: var(--pd-body); font-weight: 500; font-size: 0.9375rem; line-height: 1.5; margin: 0 0 16px; }
.foot ul { list-style: none; margin: 0; padding: 0; }
.foot li { margin-bottom: 2px; }
/* WCAG 2.2 AA (2.5.8) wants a 24x24 target. These rendered 18px tall, so the
   padding is doing accessibility work, not decoration. */
.foot li a {
  display: inline-block;
  padding: 5px 0;
  min-height: 24px;
  color: rgba(242, 241, 236, 0.66);
  text-decoration: none;
  font-size: 0.94rem;
  transition: color var(--pd-quick);
}
.foot-actions { display: flex; gap: 12px; margin: 24px 0 18px; flex-wrap: wrap; }
.foot-legal { margin-top: 34px; color: rgba(242, 241, 236, 0.55); font-size: 0.75rem; line-height: 1.6; max-width: 78ch; }

/* Designer credit, sitting on the copyright line. Same 24px target treatment as
   .foot li a above, but the negative margins cancel the padding so the "·" keeps
   its optical spacing and the baseline does not move. It has no background of its
   own, so the radius exists purely to give :focus-visible something to draw around. */
/* Prose, so the body face, not the mono. Mono means "this is a measurement"
   (§4) and a copyright line is not one; at 12px it was also wrapping onto two
   lines on a phone. */
.foot-copyline {
  font-family: var(--pd-body);
  font-size: 0.82rem;
  /* 0.6: at 0.45 this measured under 4.5:1 on the footer band. */
  color: rgba(242, 241, 236, 0.6);
}
.foot-sep { margin: 0 0.5ch; opacity: 0.55; }
.foot-credit {
  display: inline-block;
  padding: 4px 2px;
  margin: -4px -2px;
  min-height: 24px;
  border-radius: 2px;
  /* 0.6, not 0.42. On the footer band 0.42 measured 3.7:1 at 15.04px. */
  color: rgba(242, 241, 236, 0.6);
  text-decoration: none;
  transition: color var(--pd-quick);
}
@media (hover: hover) {
  .foot-credit:hover { color: var(--pd-card-ink); }
}
.foot-credit:focus-visible {
  transition: none;
  outline: none;
  color: var(--pd-card-ink);
  box-shadow: 0 0 0 2px var(--pd-band), 0 0 0 4px var(--pd-accent);
}

/* Oversized wordmark cropped by the band's bottom edge. The crop is the
   point: the glyph is taller than the box it sits in, so the band cuts it
   rather than containing it. The blur is on the span so the clip lands on
   an already-blurred glyph and the cut stays hard. */
.foot-wordmark {
  position: relative;
  /* Full bleed inside the band. It was sitting in the footer's padded content
     box, so at 375 a 104px glyph about 330px wide had only 303px to live in
     and the clip ate both ends of the word. It needs the band's whole width,
     which is what the original gives it. */
  margin: clamp(64px, 9vw, 160px) calc(clamp(24px, 4vw, 60px) * -1) 0;
  height: clamp(56px, 13.4vw, 190px);
  overflow: hidden;
  overflow: clip;
  user-select: none;
  pointer-events: none;
}
.foot-wordmark span {
  position: absolute;
  left: 50%;
  top: 0;
  transform: translateX(-50%);
  display: block;
  font-family: var(--pd-display);
  font-weight: 200;
  /* The floor is min(), not a flat 6.5rem. "PeptideAI" in this face is about
     4.17x its own type size wide, so a 104px floor is a 434px word, and below
     a 594px viewport that is wider than the band and the clip ate both ends
     of it. 22vw keeps the word at ~92% of the width whatever happens. */
  font-size: clamp(min(6.5rem, 22vw), 17.5vw, 15.5rem);
  line-height: 1;
  color: rgba(242, 241, 236, 0.22);
  filter: blur(1.6px);
  white-space: nowrap;
}

@media (max-width: 1050px) {
  .foot-grid { grid-template-columns: 1fr 1fr; gap: 32px; }
}
@media (max-width: 640px) {
  .foot-grid { grid-template-columns: 1fr; gap: 28px; }
}

/* --- Link colour, as a floor ---------------------------------------------
   Nothing on this site is ever browser-default blue. Any anchor that no other
   rule claims inherits its surrounding colour instead of #0000EE. Rules that
   want a real link treatment (prose, legal, article body) still override
   this; this only catches what would otherwise fall through. */

a { color: inherit; }
a:not([class]) { text-decoration-color: rgba(25, 25, 25, 0.32); text-underline-offset: 3px; }
.pd-band a:not([class]),
.foot a:not([class]),
.pd-card a:not([class]) { text-decoration-color: rgba(242, 241, 236, 0.34); }

/* --- Book a call, in the bar -----------------------------------------------
   Occupies one grid cell like every other nav item (§7), so the geometry is
   unchanged; it just reads as a control rather than a link. Not the accent:
   that list is closed at seven and the hero CTA already holds placement 1. */

/* In the drawer it is just the last item, but marked. */
/* The drawer is now the only place the app is offered as a button, so it
   reads as the primary action rather than as one more list item. */
.pd-drawer .pd-drawer-cta {
  margin: 14px 24px 4px;
  padding: 15px 18px;
  background: var(--pd-band);
  border: 1px solid var(--pd-band);
  border-radius: 0;
  color: var(--pd-card-ink);
  text-align: center;
  transition: background var(--pd-quick);
}
.pd-drawer .pd-drawer-cta:hover { background: #2a2a27; }

/* Some source lists use the bare URL as the link text. A long unbroken URL
   cannot wrap, so on a narrow viewport it pushes the document wider than the
   screen: 15 reconstitution pages scrolled sideways at 390px because of one
   PMC link. Let long strings break rather than overflow. */
a, li, p, td, dd { overflow-wrap: break-word; }

/* ==========================================================================
   Hover states, gated.
   A device with no pointer fires hover on tap and leaves it stuck there, so
   a card holds its lift and a button holds its ring after the finger is
   gone. Every hover rule in this file lives in here. They sit at the end,
   after the resting states they override.
   ========================================================================== */
@media (hover: hover) {
  .crumb a:hover, .breadcrumbs a:hover { color: var(--pd-ink); }
  .pd-feature:hover {
    background: rgba(242, 241, 236, 0.07);
    border-color: rgba(242, 241, 236, 0.18);
  }
  .pd-cta:hover  { background: #4ff0bd; }
  .pd-cta--ghost:hover { background: rgba(242, 241, 236, 0.22); }
  .pd-cta--ink:hover { background: #262624; }
  .pd-cta--card:hover { background: #fff; }
  .pd-cta--soft:hover { background: rgba(25, 25, 25, 0.12); }
  /* infinite, not one 2.6s pass. A single pass meant the ring stopped turning
     and sat at 360deg for as long as the pointer stayed on the button. */
  .pd-ring:hover::before {
    opacity: 1;
    animation: pd-spin 3.2s linear infinite;
  }
  .pd-nav a:hover {
    background: rgba(25, 25, 25, 0.06);
    color: var(--pd-ink);
  }
  .pd-nav-centre a:hover { background: transparent; }
  .pd-nav .pd-nav-menu a:hover { background: rgba(25, 25, 25, 0.06); }
  .pd-nav--on-dark .pd-nav-menu a:hover { background: rgba(242, 241, 236, 0.10); }
  .pd-nav a.pd-nav-swap:hover {
    background: var(--pd-band);
    border-color: var(--pd-band);
    color: var(--pd-card-ink);
  }
  .pd-nav--on-dark a.pd-nav-swap:hover {
    background: var(--pd-card-ink);
    border-color: var(--pd-card-ink);
    color: var(--pd-band);
  }
  .pd-nav--on-dark a:hover { background: rgba(242, 241, 236, 0.10); color: var(--pd-card-ink); }
  .pd-drawer a:hover { background: rgba(25, 25, 25, 0.05); }
  .foot li a:hover { color: var(--pd-card-ink); }
}

/* ==========================================================================
   Phone pass (2026-09-01). See the matching block at the end of pages.css.
   ========================================================================== */

/* --- The footer watermark fits the phone ---------------------------------
   The crop in this wordmark is meant to be horizontal only: the band's bottom
   edge cuts the descenders, which is the effect. The clamp floor of 6.5rem is
   104px, and "PeptideAI" at 104px measures 434px, so on a 375px screen the
   band cut it on the left and right as well and it read "eptideA". A sliced
   first letter is not a crop, it is a mistake. 5.25rem lands the full word at
   351px, inside the narrowest phone with its padding. Above ~360px the vw
   term takes over again and nothing changes.
   ------------------------------------------------------------------------- */
.foot-wordmark span { font-size: clamp(5.25rem, 17.5vw, 15.5rem); }

/* --- Footer links, thumb-sized ------------------------------------------- */
@media (pointer: coarse) {
  .foot li a { padding: 10px 0; min-height: 44px; }
  .foot-credit { padding: 10px 0; min-height: 44px; margin: -10px -2px; }
}

/* ==========================================================================
   §9  The updates modal
   Site-wide email capture. The markup is built in design.js rather than
   pasted into 114 files: the nav and footer are duplicated that way already
   and every edit to them has to be scripted. One dialog, one place.

   The submit button is placement 1 of the accent list, not an eighth. §3
   defines placement 1 as a role, "the primary CTA wherever it sits on a dark
   surface", and inside this panel that is what it is.
   ========================================================================== */

.pd-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(8, 9, 8, 0.62);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
}
.pd-modal[hidden] { display: none; }

/* The panel is a programmatic focus target, not a control, so it must not
   draw the focus ring. That ring is a box-shadow here, not an outline, and
   the panel already has a box-shadow, so it has to be restated rather than
   set to none. */
.pd-modal-panel:focus,
.pd-modal-panel:focus-visible {
  outline: none;
  box-shadow: 0 40px 90px rgba(0, 0, 0, 0.55);
}

.pd-modal-panel {
  position: relative;
  width: min(460px, 100%);
  max-height: calc(100vh - 48px);
  overflow-y: auto;
  padding: 40px 36px 34px;
  background: var(--pd-band);
  color: var(--pd-card-ink);
  border: 1px solid rgba(242, 241, 236, 0.14);
  border-radius: 0;                        /* square, like everything else */
  box-shadow: 0 40px 90px rgba(0, 0, 0, 0.55);
}

@media (prefers-reduced-motion: no-preference) {
  .pd-modal { animation: pd-modal-fade 200ms ease both; }
  .pd-modal-panel { animation: pd-modal-rise 320ms cubic-bezier(0.2, 0, 0, 1) both; }
}
@keyframes pd-modal-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes pd-modal-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

.pd-modal-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: rgba(242, 241, 236, 0.52);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: color var(--pd-quick);
}
.pd-modal-close:hover { color: var(--pd-card-ink); }

.pd-modal-eyebrow {
  display: block;
  margin-bottom: 18px;
  color: rgba(242, 241, 236, 0.46);
  font-size: 0.7rem;
}
.pd-modal-panel h2 {
  margin: 0 0 14px;
  font-size: 1.72rem;
  line-height: 1.14;
  font-weight: 300;
}
.pd-modal-panel p {
  margin: 0 0 26px;
  color: rgba(242, 241, 236, 0.68);
  font-size: 0.95rem;
  line-height: 1.6;
}

.pd-modal-form { display: flex; flex-direction: column; gap: 12px; }

.pd-modal-form input[type="email"] {
  width: 100%;
  padding: 14px 16px;
  background: rgba(242, 241, 236, 0.05);
  border: 1px solid rgba(242, 241, 236, 0.18);
  border-radius: 0;
  color: var(--pd-card-ink);
  font-family: inherit;
  font-size: 0.95rem;
}
.pd-modal-form input[type="email"]::placeholder { color: rgba(242, 241, 236, 0.34); }
.pd-modal-form input[type="email"]:focus {
  outline: none;
  border-color: rgba(242, 241, 236, 0.44);
  background: rgba(242, 241, 236, 0.08);
}

.pd-modal-form .pd-cta {
  justify-content: center;
  width: 100%;
  border: 0;
  font-family: inherit;
  font-size: 0.95rem;
  cursor: pointer;
}
.pd-modal-form .pd-cta[disabled] { opacity: 0.6; cursor: default; }

/* Off screen, not display:none. A hidden honeypot that screen readers skip is
   still submitted by the bots that fill every field in the DOM. */
.pd-modal-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.pd-modal-err {
  margin: 14px 0 0;
  color: #ff8a7a;
  font-size: 0.84rem;
}
.pd-modal-err:empty { display: none; }

@media (max-width: 480px) {
  .pd-modal { padding: 16px; }
  .pd-modal-panel { padding: 32px 22px 26px; }
  .pd-modal-panel h2 { font-size: 1.5rem; }
}
