/* ============================================================================
   Phenome — shared design system

   Every value here comes from the brand's own sources. Nothing is invented:

     tokens.json          the measured token set (docs/brief/tokens.json)
     prototype/system.css the same system expressed as CSS — README calls it
                          "the design system as CSS variables", and it is the
                          authority on how the breakpoint cascade resolves
     BRAND-CONTEXT.md §7  the formal identity

   Type is Outfit 400/600 only. tokens.json marks the system display family as
   unusable in production. The earlier build of this site was shipping it.

   SETTLED 2026-08-17 — black is a site colour, not only the footer's ground. Bugra:
   "siyah renk kullanabiliriz websitemizde gayet iyi". The dark section ground stays
   #000000 and the wireframe's #0A1E57 band is a recorded deviation, not a defect —
   see design_language.json › black-is-a-site-colour. The blues below are UNCHANGED
   and still open; that question is about the accent, not about this ground.

   OPEN — Appendix B #1, site colour direction. Three blues are in play:
     #0071E3 / #0066CC   tokens.json color.accent — the measured WEB scale, used here
     #203A85             Phenome Blue, the brand guideline's primary, and the colour
                         inside assets/brand/phenome-logo.svg
     #0a1e57             what this site used before — from a design mock, in neither
                         the tokens nor the guideline. Removed.
   BRAND-CONTEXT §7 records that the guideline "defines a separate web colour scale …
   and states explicitly that web is its own case", which is why the tokens' accent is
   used rather than the guideline's primary. The two sit side by side wherever the logo
   appears, so they must be reviewed together once the direction is settled.
   ========================================================================== */

@import url('/site/assets/fonts/outfit.css');

:root {
  /* ---- colour · tokens.json color.neutral -------------------------------- */
  --ph-white:        #FFFFFF;   /* page and section ground */
  --ph-surface-alt:  #FAFAFC;   /* alternate surface */
  --ph-surface:      #F5F5F7;   /* section ground */
  --ph-pill:         #E8E8ED;   /* tab / pill ground */
  --ph-line:         #D2D2D7;   /* rule · divider · arrow ground */
  --ph-text-2:       #6E6E73;   /* secondary text */
  --ph-text-1:       #1D1D1F;   /* headings and body */
  --ph-black:        #000000;   /* FOOTER GROUND ONLY — tokens: "YALNIZ footer zemini" */
  --ph-tertiary:     #86868B;

  /* tokens.json color.$gapRule — do not fill this in:
     "Five tones are packed between 83–100% lightness, then a 39-POINT GAP, then three
      dark tones. THERE IS NO MID GREY. This is the technical reason the system reads
      'plain and expensive' — add mid greys and it disappears."
     Nothing may introduce a grey between L44 (#6E6E73) and L83 (#D2D2D7), including
     via opacity or filter: brightness(), which compose into that band. */

  --ph-accent:       #0071E3;   /* color.accent.buttonFill */
  --ph-accent-link:  #0066CC;   /* color.accent.link */
  --ph-focus:        #0071E3;   /* color.semantic.$focus */

  /* A button's hover and press fills. These are NOT the link colour, and the
     difference is the whole reason they exist: --ph-accent-link is DARKER than the
     resting fill, so using it as a hover made every button on the site recede when
     you pointed at it — the opposite of what a hover is for. Bugra, 2026-08-20: a
     filled button should take a very small tone change on hover, so you feel it.
     Resting fill #0071E3, hover #0076DF, press #006EDB.
     Hover is +5 green / -4 blue — a small tone change — and press goes one step
     under the resting fill. Do not swap these for the link colour again. */
  --ph-accent-hover:  #0076DF;  /* button hover fill */
  --ph-accent-active: #006EDB;  /* button press fill */

  /* The accent inside a dark section — links, eyebrows, step numbers, tick marks,
     bullet marks, the figure's rule. #0066CC is 2.4:1 on black and unreadable, so the
     dark ground has always needed its own lighter blue; this is the value those eight
     rules were written against, named rather than repeated eight times.
     It is declared here because a var() that resolves to nothing does not fall back —
     the property is simply dropped and the element inherits whatever is above it. That
     is invisible to every gate on this project: none of the eight reads colour. */
  --ph-on-brand:     #6fa8ff;

  /* ---- semantic · the client's design system defines these and the site had never
     used one, so the only place that needed a warning colour invented three oranges of
     its own instead. Values are that file's, not new ones. Each has a ground and an ink
     because colour never carries information alone — §6. */
  --ph-warn:        #B64400;   /* warning text on its own ground */
  --ph-warn-bg:     #FFF9F4;
  --ph-warn-ink:    #F56300;
  --ph-success:     #008009;
  --ph-success-bg:  #F5FFF6;
  --ph-success-ink: #03A10E;
  --ph-error:       #E30000;
  --ph-error-bg:    #FFF2F4;


  /* ---- type · Outfit only, weights 400 and 600 ---------------------------- */
  --ph-font: 'Outfit', system-ui, -apple-system, sans-serif;
  --ph-w-400: 400;
  --ph-w-600: 600;

  /* size / line-height ladder. tokens type.lineHeightRule: line-height is
     size + 4 up to 21, size + 8 above. */

  /* THE FLOOR IS 15px, 2026-09-17, by request, and the two rungs below it are
     RAISED TO MEET IT rather than deleted.

     The complaint was site wide type "the size of ants", and measuring it found
     the cause was not one page: it was these two tokens, which 76 rules across
     seven stylesheets read. The main navigation was 12px. So were the store
     menu's eyebrows and its "Not sure where to start" links. The footer, the
     promo bar, the ISO line, every product card's name and price, every catalogue
     card's description and its Buy pill, and the homepage tiles' own buttons
     were all 14. Raising the two tokens fixes all of it in one place; raising
     the 76 rules would have left the next one to be written wrong again.

     The NAMES are deliberately not changed, because they are the ladder's rungs
     and every one of those 76 call sites names its rung. Read them as "the
     smallest rung" and "the rung below body" — the number in the name is what
     the rung used to measure, not what it measures now.

     15 and 16, not 17 and 17: the point of a rung below body is that it is
     below body. 17 everywhere would flatten a label, a price and a paragraph
     into one voice, and the smallest of the three would stop reading as the
     smallest. The line heights follow the same size + 4 and size + 6 rule the
     rungs above them use. */
  --ph-fs-12: 15px;  --ph-lh-12: 20px;
  --ph-fs-14: 16px;  --ph-lh-14: 22px;
  --ph-fs-17: 17px;  --ph-lh-17: 21px;  --ph-lh-17-loose: 25px;
  --ph-fs-19: 19px;  --ph-lh-19: 23px;  --ph-lh-19-loose: 27px;
  --ph-fs-21: 21px;  --ph-lh-21: 25px;  --ph-lh-21-loose: 29px;
  --ph-fs-24: 24px;  --ph-lh-24: 32px;
  --ph-fs-28: 28px;  --ph-lh-28: 36px;
  --ph-fs-32: 32px;  --ph-lh-32: 40px;
  --ph-fs-40: 40px;  --ph-lh-40: 48px;
  --ph-fs-48: 48px;  --ph-lh-48: 56px;
  --ph-fs-64: 64px;  --ph-lh-64: 72px;
  --ph-fs-80: 80px;  --ph-lh-80: 88px;

  /* tracking · tokens type.tracking.outfit */
  --ph-tr-80-600: -1.664px;
  --ph-tr-64-600: -1.014px;
  --ph-tr-48-600: -0.528px;
  --ph-tr-40-600: -0.32px;
  --ph-tr-32-600: -0.128px;
  --ph-tr-28-600: -0.028px;
  --ph-tr-24-600:  0.072px;
  --ph-tr-21-600:  0.126px;
  --ph-tr-12-600: -0.202px;
  /* Uppercase micro-type is the one place tracking goes POSITIVE. Capitals have no
     ascenders or descenders to separate them, so 12px caps at the ladder's own
     -0.202px jam together; the rule is +5-8% of size. Measured 2026-08-13: 439 of the
     441 uppercase labels on the site were tracked -1.68%, while the same label inside
     the mega-menu was already +8% (shared.css:1330 hand-types .08em) — the two were
     never the same label. 0.96px is 8% of 12. */
  --ph-tr-12-600-caps: 0.96px;
  /* The global nav only, at 1440: 12px links, letter-spacing -0.12px, 8px of
     horizontal padding, 44px tall, 46px between labels, colour rgba(0,0,0,.8).
     ⚠️ An earlier spec said 12/16 and #6E6E73 with -0.40 tracking. #6E6E73 in a
     translucent bar dropped the label to 3.16:1 over a dark section, while
     rgba(0,0,0,.8) clears the rule with the bar still translucent.
     Our ladder carries no 12/400 tracking pair, and writing --ph-tr-12-400 would have
     been a var that resolves to nothing — which has silently deleted its own declaration
     three times on this project. Named for its scope so it cannot be mistaken for a rung
     of the type ladder. */
  --ph-tr-nav: -0.12px;
  --ph-tr-21-400:  0.252px;
  --ph-tr-19-400:  0.228px;
  --ph-tr-17-400:  0.204px;

  /* ---- responsive type · the h1/h2 step at each band comes from system.css,
     which puts h1 at 64 between 1024 and 1439. The tokens list 64 as a measured
     size without assigning it to a design screen; system.css is where it lands. */
  --ph-h1-size: var(--ph-fs-80); --ph-h1-lh: var(--ph-lh-80); --ph-h1-tr: var(--ph-tr-80-600);
  --ph-h2-size: var(--ph-fs-48); --ph-h2-lh: var(--ph-lh-48); --ph-h2-tr: var(--ph-tr-48-600);
  --ph-h3-size: var(--ph-fs-28); --ph-h3-lh: var(--ph-lh-28); --ph-h3-tr: var(--ph-tr-28-600);
  --ph-lead-size: var(--ph-fs-21); --ph-lead-lh: var(--ph-lh-21-loose);

  /* ---- layout · system.css "kenar boşluğu merdiveni" --------------------- */
  --ph-content-max: 1260px;
  --ph-read-max:     980px;

  /* ---- the MEASURE, in characters ----------------------------------------
     --ph-read-max is a CONTAINER width and it is right as one. It is not a reading
     measure: 980px of 17px Outfit is 119 characters per line, and text is read between
     45 and 75. That is the same box at 28px, which is what 980 was sized for.
     Measured at 1920 before this existed: 11 paragraphs over 90 characters, the longest
     185 — every FAQ answer on the site (`.acc-body`, 980px at 17px) and every 14px
     paragraph in a wide box. At 390 the same pages were clean, so it was invisible on a
     phone and worst on the screen the client actually looks at.
     `ch` because it is the only unit that keeps the measure right at every font size AND
     every viewport at once — a px cap has to be re-tuned for both.

     CALIBRATED, not guessed: 1ch is the width of the "0" GLYPH, not of an average
     character, and in Outfit zero is about 35% wider than the mean lowercase letter.
     Measured: a 68ch cap produced 92 characters per line, so the ratio is 1.353 and the
     cap is set from the character count wanted rather than from it directly. */
  --ph-measure:      52ch;   /* ≈70 characters */
  --ph-measure-lead: 46ch;   /* ≈62 — larger type reads better shorter */
  --ph-gutter:        20px;   /* grid column gap — NOT the page edge */
  --ph-margin:        90px;

  /* ---- section rhythm · system.css ---------------------------------------- */
  --ph-sec-top:     144px;
  --ph-sec-bottom:  116px;
  --ph-hero-bottom:  52px;

  /* ---- radius · tokens radius.scale --------------------------------------- */
  --ph-r5: 5px; --ph-r6: 6px; --ph-r8: 8px; --ph-r12: 12px; --ph-r14: 14px;
  --ph-r18: 18px; --ph-r21: 21px; --ph-r27: 27px; --ph-r36: 36px; --ph-r56: 56px;

  /* ---- control geometry · tokens control ---------------------------------- */
  --ph-btn-h: 44px;
  --ph-btn-px: 22px;
  --ph-btn-sm-h: 38px;
  --ph-btn-sm-px: 16px;
  --ph-nav-h: 44px;

  /* ---- a11y · tokens a11y ------------------------------------------------- */
  --ph-focus-w: 3px;
  --ph-focus-offset: 3px;
  --ph-touch-min: 44px;

  /* ---- motion · tokens motion --------------------------------------------- */
  --ph-dur-fast: 160ms;
  --ph-dur-base: 240ms;
  /* Longer than any interaction token on purpose: this one is not feedback to a
     click, it is a band settling into place while the reader is still moving. At
     240ms it snaps and reads as a glitch; at 620 it reads as weight. */
  --ph-dur-reveal: 620ms;
  --ph-ease: cubic-bezier(.4, 0, .2, 1);
}

/* The cascade below is system.css's, verbatim in structure.
   ≥1440 uses the :root values above. */
@media (max-width: 1439px) {
  :root {
    --ph-margin: 64px;
    --ph-sec-top: 120px; --ph-sec-bottom: 92px; --ph-hero-bottom: 36px;
    --ph-h1-size: var(--ph-fs-64); --ph-h1-lh: var(--ph-lh-64); --ph-h1-tr: var(--ph-tr-64-600);
    --ph-h2-size: var(--ph-fs-40); --ph-h2-lh: var(--ph-lh-40); --ph-h2-tr: var(--ph-tr-40-600);
  }
}
@media (max-width: 1023px) {
  :root {
    --ph-margin: 48px;
    --ph-h1-size: var(--ph-fs-48); --ph-h1-lh: var(--ph-lh-48); --ph-h1-tr: var(--ph-tr-48-600);
    --ph-h2-size: var(--ph-fs-32); --ph-h2-lh: var(--ph-lh-32); --ph-h2-tr: var(--ph-tr-32-600);
    --ph-h3-size: var(--ph-fs-24); --ph-h3-lh: var(--ph-lh-24); --ph-h3-tr: var(--ph-tr-24-600);
    --ph-lead-size: var(--ph-fs-19); --ph-lead-lh: var(--ph-lh-19-loose);
  }
}
@media (max-width: 799px) {
  :root {
    --ph-margin: 24px;
    --ph-sec-top: 140px; --ph-sec-bottom: 112px; --ph-hero-bottom: 28px;
  }
}

/* legacy aliases — the 58 built pages reference these names */
:root {
  --navy: var(--ph-accent); --navy-hover: var(--ph-accent-link);
  --blue: var(--ph-accent); --peri: var(--ph-tertiary);
  --violet: #4b3f9e; --rose: #8e3b56; --teal: #2b6d6a; --amber: #a8641f;  /* test-kit coding only */
  --ink: var(--ph-text-1); --gray: var(--ph-text-2); --gray-2: var(--ph-tertiary);
  --bg: var(--ph-surface); --border: var(--ph-line); --hair: #E5E5EA;
  --accent: var(--ph-accent);
}

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0; background: var(--ph-white); color: var(--ph-text-1);
  font-family: var(--ph-font);
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
  letter-spacing: var(--ph-tr-17-400);
  -webkit-font-smoothing: antialiased; text-wrap: pretty;
}
a { color: var(--ph-accent-link); text-decoration: none; }
/* 🔴 THE COLOUR CHANGE THAT USED TO BE HERE ERASED BUTTON LABELS, and it did it in
   silence for as long as the tile homepage existed. This rule is (0,1,1); a control
   class that sets its own colour is (0,1,0), so on hover THIS won — and the colour it
   set was --ph-accent, which is the exact fill of every blue pill on the site. The
   result, measured in the browser 2026-08-20 on /store/supplements/ and on the
   homepage: `.atile-link` went from white text on #0071E3 to #0071E3 text on #0071E3.
   Contrast 1:1. The label did not fade — it vanished, and the pill kept its underline
   floating in the middle of nothing. Bugra caught it in a screenshot; no gate on this
   project reads colour, so nothing else could have.
   The rule is now exactly one declaration — `a:hover { text-decoration: underline; }` —
   and the link colour (#0066CC) does not move on hover. Every control that fills or
   inverts still has to say `text-decoration: none` for itself; underline is the
   default for a link and a button is not a link. */
a:hover { text-decoration: underline; }
/* Buttons, pills, cards and tiles are not underlined on hover. */
a[class*="btn"]:hover, a[class*="pill"]:hover, a[class*="cta"]:hover,
a[class*="card"]:hover, a[class*="tile"]:hover { text-decoration: none; }
/* height:auto is load-bearing, not tidiness. Every <img> carries width/height
   attributes for CLS reservation, and those map to CSS at hint specificity — so an
   image sized `width:100%` keeps the attribute's pixel height and stretches. */
img { max-width: 100%; height: auto; display: block; }
button, input, select, textarea { font-family: var(--ph-font); }

/* tokens a11y.focusRing — 3px ring, 3px offset. outline:none is forbidden. */
a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible,
textarea:focus-visible, details:focus-visible {
  outline: var(--ph-focus-w) solid var(--ph-focus);
  outline-offset: var(--ph-focus-offset);
}

/* ---- Nav ---- */
.ph-nav {
  position: sticky; top: 0; z-index: 100; height: var(--ph-nav-h);
  display: flex; align-items: center; justify-content: center;
  /* NO border — an invented one was recorded as a mistake.
     Separation comes from the tone of the page: the hero's top is #F5F5F7 and the bar
     is white, which measures 10/255 on every page.

     80% white with the blur. It was briefly opaque here: with the label at #6E6E73
     a dark section scrolling under a translucent bar dropped it to 3.16:1 against a
     rule of 4.5, and no opacity fixed it (86% → 3.66, 90% → 4.06, 94% → 4.45). The
     cause was the colour, not the glass: with the link at rgba(0,0,0,.8) the same
     translucent bar measures well clear of the rule. */
  background: rgba(255,255,255,.8); backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
}
/* The placeholder shared.js replaces has to hold the nav's space before the nav
   exists. It reserved 0px for a 44/48px bar, so every page dropped by that much the
   moment the script landed — measured on 120 of 120 pages, and on /quiz/ it was a
   real CLS rather than a paint-time shift. It is a bare <div data-ph-nav> in <body>
   before <main>, nothing else selects it, and shared.js swaps it by outerHTML, so
   this rule cannot survive injection and cannot double-count. */
[data-ph-nav] { display: block; height: var(--ph-nav-h); }
/* The SAME container formula as .wrap / .wrap-wide below — deliberately, not by
   coincidence. This was `width: min(1180px, 92vw)`, a width that appears in no token
   file, and it put the wordmark off the headline's left line by +40px at 2560/1920/1440,
   −13 at 1280, −23 at 1024, −16 at 800, +8 at 799, −8 at 390 and −11 at 320 — the sign
   flipping as the window resized, which is what made it read as two designs stacked.
   The footer has always been 0px off at every width because it uses .wrap-wide; this
   rule is that same arithmetic, so nav and page cannot drift apart again. Measured after:
   0.0px at all of 2560/1920/1440/1280/1024/935/931/901/900/800/799/390/320. */
.ph-nav-inner { width: 100%; max-width: calc(var(--ph-content-max) + var(--ph-margin) * 2);
                margin-inline: auto; padding-inline: var(--ph-margin);
                display: flex; align-items: center; justify-content: space-between; }
.ph-nav a { text-decoration: none; }
.ph-brand { display: flex; align-items: center; color: var(--ph-text-1); }
.ph-brand img { height: 18px; width: auto; }
/* At 1440: 46px between labels, the same for all twelve pairs, built as an 8px link
   padding either side plus a 30px gap. We take 8 + 32 = 48, two pixels wider, because
   30 is not on tokens' spacing scale and 32 is — and because Outfit sets wide at 12px,
   so the row wants the extra rather than less. Two pixels at this size is invisible;
   an off-scale number is not. */
.ph-links { display: flex; align-items: center; gap: 32px; font-size: var(--ph-fs-12); }
/* The nav link: rgba(0,0,0,.8), 8px of padding
   either side, and the full 44px bar height as the hit area — which is also the touch
   minimum, so the row's targets stop being 17px tall.

   ⚠️ DIRECT CHILDREN ONLY, and this is not a nicety. The mega-menu panel lives INSIDE
   `.ph-links` — that one arrangement is what gives both the desktop card and the mobile
   accordion from CSS alone — so a plain `.ph-links a` also matches every link inside
   every panel. Written that way for ten minutes on 2026-08-13 it gave all eight panels'
   product cards a 44px height and 8px of padding, and the cards collapsed into each
   other: images over titles, prices over names. Bugra caught it in a screenshot. Any
   rule meant for the top row has to say `>`. */
.ph-links > a, .ph-links > .ph-item > .ph-trigger {
              color: rgba(0,0,0,.8); letter-spacing: var(--ph-tr-nav);
              display: inline-flex; align-items: center;
              height: var(--ph-nav-h); padding: 0 8px; }
.ph-links a:hover { color: var(--ph-black); text-decoration: none; }
.ph-links a.active { color: var(--ph-accent-link); font-weight: var(--ph-w-600); }
.ph-nav-cta {
  display: inline-flex; align-items: center; height: 28px; padding: 0 14px;
  border-radius: var(--ph-r14); background: var(--ph-accent); color: var(--ph-white) !important;
  font-size: var(--ph-fs-12); font-weight: var(--ph-w-400);
}
.ph-nav-cta:hover  { background: var(--ph-accent-hover);  text-decoration: none; }
.ph-nav-cta:active { background: var(--ph-accent-active); }
/* The design's right-hand cluster: Clinic as an outlined pill, then two icon
   links. Measured off docs/design-frames/phenome-navigation.png at 1440. The pill
   is the same 36px control height as .ph-nav-cta so the two sit on one baseline;
   the icons are 44px targets with an 18px glyph, which is the touch minimum and
   the same size the rail arrows use. No shadow anywhere — elevation YOK. */
.ph-nav-end { display: flex; align-items: center; gap: 10px; }
/* The Clinic trigger, moved out of the row and drawn as the design's outlined pill.
   It is still a .ph-trigger inside a .ph-item, so it keeps hover-intent, the
   chevron, Escape and the active state; only its skin changes. */
.ph-nav-end .ph-item > .ph-trigger {
  /* inline-flex + centring, because .ph-trigger is display:block and a fixed
     height on a block just leaves the line box at the top: an 18px line in a
     36px pill sat 18px above the bottom edge, which is what Can was seeing when
     he said the font looked wrong. It was never the font — Outfit 14/400 was
     correct throughout. */
  display: inline-flex; align-items: center; justify-content: center;
  /* A nav item, not a pill: height 44 (the row's own height, so the target is
     full-bar), 8px of horizontal padding, no ground, no radius, no border.
     It was an outlined 36px pill in a 44px bar, which left 4px of air above and below
     and read as oversized; the client's own frame gives the same pill 14px of air by
     drawing a 61px bar. Two coherent designs, and we had half of each. */
  height: var(--ph-nav-h); padding: 0 8px;
  color: rgba(0,0,0,.8); white-space: nowrap;
  font-size: var(--ph-fs-12); line-height: var(--ph-lh-12);
  font-weight: var(--ph-w-400);
  /* `normal`, not a token: the scale has no 14/400 tracking, and --ph-tr-14-400
     resolved to nothing, so the declaration was DROPPED and the pill inherited
     body's 0.204px — which is the SEVENTEEN-pixel tracking, spread across 14px
     text. Can saw it as the letters flying apart. The rest of this bar is already
     pinned to normal (see .ph-trigger), so this matches it rather than inventing
     a number. Third time today a var() that does not exist has silently deleted
     its own line; grep the token before you write it. */
  letter-spacing: normal;
}
.ph-nav-end .ph-item > .ph-trigger:hover { color: var(--ph-black); }
.ph-nav-end .ph-item > .ph-trigger.active {
  color: var(--ph-accent-link); font-weight: var(--ph-w-600);
}
.ph-nav-end .ph-item > .ph-toggle { display: none; }
.ph-nav-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; color: var(--ph-text-1); text-decoration: none;
}
.ph-nav-icon:hover { color: var(--ph-accent-link); }

/* ---- the profile icon, 2026-09-22 by request, cost the bar 54px it did not
   have below 1180 (the logo was squeezed). Between 901 and 1179 the row gaps
   close up; on phones the icons keep a 36px target around their 18px glyph. */
@media (min-width: 901px) and (max-width: 1179px) {
  .ph-nav .ph-links { gap: 12px; }
  .ph-nav .ph-nav-end { gap: 2px; }
  .ph-nav .ph-brand { flex-shrink: 0; }
}
@media (min-width: 901px) and (max-width: 979px) {
  .ph-nav .ph-links { gap: 4px; }
  .ph-nav .ph-nav-inner { padding-inline: 24px; }
}
@media (max-width: 480px) {
  .ph-nav .ph-brand { flex-shrink: 0; }
  .ph-nav .ph-nav-end { gap: 2px; }
  .ph-nav .ph-nav-icon { width: 36px; }
  .ph-nav .ph-burger { padding: 8px 4px 8px 8px; }
}
/* Below 901 the drawer owns the links and the row must not grow: the bar already
   has only 0px of slack at 901 (see .ph-toggle's note), so the pill and the CTA
   step out and the two icons stay — they are the only things a phone can use
   without opening the drawer. */
@media (max-width: 900px) {
  .ph-nav-end .ph-item, .ph-nav-cta { display: none; }
}
.ph-burger { display: none; background: none; border: 0; cursor: pointer; padding: 8px; }
.ph-burger span { display: block; width: 18px; height: 1.6px; background: var(--ph-text-1); margin: 4px 0; }
@media (max-width: 900px) {
  .ph-nav, [data-ph-nav] { height: 48px; }
  /* The drawer covers the screen under the bar and slides in from the right.
     It stays laid out and is only moved and hidden, so opening and closing
     both animate. */
  .ph-links {
    /* The bar's backdrop filter makes the bar, not the window, the box a fixed
       child is placed in: so the drawer starts at the bar's foot and states its
       own height instead of stretching to a bottom edge it cannot see. */
    display: flex; position: fixed; inset: 48px 0 auto 0;
    height: calc(100vh - 48px); height: calc(100dvh - 48px);
    /* Opaque, not translucent: the drawer holds expanded menu panels, and page
       copy showing through behind two columns of links is unreadable. */
    background: var(--ph-white);
    /* And scrollable: an open Supplements panel is taller than a phone. */
    overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain;
    flex-direction: column; align-items: stretch; gap: 0;
    padding: 12px var(--ph-margin) calc(40px + env(safe-area-inset-bottom));
    transform: translateX(100%); visibility: hidden;
    transition: transform .46s cubic-bezier(.32, .72, 0, 1), visibility 0s linear .46s;
  }
  .ph-links.open {
    transform: none; visibility: visible;
    transition: transform .46s cubic-bezier(.32, .72, 0, 1), visibility 0s;
  }
  .ph-links a { font-size: var(--ph-fs-17); padding: 12px 0; width: 100%; border-bottom: 1px solid var(--hair); }
  /* The lines sit at the left of a 44px tap target; the target reaches into the
     gutter so the lines end on the same 24px margin as the logo. */
  .ph-burger { display: block; margin-right: -18px; }
}

/* ---- Layout primitives ---- */
/* system.css: the content box is content-max PLUS two margins; above that the
   margin absorbs the extra width, so content stays locked at 1260. */
.wrap      { width: 100%; max-width: calc(var(--ph-content-max) + var(--ph-margin) * 2);
             margin-inline: auto; padding-inline: var(--ph-margin); }
.wrap-wide { width: 100%; max-width: calc(var(--ph-content-max) + var(--ph-margin) * 2);
             margin-inline: auto; padding-inline: var(--ph-margin); }

/* ---- The reading measure ----
   .wrap and .wrap-wide are byte-identical — both resolve to 1260px — so 725 of the
   site's 747 sections are the same width, and no amount of motion composes a rhythm
   out of a page that has exactly one column. This is the third container, and its
   width is not invented: --ph-read-max is already the measure the .lede and the
   .acc-body are set to, so a prose slab moved into it is being put in the box the
   design system had all along and nothing was using.

   The visible change is the heading. A 48-point h2 spanning 1260px above a paragraph
   that stops at 820 reads as a poster; the same h2 stopping at 980 with the paragraph
   under it reads as an article, which is what those sections are.

   Below 1024 the page margin has already closed the gap and this computes identical
   to .wrap-wide — correctly. There is no room for two measures on a phone, and
   pretending otherwise is how a narrow column becomes a narrow column with no
   margin. */
.wrap-read { width: 100%; max-width: calc(var(--ph-read-max) + var(--ph-margin) * 2);
             margin-inline: auto; padding-inline: var(--ph-margin); }

.section       { padding: var(--ph-sec-top) 0 var(--ph-sec-bottom); }
.section-tight { padding: calc(var(--ph-sec-top) / 2) 0 calc(var(--ph-sec-bottom) / 2); }
.section-gray  { background: var(--ph-surface); }
.section-black { background: var(--ph-black); color: var(--ph-surface); }
/* Two components read a light-ground token and have no dark-ground rule of their own.
   The figcaption measures 4.1:1 on the ORIGINAL black too, so this is a pre-existing
   failure being closed rather than one the brand colour introduced — which is why it
   is not scoped to the modifier. */
.section-black .quote figcaption,
.section-black table th { color: var(--ph-tertiary); }
.section-black h2, .section-black h3 { color: var(--ph-white); }
/* The dark band keeps the two voices it already had — Bugra chose it over the frame's
   centred column on 2026-08-17, having seen both. Only its label row changed. */
/* ============================================================= LABEL PILLS
   THE SITE-WIDE SHAPE FOR A NAMED SET. Wherever a section lists short labels — the
   five signals the ring streams, the areas a report covers, the goals a quiz maps to —
   they ship as this row and nothing else. Bugra, 2026-08-17: "bu tarz etiketli
   bölümlerde tasarımı bu şekilde yapalım site genelinde, sistem kural olarak göm."

   They were middot-joined running text before, then a smaller 36px chip. Both read as
   an aside. A set of things the product measures is not an aside — it is the evidence
   under the claim above it, and it is sized to be read rather than skimmed past.

   Every value is on the system: 48 and 24 are tokens.space.scale, 17/24 is the type
   ladder's body rung, r27 is tokens.radius.scale and exceeds half the height so the
   end caps are true semicircles. No shadow and no border — elevation YOK; the pill is
   legible because its ground differs from the page's, which is the surface ladder
   doing its job. */
.pills { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; }
.pill-lab {
  display: inline-flex; align-items: center; height: 48px; padding: 0 24px;
  border-radius: var(--ph-r27); background: var(--ph-surface);
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
  letter-spacing: var(--ph-tr-17-400); color: var(--ph-text-1);
}
/* On the dark band the pill lifts off the ground rather than sitting on a light chip:
   a white chip there would punch holes in the one moment the page goes quiet. */
.section-black .pill-lab { background: rgba(255,255,255,.12); color: var(--ph-white); }
.section-black p { color: var(--ph-tertiary); }

/* ---- Type ---- */
/* THE OVERLINE IS A PROMISE LINE NOW, 2026-09-16, by request.

   It was 12px at +0.96px of tracking, which is the setting a caps overline
   takes — the letters were sentence case but the TYPESETTING was the all caps
   device, and the page read that way. The Band has no overline at all; it has
   one line under the name, at 600 in the brand blue, saying what the thing is
   for, and that is the shape the whole store takes now: a size up, tracking
   back to normal, the same blue. See the text ladder in phenome-glass.css § 3
   for the four inks and the four sizes this belongs to. */
.eyebrow {
  font-size: 15px; line-height: 20px; font-weight: var(--ph-w-600);
  letter-spacing: -0.005em; color: var(--ph-tone-promise, var(--ph-accent-link));
}
h1.hero-title {
  margin: 14px 0 0; font-size: var(--ph-h1-size); line-height: var(--ph-h1-lh);
  font-weight: var(--ph-w-600); letter-spacing: var(--ph-h1-tr); text-wrap: balance;
}
h2.sec-title {
  margin: 16px 0 0; font-size: var(--ph-h2-size); line-height: var(--ph-h2-lh);
  font-weight: var(--ph-w-600); letter-spacing: var(--ph-h2-tr); text-wrap: balance;
}
h2.card-title, h3.card-title {
  margin: 0; font-size: var(--ph-h3-size); line-height: var(--ph-h3-lh);
  font-weight: var(--ph-w-600); letter-spacing: var(--ph-h3-tr); text-wrap: balance;
}
/* The rule above is right for the 33 card titles that take their size from the class
   (28/36 at ≥1024, 24/32 below — both size + 8, both on the ladder) and wrong for the
   722 that do not. Measured 2026-08-13: of 796 .card-title on 72 pages, 722 carry
   style="font-size:21px" written by build_pages/build_pages2/build_pages3/build_all, and
   an inline font-size overrides ONLY the size. So the 28 rung's leading and tracking kept
   applying to 21px text: 21/36 at ≥1024 and 21/32 below, at -0.028px and +0.072px of
   tracking. tokens type.lineHeightRule is a FIXED OFFSET — size + 4 tight, size + 8 loose
   — so 36 is size + 15 and 32 is size + 11, and neither is on any rung. That is 11px of
   surplus leading on every one of those 722 titles, which is most of what "boş" meant.
   This is the same failure fix_type_scale.py was written for ("an inline font-size
   overrides only the first … keeps tracking calibrated for a 17px glyph") and it is
   deliberately outside that pass's scope: it leaves inline sizes that are ALREADY on the
   ladder alone, and 21 is on the ladder. So the class has to answer for it, and the only
   place the size is written is the style attribute — hence the attribute selector.
   It cannot false-match: "font-size:121px" does not contain the substring
   "font-size:21px" and neither does any other rung. The titles that already carry an
   inline line-height:25px keep it — inline wins, and it is the same value.
   Both spacings are listed, and that is not belt-and-braces. All four generators and
   fix_type_scale write "font-size:21px" closed up today; the day one of them writes a
   space instead, a one-form selector stops matching and 761 titles go quietly back to
   21/36 with nothing on this project able to see it — no gate reads leading, the markup
   stays valid, and the page just gets airy again.
   The durable fix is for the four generators to write the rung's leading beside the size
   they hard-code; until they do, this keeps every built page on the ladder. */
/* Tag-agnostic since 2026-08-13. It read h2 and h3 only, and /store/checkout/ writes
   `<span class="card-title" style="font-size:21px">` — same class, same size, a
   different tag, so the leading never reached it and the line sat at 21/21. Found by
   check_design_language on its second run. A selector that names the tag is a selector
   that is one generator away from being wrong. */
.card-title[style*="font-size:21px"],
.card-title[style*="font-size: 21px"] {
  line-height: var(--ph-lh-21); letter-spacing: var(--ph-tr-21-600);
}
.lede {
  margin: 20px auto 0; max-width: min(var(--ph-read-max), var(--ph-measure-lead));
  font-size: var(--ph-lead-size); line-height: var(--ph-lead-lh);
  letter-spacing: var(--ph-tr-21-400); color: var(--ph-text-2);
}
/* Body copy takes the LOOSE offset. tokens type.lineHeightRule is "size + 4 (tight,
   heading) or + 8 (loose, body)" and this paragraph had been taking the tight one:
   17px in a 21px box is 1.235, which is inside the booklet's HEADING band (1.05-1.25)
   and nowhere near its body band (1.5-1.6). Measured 2026-08-13 on 2,272 elements
   across all 120 pages. Only .body-text moves: re-pointing --ph-lh-17 itself, or body,
   would drag 1,823 elements that inherit that box at other sizes with it. */
.body-text {
  max-width: var(--ph-measure);
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17-loose);
  letter-spacing: var(--ph-tr-17-400); color: var(--ph-text-2);
}
.center { text-align: center; }

/* ---- Opener ----
   163 sections open with an eyebrow, a 48-point heading and a 21-point lede stacked
   in one column, and every one of them is the same three lines at the same three
   sizes in the same box. The copy is signed off and cannot be rewritten, so the only
   thing left to change is where it sits and how big it is — which turns out to be
   enough, because the lede was never a subtitle. It is the second half of the
   argument, and stacking it under the heading at body weight is what made it read as
   a caption.

   28, not 48. A lede promoted to its heading's own size stops being a lede and
   becomes a second headline, and on a health brand a sentence that gains rhetorical
   weight has gained a claim nobody signed off. 28 is the h3 token: out of body
   register, still subordinate, still the same words.

   1.05fr / 1fr with a 64px gap is not a new geometry — it is exactly the split
   .hero-split already uses, so the page's second-level head reads as the hero's echo
   rather than as a fourth arrangement.

   minmax(0, …) on both tracks is not decoration. 1fr carries an automatic min-content
   minimum, and one long unbroken string would push the grid wider than the page —
   which un-gates audit_layout's ENTIRE overflow sweep for that page and surfaces
   every unrelated pre-existing near-miss on it. Both tracks are floored at zero.

   row-gap is zero and stays zero: every component that follows a head on this site
   carries its own inline margin-top, and a grid gap on top of it would silently add
   20px to 163 sections. And nothing here gets a negative margin for optical
   alignment — grid children are exactly what the overlap check walks, and two of them
   sharing two pixels on both axes is the one thing that turns the whole gate red. */
.opener { display: grid;
          grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
          column-gap: 64px; row-gap: 0; align-items: start; }
.opener > .opener-t { grid-column: 1; }
/* Both columns top-align: with an eyebrow present the h2 keeps its 16px; without one
   the h2 is first and that 16px would drop column 1 below column 2. */
.opener > .opener-t > :first-child { margin-top: 0; }
.opener > .lede { grid-column: 2; margin-top: 0; max-width: none;
                  font-size: var(--ph-fs-28); line-height: var(--ph-lh-28); }
/* No letter-spacing here on purpose. The tracking ladder has no 28/400 value — the 28
   token is a 600-weight figure — so rather than invent one the lede keeps the 0.252px
   it already carries, which at this size is a hair loose and true. */
.opener > :nth-child(n+3) { grid-column: 1 / -1; }

/* ---- Showcase ----
   The opener gives a section two voices. This gives it a picture, and it is the one
   arrangement the site did not have: below the hero, every photograph on this site is
   either a full-bleed band or a thumbnail inside a card. There was nothing in between,
   which is why the Ceramic and Titanium sections shipped as a price, a colour list and
   a button in the left 760px of a 1440px screen, with the other 40% white.

   The proportions are the hero's, mirrored: the picture takes the larger track,
   because the point of this arrangement is the picture. Copy stays in the smaller one
   and keeps its own measure, so a paragraph never runs past 60 characters.

   Alternating the side is the whole rhythm. Two of these stacked with the photograph
   on the same side is a template; swapped, it is a page turning. .showcase-alt only
   moves grid-column — the DOM order never changes, so at one column the copy is always
   above its picture and a screen reader gets the same page a sighted reader does.

   No shadow, at any size: tokens.json records `elevation YOK`. The radius and the
   clipped box are the whole treatment, and the clipping is also what lets the picture
   settle on arrival without a single layout box moving. */
.showcase { display: grid;
            grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr);
            column-gap: clamp(48px, 6vw, 96px); align-items: center; }
/* grid-row: 1 on both, and it is not belt-and-braces. Auto-placement walks the DOM in
   order and never moves backwards across a row: on the alternating instance the copy
   is placed in column 2, and the picture — asked for column 1, which the cursor has
   already passed — is pushed to row 2. Measured before this line: the photograph sat
   437px BELOW the paragraph it belongs to, inside a section that still passed every
   gate, because a gap is not an overlap. */
.showcase > .showcase-t   { grid-column: 1; grid-row: 1; }
.showcase > .showcase-img { grid-column: 2; grid-row: 1; }
/* The track sizes swap with the columns, not just the content. Moving the picture
   into column 1 without this put it in the 1fr track and shrank it by 8% on every
   alternate row — the alternation stopped reading as a rhythm and started reading as
   a mistake. The picture is always in the larger track, whichever side it is on. */
.showcase.showcase-alt { grid-template-columns: minmax(0, 1.08fr) minmax(0, 1fr); }
.showcase.showcase-alt > .showcase-t   { grid-column: 2; }
.showcase.showcase-alt > .showcase-img { grid-column: 1; }
.showcase > .showcase-t > :first-child { margin-top: 0; }
.showcase-img { border-radius: var(--ph-r27); overflow: hidden; }
.showcase-img img { width: 100%; height: clamp(360px, 33vw, 540px);
                    object-fit: cover; display: block; }
@media (max-width: 1023px) {
  .showcase { grid-template-columns: minmax(0, 1fr); row-gap: 36px; }
  .showcase, .showcase.showcase-alt { grid-template-columns: minmax(0, 1fr); }
  /* One column, and the row pin comes off with it — otherwise both children are
     asked for row 1 and the picture lands on top of the paragraph. */
  .showcase > .showcase-t, .showcase > .showcase-img,
  .showcase.showcase-alt > .showcase-t, .showcase.showcase-alt > .showcase-img {
    grid-column: 1; grid-row: auto;
  }
  .showcase-img img { height: clamp(300px, 52vw, 420px); }
}

/* ---- Product card ----
   The store category pages shipped their products as three grey boxes holding a name,
   a price, and one paragraph with the feature list typed into it separated by pipes:
   "✓ Digestion, bloating & IBS drivers | ✓ Mood, sleep & energy links | ✓ Immunity &
   inflammation markers | At-home stool sample". Four facts, one sentence, no picture,
   and nothing clickable — under a single "Explore" button that went to the first
   product only. That is a catalogue rendered as prose.

   The photograph is the real kit, never a generated stand-in: BRIEF §8.3, and it has
   cost a rework twice. 3:2 because that is what every kit was shot at, so the crop is
   the frame rather than a decision.

   .ticks is .assure at card scale — same meaning, a mark that belongs to the
   component rather than being typed into the sentence, so the day someone edits the
   line the tick goes with it. 14px rather than 20px because three of these stacked in
   a 347px column at the assurance size is a wall of blue. */
/* The ground is --ph-pill, not --ph-surface, and that is the surfaceLadder rule rather
   than a preference. tokens color.surfaceLadder: L1 card and L2 in-card media MUST
   differ, and its $why is this exact bug reported once already — "Ürün Kartı'nın medyası
   #F5F5F7 kuruldu, sayfa zemini de #F5F5F7'ydi → kartlar zemine karıştı". Here the media
   well was #F5F5F7 and .card-gray is #F5F5F7, so on a white section the well and its own
   card were one colour. The photograph hides it once loaded; before that, and on a slow
   connection, the card is a seamless grey slab with no picture in it. #E8E8ED is the
   ladder's own L2 and is one ramp step from both grounds this card takes (#F5F5F7 on a
   white section, #FFFFFF on a grey one), so $minDelta holds either way round. */
.card-img.kit { aspect-ratio: 3 / 2; margin-bottom: 22px; background: var(--ph-pill); }
.card-img.kit img { width: 100%; height: 100%; object-fit: cover; }

/* The picture runs the card's full width. Measured 2026-08-13: all 15 kit photographs on
   the site are the first child of a .card-gray (12 anchors, 3 divs, on /store/genetic/,
   /store/microbiome/, /store/devices/ and /testing/genetic/), and every one sat 30px in
   from the card's sides and 32px down from its top — 90.3% of its card's width at 1440
   and 82.5% at 390, which is 49.7% and 31.7% of the card's area. .tile-media, which is
   the client's own Store Card, is 100% of its tile's width and 63.9–68.8% of its area.
   tokens image.cardImage locks the WIDTH and lists 372 and 620; 620 is exactly this
   card's own width at 1440, i.e. the picture is specified to run the card edge to edge.
   Negative margins, never overflow:hidden on the card. audit_layout's clipped check
   reports any text leaf whose box escapes an ancestor with computed overflow hidden, and
   .card-gray has 776 instances with three text leaves in each — the same trap .ctable-box
   documents. The three values here are .card-gray's own padding (32px 30px) negated
   exactly, so the picture reaches the card's border box and stops: the card's own box
   does not move and no sibling's does either. If that padding ever changes, these change
   with it.
   Square bottom corners, because the photograph now meets the card's flat ground there
   and rounded ones would leave a grey wedge under each bottom corner. The top two match
   the card's own 18. Scoped to .card-gray rather than .card because that is the only
   shape that exists; a kit dropped into a white .card keeps its inset rather than
   bleeding by the wrong 4px and 2px. */
.card-gray > .card-img.kit:first-child {
  margin: -32px -30px 22px;
  border-radius: var(--ph-r18) var(--ph-r18) 0 0;
}

.ticks { list-style: none; margin: 16px 0 0; padding: 0; display: grid; gap: 9px; }
.ticks li { display: grid; grid-template-columns: 15px 1fr; gap: 10px; align-items: start;
            font-size: var(--ph-fs-17); line-height: var(--ph-lh-17); color: var(--ph-text-2); }
.ticks svg { width: 14px; height: 14px; margin-top: 4px; color: var(--ph-accent); }
.section-black .ticks li { color: var(--ph-tertiary); }
.section-black .ticks svg { color: var(--ph-on-brand); }

/* The sample type is not a claim, it is a specification, so it sits below the ticks in
   the secondary tone with a rule above it rather than taking a mark it has not earned. */
.card-spec { margin: 16px 0 0; padding-top: 14px; border-top: 1px solid var(--hair);
             font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); color: var(--ph-text-2); }
.card-price { margin: 12px 0 0; font-size: var(--ph-fs-21); line-height: var(--ph-lh-21);
              font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-21-600);
              color: var(--ph-text-1); }
.section-black .card-price { color: var(--ph-white); }

/* ---- Spec rows ----
   "Price / From £199" and "Colours / Pink · Black · White" shipped as two flex divs
   with five inline styles between them, the value set at body size in the same colour
   as the label beside it. On a product page the specification is the argument, and a
   figure that looks exactly like the word next to it is not being read as a figure.

   A definition list, because that is what it is: dt is the question, dd is the answer.
   The answer goes up one step to 19/600 — enough to be the thing your eye lands on,
   not so much that it competes with the section's own heading. */
.spec { margin: 30px 0 0; max-width: 620px; }
.spec > div { display: grid; grid-template-columns: minmax(0, 1fr) auto;
              column-gap: 24px; align-items: baseline;
              padding: 15px 0; border-top: 1px solid var(--hair); }
.spec dt { margin: 0; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
           color: var(--ph-text-2); }
.spec dd { margin: 0; font-size: var(--ph-fs-19); line-height: var(--ph-lh-19);
           font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-19-400);
           text-align: right; }
.section-black .spec > div { border-color: rgba(255,255,255,.18); }
.section-black .spec dt { color: var(--ph-tertiary); }
.section-black .spec dd { color: var(--ph-white); }

/* A ≥1024 gesture, and it says so. At 800 two columns are 320 and 300px, and a
   32-point heading beside a 24-point paragraph in 300px is not a composition, it is a
   squeeze. Below this the section is byte-for-byte what it is today. */
@media (max-width: 1023px) {
  .opener { grid-template-columns: minmax(0, 1fr); }
  .opener > .opener-t, .opener > .lede { grid-column: 1; }
  .opener > .lede { margin-top: 20px;
                    font-size: var(--ph-fs-24); line-height: var(--ph-lh-24); }
}
@media (max-width: 799px) {
  .opener > .lede { font-size: var(--ph-lead-size); line-height: var(--ph-lead-lh); }
}

/* ---- Buttons & links ---- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--ph-btn-h); padding-inline: var(--ph-btn-px);
  border-radius: var(--ph-r56); background: var(--ph-accent); color: var(--ph-white) !important;
  font-family: var(--ph-font); font-size: var(--ph-fs-17); font-weight: var(--ph-w-400);
  text-decoration: none !important; border: 0; cursor: pointer;
}
.btn:hover  { background: var(--ph-accent-hover); }
.btn:active { background: var(--ph-accent-active); }
.btn.small { height: var(--ph-btn-sm-h); padding-inline: var(--ph-btn-sm-px); font-size: var(--ph-fs-14); }
.btn.ghost { background: var(--ph-white); color: var(--ph-text-1) !important; border: 1px solid var(--ph-line); }
/* Outlined controls fill with the brand blue on hover, not grey and not black.
   Can, 2026-08-13: "üstüne imleç geldiğinde siyah değil mavi olsun bu tarz
   butonların içi". The !important on .btn.ghost's colour is why the hover needs
   one too.
   The fill is --ph-accent-HOVER, not the resting --ph-accent: it is the difference
   between "this button filled in" and "this button became a different button" —
   resting { background: transparent; color: #0066CC; border-color: #0066CC }, hover
   { background: #0076DF; color: #fff; border-color: transparent }. Bugra sent the
   before/after of exactly this control on 2026-08-20 and asked for it by name. The border goes transparent rather than blue so nothing draws a second edge
   a shade off the fill; the 1px is still there, so no measurement moves. */
.btn.ghost:hover {
  background: var(--ph-accent-hover); border-color: transparent;
  color: var(--ph-white) !important;
}
.btn.ghost:active { background: var(--ph-accent-active); border-color: transparent; }
.btn.disabled { background: var(--ph-surface); color: var(--ph-tertiary) !important;
                border: 1px solid var(--ph-line); pointer-events: none; }
.link-more { font-size: var(--ph-fs-17); }
/* The chevron was `content: " ›"` — a plain space, which renders at the element's
   own tracking and reads as if the glyph is stuck to the last letter. Can: "neden bi
   aşırı bitişik". It also inherited a 21px/600 card title, so it came out heavier than
   the word it follows. Both fixed here and on the three other selectors that carry it:
   a real gap, and the glyph one weight down at the size it sits beside. */
.link-more:after,
a.card-gray .card-title:after, a.card .card-title:after,
a.compare-side .k:after {
  content: "›"; margin-inline-start: .3em;
  font-weight: var(--ph-w-400); font-size: .92em; line-height: 1;
  display: inline-block; vertical-align: baseline;
}

/* ---- Cards & grids ---- */
.grid { display: grid; gap: var(--ph-gutter); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 900px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }
.card      { background: var(--ph-white); border-radius: var(--ph-r18); padding: 36px 32px 30px; }
.card-gray { background: var(--ph-surface); border-radius: var(--ph-r18); padding: 32px 30px; }
/* A surface has to differ from the surface it sits on — in colour, or by a border, or by
   a shadow. With none of the three the card disappears, and that is exactly what was
   shipping: `.card-gray` is #F5F5F7 and `section-gray` is #F5F5F7, so on a grey section
   the card had no edge at all. Measured before this rule: 418 such cards site-wide, and
   on 23 of 28 pages probed — 22 on /store/, 18 on /testing/carrier-screening/, 15 on
   /testing/comprehensive-genomic/. Those pages read as undifferentiated slabs of grey
   text, which is what the client meant by "cansız": the page was not short of colour,
   its cards had dissolved into the section.

   Fixed in COLOUR, not with a border or a shadow. A shadow is out — tokens.json records
   `elevation YOK`, and the design system's one exception is a card you can buy from. A
   border would add a hairline to 418 cards and make the page busier, not clearer. White
   on #F5F5F7 is 3.4 L* of separation using two tokens the system already has, and it
   inverts the relationship correctly: on a white section the card is grey, on a grey
   section the card is white. Same idea either way round — the card always sits on top.

   Not extended to .section-black: a grey card on black already separates by 96 L*, and a
   white card there is the "eleven products with no name on them" bug, because
   .section-black paints its headings white. */
.section-gray .card-gray { background: var(--ph-white); }
.card-img  { border-radius: var(--ph-r18); overflow: hidden; }
.card-img img { width: 100%; height: 100%; object-fit: cover; }

/* A card that is a destination. The "Related tests & services" blocks shipped as
   plain divs — three product names, three prices, nothing clickable — so the one
   section on the page whose job was to send you onward did not. Same card, made an
   anchor: the whole surface is the target, the chevron says so, and the ground
   steps one tone on hover rather than lifting, because the token set has no
   elevation scale to lift into. */
a.card-gray, a.card { display: block; color: inherit; text-decoration: none;
                      transition: background var(--ph-dur-fast) var(--ph-ease); }
a.card-gray:hover, a.card:hover { text-decoration: none; background: var(--ph-pill); }
a.card-gray:hover .card-title, a.card:hover .card-title { color: var(--ph-accent-link); }
a.card-gray .card-title:after, a.card .card-title:after { color: var(--ph-accent-link); }
.section-black a.card-gray:hover, .section-black a.card:hover { background: #27272a; }
.dot { width: 64px; height: 64px; border-radius: 50%; }
.pill {
  display: inline-flex; align-items: center; height: 30px; padding: 0 14px;
  border-radius: var(--ph-r14); background: var(--ph-pill); color: var(--ph-accent-link);
  font-size: var(--ph-fs-14); font-weight: var(--ph-w-600);
}
.tag {
  display: inline-flex; align-items: center; gap: 9px; height: var(--ph-btn-h);
  padding: 0 20px; border-radius: var(--ph-r21); border: 1px solid var(--ph-line);
  font-size: var(--ph-fs-14);
}
/* A .tag had a transition and a press state and NO hover, for as long as it has
   existed — so 154 of them ("Read →", "Explore →", "Retake quiz", the app's four
   nav labels) answered the cursor with nothing but the global underline. It is the
   site's outlined pill, so it fills the same way
   .btn.ghost does.
   ⚠️ THE ELEMENT IN THE SELECTOR IS LOAD-BEARING, against this project's own rule
   that naming a tag is one generator away from being wrong. Measured 2026-08-20:
   .tag is on 154 <a> AND 58 <span>. The spans are captions and spec chips — nothing
   you can click. A bare `.tag:hover` would light all 58 of them blue under the
   cursor, which is a fake control, exactly what the pro-guard exists to catch. If a
   generator ever emits a clickable .tag on some third element, add that element
   here; do not widen this to `.tag:hover`. */
a.tag:hover, button.tag:hover {
  background: var(--ph-accent-hover); border-color: transparent;
  color: var(--ph-white); text-decoration: none;
}
a.tag:active, button.tag:active {
  background: var(--ph-accent-active); border-color: transparent;
}

/* ---- Hero split ---- */
/* The BOTTOM is --ph-hero-bottom, not --ph-sec-bottom. tokens.json ›
   rhythm.byBreakpoint.*.heroBottom is 52/52/36/28; the property was declared with
   exactly those three values at the top of this file and referenced NOWHERE, so every
   hero shipped the full section bottom instead — 116/116/92/112, two to four times the
   rule, on 119 pages. Corroborated in the client's own frames, measured 2026-08-13 as
   the gap from the hero's last element to the bottom edge of the hero's own band:
   Find Your Size 47 · Longevity Hub Gut Microbiome 52 · Gut Guide 56 (desktop 1440),
   and 27/27/28/30 on the four mobile product frames (390). The frames and the token
   agree; the stylesheet was the only thing that did not.

   The TOP stays 84px and is NOT var(--ph-hero-bottom)'s partner. tokens.json says the
   hero's top padding is 0 and every frame contradicts it: measured from the bottom of
   the nav to the first hero line, 55–103px at 1440 (Hub 55/59 · Category Supplements 56
   · Shop all 70 · Oral Microbiome 73 · Explore 80 · Homepage 85 · Find Your Size
   and How It Works 99 · Ring — Explore 103) and 30–60px at 390. 84 sits inside the
   desktop range; at 390 it is above ALL of it, and the frames cluster at three different
   values there (30 · 44 · 60) rather than one. Choosing between them is Phenome's, not
   this file's — see the session report. Do not "finish the job" by writing 0 here. */
.hero { padding: 84px 0 var(--ph-hero-bottom);
        /* Grey at the TOP, white at the bottom — the gradient used to run the other way.
           The nav is 80% white with no border, so what makes its edge visible is the
           tone of the page behind it, which is why the page ground is #F5F5F7. Against a
           white hero the bar's bottom edge measured 6/255 — invisible. Bugra asked for
           this on 2026-08-13. It still lands on white where the copy continues, so the
           section after it keeps the alternation fix_section_rhythm owns. */
        background: linear-gradient(180deg, var(--ph-surface) 0%, var(--ph-white) 100%); }
.hero-split { display: grid; grid-template-columns: 1.05fr 1fr; gap: 64px; align-items: center; }
@media (max-width: 900px) { .hero-split { grid-template-columns: 1fr; gap: 36px; } }
.hero-facts { margin-top: 34px; display: flex; gap: 36px; flex-wrap: wrap; }
.hero-fact .k { font-size: var(--ph-fs-12); font-weight: var(--ph-w-600);
                letter-spacing: var(--ph-tr-12-600-caps); color: var(--ph-text-2); }
.hero-fact .v { margin-top: 4px; font-size: var(--ph-fs-17); font-weight: var(--ph-w-600); }
/* No shadow. tokens.json records `elevation YOK` — the measured system has no
   elevation scale, so nothing on this site may lift, glow or drop a shadow, and that
   constraint is why the motion layer reads as restraint rather than as a theme. This
   rule carried `box-shadow: 0 24px 70px rgba(0,0,0,.12)` on 87 pages' heroes, and
   .hero-figure below carried the same declaration on two more. NO GATE ON THIS
   PROJECT READS COLOUR OR SHADOW, which is the only reason it survived this long;
   the client's own frames draw these heroes flat. */
.hero-img { border-radius: var(--ph-r27); overflow: hidden; }
.hero-img img { width: 100%; display: block; }

/* ---- Steps ----
   One component for every "how it works" sequence on the site. There used to be
   two: .step here, and a .card-gray whose <h3> read "STEP 01 — Order your kit",
   with the number written into the copy. Thirteen sections between them, and the
   label spelled three ways — "01", "Step 01", "STEP 01". The number is a piece of
   the design, not a piece of the sentence, so it lives in .num and the title is
   just the title.

   The icon is inline SVG rather than an <img> because it takes its colour from
   currentColor: the same markup has to read on white and on .section-black, and
   an <img> cannot invert. Stroke, never fill — a filled glyph at 24px turns into
   a blob, and the brand's own menu artwork is illustration, not iconography. */
.steps { display: grid; gap: var(--ph-gutter); }
.steps-3 { grid-template-columns: repeat(3, 1fr); }
.steps-4 { grid-template-columns: repeat(4, 1fr); }
.steps-5 { grid-template-columns: repeat(5, 1fr); }
.steps-6 { grid-template-columns: repeat(3, 1fr); row-gap: 44px; }
.step { position: relative; }
.step-mark {
  width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center;
  background: var(--ph-surface); border: 1px solid var(--ph-line);
  color: var(--ph-text-1); margin-bottom: 20px;
}
.step-mark svg { width: 24px; height: 24px; display: block; }

/* The rule that ties the marks into one sequence. It starts at the mark's right
   edge and is pulled a full gutter past the cell, which lands it exactly on the
   next mark — so the line follows the grid instead of being guessed at. */
.step::after {
  content: ""; position: absolute; top: 26px; left: 68px;
  right: calc(-1 * var(--ph-gutter)); height: 1px; background: var(--ph-line);
}
.step:last-child::after { display: none; }

.step .num { font-size: var(--ph-fs-12); font-weight: var(--ph-w-600);
             letter-spacing: var(--ph-tr-12-600-caps); color: var(--ph-accent-link); }
.step .t { margin-top: 10px; font-size: var(--ph-fs-21); line-height: var(--ph-lh-21);
           font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-21-600); }
.step p { margin: 8px 0 0; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17); color: var(--ph-text-2); }

/* Below the grid breakpoint the row becomes a vertical timeline rather than four
   stacked cards: the mark moves out of flow to the left and the connector turns
   90°, so the sequence still reads as a sequence on a phone. */
@media (max-width: 900px) {
  .steps, .steps-3, .steps-4, .steps-5, .steps-6 { grid-template-columns: 1fr; gap: 0; }
  .step { padding: 0 0 30px 72px; }
  .step:last-child { padding-bottom: 0; }
  .step-mark { position: absolute; left: 0; top: 0; margin-bottom: 0; }
  .step::after { top: 60px; bottom: 6px; left: 26px; right: auto; width: 1px; height: auto; }
}

/* Reversed ground. rgba over black composes below L44, so it stays clear of the
   83–100% band the token set forbids filling in. */
.section-black .step-mark { background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.18);
                            color: var(--ph-white); }
.section-black .step::after { background: rgba(255,255,255,.18); }
.section-black .step .num { color: var(--ph-on-brand); }
.section-black .step .t { color: var(--ph-white); }
.section-black .step p { color: var(--ph-tertiary); }

/* ---- Figures ----
   The most interesting content on the Ring pages is its numbers — 90 sleep, 94 heart
   rate, 20,000 genes — and every one of them shipped as a 21px card title inside a
   grey box, the same weight as the word next to it. A number set at the size of a
   label stops being a number.

   No box: the figure carries itself, and the rule above it does the grouping the
   card used to do. This is the one place on the site where type goes to display
   size outside a heading, which is exactly what makes the row read as evidence
   rather than as four more cards. */
/* Each figure on its own card, which is what the frame draws — four grey panels, the
   number centred over its caption. Bare figures under a rule read as a table of
   contents; on a card they read as four settled facts. The card is the surface
   ladder's L0 on white, no border and no shadow. */
.figures { display: grid; gap: var(--ph-gutter); }
.figures .figure {
  background: var(--ph-surface); border-radius: var(--ph-r18);
  padding: 32px 24px; text-align: center; border-top: 0;
}
.section-gray .figures .figure { background: var(--ph-white); }
/* minmax(0, 1fr), not 1fr: a bare `1fr` is minmax(AUTO, 1fr), so a column whose figure
   holds a long word cannot shrink below that word and steals width from its siblings.
   Measured on /devices/ring/ at 390: the five-figure row wrapped to two columns of 176
   and 146px — a 30px spread in a grid whose whole job is that the columns are equal. */
.figures-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.figures-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.figures-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.figure { border-top: 1px solid var(--hair); padding-top: 22px; }
.figure .n { font-size: var(--ph-h2-size); line-height: var(--ph-h2-lh);
             font-weight: var(--ph-w-600); letter-spacing: var(--ph-h2-tr);
             color: var(--ph-text-1); }
.figure .n small { font-size: var(--ph-fs-24); line-height: var(--ph-lh-24);
                   letter-spacing: 0; margin-left: 4px;
                   color: var(--ph-text-2); font-weight: var(--ph-w-400); }
.figure .l { margin-top: 10px; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
             color: var(--ph-text-2); }
@media (max-width: 900px) {
  .figures-3, .figures-4, .figures-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.section-black .figure { border-color: rgba(255,255,255,.18); }
.section-black .figure .n { color: var(--ph-white); }
.section-black .figure .l, .section-black .figure .n small { color: var(--ph-tertiary); }

/* ---- Two-up comparison ----
   The site's most repeated rhetorical move, and the one with no component: "Your
   genome / Your phenome", "The question / The answer", "Here on the web / In the
   Phenome app" — 33 of them, every one drawn as two identical grey boxes sitting
   next to each other by coincidence.

   Two boxes do not say "these two things are being weighed against each other".
   One surface split by a rule does. So the cards come off, the ground shows
   through, and the relationship is carried by the divider — which is also how the
   type gets to be bigger, because it is no longer inside a padded card. */
.compare { display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--hair); }
/* The choice variant. A comparison that carries a price is not a table of differences,
   it is two things you pick between — and it has to be legible as that. Two cards,
   the chosen one outlined, which is the SAME shape the buy block's Material picker
   uses higher up the same page: one page, one way of showing a choice. A border, never
   a shadow — elevation YOK. */
.compare.compare-pick {
  border-top: 0; gap: 24px; align-items: start;
}
.compare-pick .compare-side {
  padding: 28px 32px; border: 1px solid var(--ph-line);
  border-radius: var(--ph-r18); background: var(--ph-white);
}
.compare-pick .compare-side + .compare-side { border-left: 1px solid var(--ph-line); padding-left: 32px; }
.compare-pick .compare-side.on { border-color: var(--ph-accent); }
.compare-pick .compare-side .k { color: var(--ph-text-2); }
/* The material's own colour, beside its name. Same fills as the buy block's finish
   swatches — one product, one colour language. A real border rather than an inset
   shadow, for the same reason .buy-swatch has one: elevation YOK. */
.compare-pick .compare-side .k { display: flex; align-items: center; gap: 10px; }
.mat-sw {
  width: 18px; height: 18px; border-radius: 50%; background: var(--sw);
  display: inline-block; flex: 0 0 auto; box-sizing: border-box;
  border: 1px solid rgba(0,0,0,.10);
}
@media (max-width: 900px) {
  .compare.compare-pick { grid-template-columns: 1fr; }
}
.compare-side { padding: 32px 44px 0 0; }
.compare-side + .compare-side { padding-left: 44px; padding-right: 0;
                                border-left: 1px solid var(--hair); }
.compare-side .k { font-size: var(--ph-fs-12); font-weight: var(--ph-w-600);
                   letter-spacing: var(--ph-tr-12-600-caps);
                   color: var(--ph-accent-link); }
.compare-side p { margin: 14px 0 0; font-size: var(--ph-fs-19); line-height: var(--ph-lh-19-loose);
                  color: var(--ph-text-1); }
/* Some of these carry a product photograph — /devices/ compares Ceramic against
   Titanium with a picture of each — so the side has to hold an image as well as a
   sentence. */
.compare-side img { width: 100%; height: auto; margin-top: 22px; border-radius: var(--ph-r18); }
/* The price is the thing being compared, so it is set as a figure rather than as
   another line of body copy. */
.compare-side .price { margin-top: 12px; font-size: var(--ph-h3-size); line-height: var(--ph-h3-lh);
                       font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-28-600); }
a.compare-side { color: inherit; text-decoration: none; display: block;
                 transition: background var(--ph-dur-fast) var(--ph-ease); }
a.compare-side:hover { text-decoration: none; }
a.compare-side:hover .k { color: var(--ph-accent); }

.compare-side .cta-actions, .compare-side .tag { margin-top: 18px; }
/* The spec lines under a side's one-line claim. Set below the body size and in the
   secondary colour so the eye reads claim first and specification second — five
   lines at body size per side is a wall, and the point of a comparison is that both
   sides can be taken in at once. */
.compare-side .spec { margin-top: 7px; font-size: var(--ph-fs-17);
                      line-height: var(--ph-lh-17); color: var(--ph-text-2); }
.compare-side .spec:first-of-type { margin-top: 14px; }
.section-black .compare-side .spec { color: var(--ph-tertiary); }
@media (max-width: 900px) {
  .compare { grid-template-columns: 1fr; }
  .compare-side { padding: 28px 0 0; }
  .compare-side + .compare-side { padding-left: 0; border-left: 0; border-top: 1px solid var(--hair); }
}
.section-black .compare, .section-black .compare-side + .compare-side { border-color: rgba(255,255,255,.16); }
.section-black .compare-side p { color: var(--ph-surface); }
.section-black .compare-side .k { color: var(--ph-on-brand); }

/* ---- Review ----
   These shipped as <h3 class="card-title">★★★★★</h3>: five stars announced by a
   screen reader as a heading, in a glyph that renders as a different weight on
   every platform, with the name of the person who said it buried at the end of the
   sentence. A quote is a <figure> with a <blockquote> and a <figcaption>, the
   rating is one labelled image, and the stars are drawn rather than typed. */
.quote { margin: 0; padding: 32px 0 0; border-top: 1px solid var(--hair); }
.quote-stars { display: flex; gap: 3px; color: var(--ph-accent); }
.quote-stars svg { width: 15px; height: 15px; display: block; }
.quote blockquote { margin: 16px 0 0; font-size: var(--ph-fs-19); line-height: var(--ph-lh-19-loose);
                    color: var(--ph-text-1); text-wrap: pretty; }
.quote figcaption { margin-top: 16px; font-size: var(--ph-fs-14); color: var(--ph-text-2); }
.section-black .quote { border-color: rgba(255,255,255,.16); }
.section-black .quote blockquote { color: var(--ph-surface); }

/* ---- Assurance list ----
   For "what's included" and "the science": a list of things that are true, or that
   you get. Several of these had the tick typed into the heading — "✓ Clinical
   genetics guidelines" — and the supporting line opened with a typed em dash. A
   mark that means something belongs to the component, not to the copy, or the day
   someone edits that line the tick goes with it. */
/* ---- Bulleted card body ----
   Two pages wrote a card's three points into one paragraph with the bullets typed
   into the running text: "•  Getting ahead of genetic risks before they matter •
   Understanding your complete genetic health picture • …". Set as a sentence it
   reads as a run-on with loose glyphs in it. The list is real; only its markup was
   missing. Hanging marker, so the second line of a point lines up under the first. */
.bullets { list-style: none; margin: 10px 0 0; padding: 0; }
.bullets li { position: relative; padding-left: 18px; margin-top: 8px;
              font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
              color: var(--ph-text-2); }
.bullets li:before { content: "•"; position: absolute; left: 2px; top: -1px;
                     color: var(--ph-accent); font-weight: var(--ph-w-600); }
.section-black .bullets li { color: var(--ph-tertiary); }
.section-black .bullets li:before { color: var(--ph-on-brand); }

.assure { list-style: none; margin: 0; padding: 0; display: grid; gap: 26px; }
.assure-2 { grid-template-columns: repeat(2, 1fr); column-gap: var(--ph-gutter); }
.assure-3 { grid-template-columns: repeat(3, 1fr); column-gap: var(--ph-gutter); }
.assure-4 { grid-template-columns: repeat(4, 1fr); column-gap: var(--ph-gutter); }
.assure li { display: grid; grid-template-columns: 24px 1fr; gap: 14px; align-items: start; }
.assure svg { width: 20px; height: 20px; margin-top: 2px; color: var(--ph-accent); }
.assure b { display: block; font-size: var(--ph-fs-19); font-weight: var(--ph-w-600);
            line-height: var(--ph-lh-19); letter-spacing: var(--ph-tr-19-400); }
.assure span, .assure p { display: block; margin: 6px 0 0; font-size: var(--ph-fs-17);
                          line-height: var(--ph-lh-17); color: var(--ph-text-2); }
.assure a { color: var(--ph-accent-link); }
@media (max-width: 900px) { .assure-2, .assure-3, .assure-4 { grid-template-columns: 1fr; } }
/* Both of these replace a .grid that carried an inline margin-top:40px, and
   replace_with does not carry an inline style across — so where the section has no
   lede between its heading and the list, the first row sat directly under the h2 with
   only the heading's own bottom margin. Four pages were already shipping like that
   before this rule existed (three /store/ product pages and ring water-resistance).
   Keyed on the adjacency rather than set on the component, because every other
   instance follows a lede <p> whose own spacing is already correct. */
:is(h1, h2, h3, p) + .assure,
:is(h1, h2, h3, p) + .compare { margin-top: 40px; }
/* Both replace a .grid that carried an inline margin-top:40px, and replace_with does
   not carry an inline style across — so wherever the section had no lede, or the
   lede's own bottom margin was small, the component opened straight under the text
   above it. Measured at 1440 across every instance on the site before this rule: the
   gap above ul.assure was 0px ×9, 14px, 17px ×2, 18px ×3, 40px ×5, 44px — six
   different answers to one question, and the 0px cases put the first tick row on the
   lede's descenders. One value, matching the 40px the grid utility used. */
.section-black .assure b { color: var(--ph-white); }
.section-black .assure span { color: var(--ph-tertiary); }
.section-black .assure svg { color: var(--ph-on-brand); }

/* ---- Full-bleed band ----
   The breath between two arguments. Every other section on the site is text inside
   a 1260px container, so a page reads argument, argument, argument all the way
   down; this is the one that stops talking and lets the reader look.

   Edge to edge on purpose — it is the only element that leaves the container, and
   that is what makes it register as a change of register rather than another card.
   No CTA, deliberately: a band that sells is just a hero in the wrong place.

   object-fit: cover means the rendered box will not match the file's aspect, which
   is why audit_layout skips cover/contain images in its stretched check — the box
   is doing the cropping, the pixels are not being squashed. Height is fluid so the
   crop tightens on a phone instead of leaving a letterbox.

   The scrim is a gradient over the photograph, not a surface tone, so it is outside
   the neutral ramp the tokens forbid filling in. */
.band { padding: 0; position: relative; overflow: hidden; }
.band img {
  width: 100%; height: clamp(400px, 48vw, 680px);
  object-fit: cover; display: block;
}
.band-copy {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 0 var(--ph-margin) clamp(36px, 5vw, 72px);
  background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.42) 62%, rgba(0,0,0,.62) 100%);
  padding-top: clamp(90px, 16vw, 200px);
}
.band-copy .inner { width: 100%; max-width: calc(var(--ph-content-max) + var(--ph-margin) * 2);
                    margin-inline: auto; }
.band-copy h2 { color: var(--ph-white); margin: 0; text-wrap: balance;
                font-size: var(--ph-h2-size); line-height: var(--ph-h2-lh);
                font-weight: var(--ph-w-600); letter-spacing: var(--ph-h2-tr); }
.band-copy p { margin: 12px 0 0; max-width: 46ch; color: rgba(255,255,255,.82);
               font-size: var(--ph-fs-19); line-height: var(--ph-lh-19); }

/* ---- Plate ----
   A band is a breath between two arguments; it is 680px tall and sits inside the same
   260px of section padding as everything else, which is why 21 of them read as a
   rhythm rather than as a moment. A plate is the page stopping. It is the same
   component — same crop, same settle, same "B" to the rhythm pass, and the same
   absolute ban on copy over it — given a real height and, more importantly, given the
   type on either side of it leaning in. The two rules under the height are the ones
   that do the work: at full section padding a taller picture is just a taller
   picture, but with the join tightened to 88px the paragraph above it reads as a
   caption written before the fact, and the section under it reads as the page
   starting again.

   820 rather than 880 at 1440, deliberately: a picture that exactly fills a 900px
   viewport reads as a dead end, and one that leaves 80px of the next section showing
   reads as a page continuing. One per page. The moment stops being a moment at two.

   It is a MODIFIER on .band and must stay one. fix_section_rhythm.ground() returns
   "B" for any class list carrying the token `band`, and treats "B" as punctuation it
   refuses to touch; a standalone .plate would read to it as plain white, get
   section-gray bolted on under a full-bleed photograph, and corrupt the alternation
   downstream. It also inherits object-fit:cover — which is what makes the layout
   gate's stretched check skip it — overflow:hidden, and the settle animation.

   :has() is a progressive enhancement here on purpose: a browser without it gets the
   full 144px and a page that is merely correct, never a broken one. */
.plate img { height: clamp(440px, 58vw, 820px); }
.plate + .section, .plate + .cta-band { padding-top: clamp(56px, 5vw, 88px); }
.section:has(+ .plate) { padding-bottom: clamp(56px, 5vw, 88px); }

/* ---- Section reveal ----
   A long page whose bands are all simply *there* reads as one tall image. Settling
   each one in as it arrives is what turns the scroll into a sequence.

   Two things make this safe rather than decorative:

   1. It moves the section's CONTENTS, never the section. audit_layout compares the
      bounding boxes of consecutive siblings and calls two that share pixels an
      overlap — a translateY on <section> itself would push every page 24px into
      its neighbour and turn the whole gate red. Every section but one has exactly
      a single child wrapper, so moving the child moves the visible block and
      leaves the layout boxes untouched.

   2. Nothing is hidden until JavaScript has run and confirmed it can animate. The
      .ph-reveal class is added by shared.js only after it has checked for
      IntersectionObserver and for prefers-reduced-motion, so a browser without
      either — or a reader who asked for stillness — is served the page painted,
      not a page waiting for a script that will never arrive. Content first. */
html.ph-reveal main > section.ph-r > * {
  opacity: 0; transform: translate3d(0, 24px, 0);
  transition: opacity var(--ph-dur-reveal) var(--ph-ease),
              transform var(--ph-dur-reveal) var(--ph-ease);
}
html.ph-reveal main > section.ph-in > * { opacity: 1; transform: none; }

/* The band is a single image filling a fixed-height, overflow:hidden box. Sliding
   it up would show a strip of background along the top edge and clip the same
   amount off the bottom — a parallax nobody asked for. It fades only. */
html.ph-reveal main > section.band.ph-r > * { transform: none; }

/* ---- Stat figures ---- */
.stat-num { font-size: var(--ph-h2-size); line-height: var(--ph-h2-lh);
            font-weight: var(--ph-w-600); letter-spacing: var(--ph-h2-tr); }

/* ---- Accordion ---- */
.acc { border-top: 1px solid var(--hair); }
.acc-item { border-bottom: 1px solid var(--hair); }
.acc-item summary {
  list-style: none; cursor: pointer; display: flex; justify-content: space-between;
  align-items: center; padding: 20px 0; font-size: var(--ph-fs-19);
  line-height: var(--ph-lh-19);
  font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-19-400);
}
.acc-item summary::-webkit-details-marker { display: none; }
.acc-item summary:after { content: "+"; font-size: var(--ph-fs-24); font-weight: var(--ph-w-400);
                          color: var(--ph-tertiary); transition: transform var(--ph-dur-fast) var(--ph-ease); }
.acc-item[open] summary:after { transform: rotate(45deg); }
.acc-item .acc-body { padding: 0 0 22px; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
                      color: var(--ph-text-2);
                      /* was var(--ph-read-max) — 980px of 17px text is 119 characters */
                      max-width: var(--ph-measure); }

/* ---- Questions ----------------------------------------------------------
   ONE component for every question block on the site (2026-09-16). Before
   this there were three: the hairline list above, a tabbed two column panel
   on devices/band and store/gut-microbiome, and a card stack on app/ and
   supplements/nad. A reader met a different control on every page.

   What it is made of, and where each part came from:
     · the heading is the Band page's Questions heading, at its size, weight,
       tracking and ink, centred over the column;
     · the type inside is the Band's too, 18/600 for the question and 17/1.6
       for the answer;
     · the arrangement is the app page's, a single centred column of cards,
       stacked, each one opening in place.
   Only ever one card is open: shared.js closes the others, and animates the
   close rather than letting it snap. See `faq()` there.

   `.acc` on its own is untouched, because it is still the right control for
   a list that is not a question, such as the bacteria dictionaries on the
   microbiome report pages. A question block opts in with `.is-faq`.
   ---------------------------------------------------------------------- */
.faq-h {
  margin: 0;
  text-align: center;
  font-size: clamp(44px, 5vw, 64px);
  line-height: 1.05;
  font-weight: 600;
  letter-spacing: -0.03em;
  color: var(--ink-900);
  text-wrap: balance;
}

/* What a question block may carry besides the list, all on the same centre
   line: an eyebrow above the heading, one sentence of lede under it, and a
   way out under the last card for the reader whose question is not there. */
.faq-eyebrow { text-align: center; }
.faq-lede {
  max-width: 560px;
  margin: 18px auto 0;
  text-align: center;
  font-size: 17px;
  line-height: 1.6;
  color: var(--ink-600);
}
.faq-note {
  max-width: 760px;
  margin: 26px auto 0;
  text-align: center;
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink-600);
}
.faq-note b { font-weight: 600; color: var(--ink-900); }
.faq-note a { margin-left: 4px; font-weight: var(--ph-w-500, 500); color: var(--ph-accent, #203a85); white-space: nowrap; }

.acc.is-faq {
  display: grid;
  gap: 10px;
  max-width: 760px;
  margin: clamp(34px, 4vw, 54px) auto 0;
  border-top: 0;
}
.acc.is-faq .acc-item {
  border-bottom: 0;
  border-radius: 12px;
  background: var(--gl-tint-sunk);
  transition: background var(--ph-dur-base) var(--ph-ease);
}
/* On the tinted sections the recess tint IS the ground, so the card has to
   come forward instead of sinking. */
.section-gray .acc.is-faq .acc-item { background: var(--ph-white); }
.acc.is-faq .acc-item:hover { background: #e9edf7; }
.section-gray .acc.is-faq .acc-item:hover { background: #fbfcfe; }

.acc.is-faq .acc-item summary {
  gap: 20px;
  align-items: flex-start;
  padding: 18px 22px;
  font-size: 18px;
  line-height: 1.35;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink-900);
}
/* The marker is drawn from two gradients rather than typed, so it can go from
   a plus to a minus. A rotated "+" only ever becomes a cross, which reads as
   "dismiss", not "collapse". */
.acc.is-faq .acc-item summary:after {
  content: '';
  flex: none;
  width: 14px;
  height: 14px;
  margin-top: 5px;
  color: var(--ink-400);
  background:
    linear-gradient(currentColor, currentColor) center / 100% 1.5px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1.5px 100% no-repeat;
  transform: none;
  transition: background-size var(--ph-dur-base) var(--ph-ease), color var(--ph-dur-base) var(--ph-ease);
}
.acc.is-faq .acc-item[open] summary:after { background-size: 100% 1.5px, 1.5px 0; color: var(--ink-900); }

/* The answer is sized by its grid row, so it can open and close from nothing.
   The padding sits one level in, on the paragraph, because a padded box in a
   0fr row is still as tall as its padding. */
.acc.is-faq .acc-body {
  display: grid;
  grid-template-rows: 0fr;
  padding: 0;
  max-width: none;
  transition: grid-template-rows 420ms var(--ph-ease);
}
.acc.is-faq .acc-item[open] .acc-body { grid-template-rows: 1fr; }
.acc.is-faq .acc-body > div { overflow: hidden; min-height: 0; }
.acc.is-faq .acc-body p {
  margin: 0;
  padding: 0 22px 20px;
  max-width: 62ch;
  font-size: 17px;
  line-height: 1.6;
  color: var(--ink-600);
}

@media (max-width: 600px) {
  .acc.is-faq .acc-item summary { padding: 16px 18px; font-size: 17px; }
  .acc.is-faq .acc-body p { padding: 0 18px 18px; font-size: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .acc.is-faq .acc-body { transition: none; }
}

/* ---- Reviews --------------------------------------------------------------
   One reviews section for every product page, lifted whole from
   supplements/nad (2026-09-23, user rule: every reviews section takes this
   layout and behaviour). The two tone heading, the score with its
   distribution bars, search, popular topics, sort, the three column review
   rows with a helpful vote, and the numbered pager. Behaviour is shared.js
   `reviews()`; the markup it reads is
     section.rv-sec > .rv-wrap > h2.ph-display.is-ink + .rv-score + .rv-tools
       + [data-rv-list] > article.rv[data-date][data-rating][data-helpful][data-topics]
       + nav[data-rv-pager]
   Every size and colour here is the NAD page's, token for token. */
.rv-sec {
  --rv-navy: var(--phenome-blue-900, #0a1e57);
  --rv-accent: var(--phenome-blue-500, #203a85);
  --rv-soft: var(--phenome-blue-100, #e6e9f2);
  --rv-sunk: var(--gl-tint-sunk, #f4f6fb);
  --rv-rule: #203a851f;
  --rv-ink: var(--ph-text-1, #10214b);
  --rv-ink-2: var(--ph-text-2, #4a5578);
  --rv-ease: var(--ease-frosted-out, cubic-bezier(.16, 1, .3, 1));
  background: #fafbfd;
  padding-block: clamp(72px, 8vw, 128px);
}
.rv-wrap {
  width: 100%; margin-inline: auto;
  max-width: calc(var(--ph-content-max, 1260px) + var(--ph-margin, 90px) * 2);
  padding-inline: var(--ph-margin, 90px);
}
.rv-sec .ph-display { font-size: clamp(40px, 5.6vw, 84px); }
.rv-score { display: flex; justify-content: center; align-items: center; gap: clamp(28px, 4vw, 56px); margin-top: 40px; flex-wrap: wrap; }
.rv-score-big { display: flex; align-items: center; gap: 12px; }
.rv-score-big b { font-size: 64px; line-height: 1; font-weight: 500; letter-spacing: -.03em; color: var(--rv-navy); }
.rv-score-big > span { display: grid; gap: 2px; font-size: 15px; color: var(--rv-ink); }
.rv-score-big .supp-stars { font-size: 22px; }
.rv-dist { margin: 0; padding: 0 0 0 clamp(28px, 4vw, 56px); border-left: 1px solid var(--rv-rule); }
.rv-dist li { list-style: none; display: grid; grid-template-columns: 34px 160px 40px; gap: 12px; align-items: center; font-size: 16px; line-height: 25px; color: var(--rv-ink); }
.rv-dist i { position: relative; height: 6px; border-radius: 6px; background: var(--rv-soft); overflow: hidden; }
.rv-dist i::after {
  content: ""; position: absolute; inset: 0 auto 0 0; width: var(--w); border-radius: 6px; background: var(--rv-navy);
  transform-origin: left; transform: scaleX(0); transition: transform 1.1s var(--rv-ease);
}
.rv-dist.is-in i::after { transform: scaleX(1); }
.rv-tools { margin-top: 56px; }
.rv-search {
  display: flex; align-items: center; gap: 10px; width: min(100%, 360px); height: 44px; padding: 0 14px;
  border: 1px solid var(--ph-line, #203a8529); border-radius: 10px; background: #fff;
}
.rv-search:focus-within { border-color: var(--rv-accent); }
.rv-search svg { width: 17px; height: 17px; fill: none; stroke: var(--rv-ink-2); stroke-width: 1.6; stroke-linecap: round; flex: none; }
.rv-search input { flex: 1; min-width: 0; border: 0; outline: 0; background: none; font-size: 15px; color: var(--rv-ink); }
.rv-topics-h { margin-top: 22px; font-size: 15px; font-weight: 600; color: var(--rv-navy); }
.rv-row { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; margin-top: 12px; padding-bottom: 24px; border-bottom: 1px solid var(--rv-rule); }
.rv-topics { display: flex; flex-wrap: wrap; gap: 10px; }
.rv-topics button {
  height: 36px; padding: 0 16px; border-radius: 8px; border: 1px solid var(--ph-line, #203a8529);
  background: #fff; font: inherit; font-size: 16px; color: var(--rv-ink); cursor: pointer;
  transition: background .2s, color .2s, border-color .2s;
}
.rv-topics button:hover { border-color: var(--rv-accent); }
.rv-topics button[aria-pressed="true"] { background: var(--rv-navy); border-color: var(--rv-navy); color: #fff; }
.rv-sort { display: inline-flex; align-items: center; gap: 6px; font-size: 16px; color: var(--rv-ink); }
.rv-sort select { border: 0; background: none; font: inherit; font-weight: 600; color: var(--rv-navy); cursor: pointer; padding: 6px 2px; }

.rv {
  display: grid; grid-template-columns: 180px minmax(0, 1fr) auto; gap: 8px 24px;
  padding: 32px 0; border-bottom: 1px solid var(--rv-rule);
  animation: rv-in .5s var(--rv-ease) both;
}
@keyframes rv-in { from { opacity: 0; translate: 0 8px; } }
/* `display: grid` above outranks the UA's [hidden] rule, so without this the
   topic filter and the pager hide nothing. */
.rv[hidden] { display: none; }
.rv-who { display: grid; align-content: start; gap: 3px; font-size: 16px; color: var(--rv-ink-2); }
.rv-who b { font-size: 16px; color: var(--rv-navy); }
.rv-verified { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px; font-size: 16px; color: var(--rv-ink); }
.rv-verified svg {
  width: 16px; height: 16px; padding: 2px; border-radius: 50%; background: var(--rv-navy); color: #fff;
  fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round;
}
.rv-main p { margin: 0; }
.rv-main .supp-stars { font-size: 18px; }
.rv-star-off { color: #d5dbea; }
.rv-main p + p { margin-top: 10px; font-size: 17px; line-height: 1.6; color: var(--rv-ink); max-width: 72ch; }
.rv time { font-size: 16px; color: var(--rv-ink); }
.rv-help { grid-column: 2 / -1; justify-self: end; display: flex; align-items: center; gap: 14px; margin: 0; font-size: 16px; color: var(--rv-ink); }
.rv-help button {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; margin: -6px -10px;
  border: 0; border-radius: 8px; background: none; font: inherit; font-size: 16px; color: var(--rv-ink); cursor: pointer;
}
.rv-help button:hover { background: var(--rv-sunk); }
.rv-help svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linejoin: round; transition: scale .3s var(--rv-ease); }
.rv-help button[aria-pressed="true"] { color: var(--rv-navy); font-weight: 600; }
.rv-help button[aria-pressed="true"] svg { fill: var(--rv-soft); scale: 1.15; }
.rv-empty { padding: 48px 0; text-align: center; color: var(--rv-ink-2); }

/* The pager: the current page is the one filled disc, the rest are bare
   figures that take a tint on hover. Every target is 44px. */
.rv-pager { display: flex; justify-content: center; align-items: center; gap: 6px; margin-top: 40px; }
.rv-pager[hidden] { display: none; }
.rv-pg {
  display: grid; place-items: center; width: 44px; height: 44px; padding: 0;
  border: 0; border-radius: 50%; background: none; cursor: pointer;
  font: inherit; font-size: 16px; line-height: 1; font-weight: 500;
  font-variant-numeric: tabular-nums; color: var(--rv-ink-2);
  transition: background .25s var(--rv-ease), color .25s var(--rv-ease);
}
.rv-pg:hover:not(:disabled):not([aria-current]) { background: var(--rv-soft); color: var(--rv-navy); }
.rv-pg[aria-current="page"] { background: var(--rv-navy); color: #fff; cursor: default; }
.rv-pg:focus-visible { outline: 2px solid var(--rv-accent); outline-offset: 2px; }
.rv-pg-step { color: var(--rv-navy); }
.rv-pg-step:first-child { margin-right: 10px; }
.rv-pg-step:last-child { margin-left: 10px; }
.rv-pg-step:disabled { opacity: .28; cursor: default; }
.rv-pg-step svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }

@media (max-width: 1024px) {
  .rv { grid-template-columns: minmax(0, 1fr) auto; }
  .rv-main { grid-column: 1 / -1; grid-row: 2; }
  .rv time { grid-column: 2; grid-row: 1; }
  .rv-help { grid-column: 1 / -1; }
}
@media (max-width: 760px) {
  .rv-dist { padding-left: 0; border-left: 0; }
}
@media (max-width: 560px) {
  .rv-dist li { grid-template-columns: 34px minmax(0, 1fr) 40px; }
  .rv-dist { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
  .rv { animation: none; }
  .rv-dist i::after { transition: none; }
}

/* ---- Trust strip ---- */
.trust { display: flex; justify-content: center; gap: 40px; flex-wrap: wrap; padding: 28px 0;
         border-top: 1px solid var(--hair); border-bottom: 1px solid var(--hair);
         font-size: var(--ph-fs-14); color: var(--ph-text-2); }
.trust span { display: flex; align-items: center; gap: 8px; }
.trust b { color: var(--ph-text-1); font-weight: var(--ph-w-600); }
/* The footer's copy of the strip starts on the footer's own left line. Centred, it
   opened a fourth vertical line on every page against a rule of three or four for the
   whole page — and below 935px it wrapped 3+1, leaving one centred orphan under three
   centred items. Scoped to .ph-footer on purpose: the in-page strips on the two
   checkout pages sit full-width under a hero and were measured as centred there. */
.ph-footer .trust { justify-content: flex-start; }

/* ---- Breadcrumb ---- */
.crumb { padding: 18px 0 0; font-size: var(--ph-fs-14); color: var(--ph-text-2); }
.crumb a { color: var(--ph-text-2); }

/* ---- Footer ---- */
.ph-footer { background: var(--ph-surface); color: var(--ph-text-2);
             font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); padding: 56px 0 44px; }
.ph-footer .cols { display: grid; grid-template-columns: repeat(5, 1fr); gap: 40px; }
@media (max-width: 900px) { .ph-footer .cols { grid-template-columns: repeat(2, 1fr); } }
.ph-footer .col-t { font-weight: var(--ph-w-600); color: var(--ph-text-1); margin-bottom: 12px; }
.ph-footer .col a { display: block; color: var(--ph-text-2); margin-bottom: 8px; }
.ph-footer .rule { margin: 34px 0 20px; height: 1px; background: var(--ph-line); }
/* --ph-measure, not --ph-read-max: 980px of 14px type is 145 characters on a line —
   the longest line on the site, on every page, against a 45-75 rule. .acc-body was
   migrated off --ph-read-max for exactly this reason and this element was the one the
   migration missed. Measured after: 477.6px, about 71 characters, and it caps at 800
   too, where the old box still gave 109. */
.ph-footer .legal { max-width: var(--ph-measure); margin: 0 0 18px; }
.ph-footer .bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }

/* ---- REGION, in the footer, 2026-09-22 by request ------------------------
   First drawn in the nav as a round flag pill, then moved
   down here the same day: the disclaimer row's right end was the footer's one
   empty stretch. A grey "Shopping from" over a white pill (flag, full country
   name, chevron); the menu opens UPWARD, because below it is the page's end.
   UI ONLY, see regionKit() in shared.js. */
.ph-footer .foot-legal-row {
  display: flex; align-items: flex-end; justify-content: space-between;
  flex-wrap: wrap; gap: 24px 48px; margin: 0 0 22px;
}
.ph-footer .foot-legal-row .legal { margin: 0; }
.ph-region { position: relative; flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
.ph-region-k { font-size: 15px; line-height: 20px; color: var(--ph-text-2); }
.ph-region-btn {
  display: inline-flex; align-items: center; gap: 9px;
  height: 42px; padding: 0 14px 0 9px; border-radius: 99px;
  border: 1px solid var(--ph-line); background: var(--ph-white);
  color: var(--ph-text-1); cursor: pointer;
  font: inherit; font-size: 15px; line-height: 1; font-weight: 600; letter-spacing: normal;
  transition: border-color .2s ease, box-shadow .2s ease;
}
.ph-region-btn:hover,
.ph-region.is-open .ph-region-btn {
  border-color: color-mix(in srgb, var(--ph-accent) 45%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--ph-accent) 8%, transparent);
}
.ph-region-btn:focus-visible { outline: 2px solid var(--ph-accent-link); outline-offset: 2px; }
.ph-region-chev {
  width: 12px; height: 12px; margin-left: 2px; color: var(--ph-text-2);
  transition: transform .3s cubic-bezier(.2, .8, .2, 1);
}
.ph-region.is-open .ph-region-chev { transform: rotate(180deg); }
.ph-flag {
  flex: none; display: block; width: 24px; height: 24px; border-radius: 50%;
  overflow: hidden; box-shadow: 0 0 0 1px rgba(10, 30, 87, .1);
}
/* A choice lands with the new flag turning in, so the swap is seen, not missed. */
.ph-region.is-changed .ph-region-btn .ph-flag { animation: ph-flag-in .55s cubic-bezier(.2, .9, .25, 1.2); }
@keyframes ph-flag-in {
  from { transform: rotate(-90deg) scale(.4); opacity: 0; }
  to   { transform: none; opacity: 1; }
}
.ph-region-menu {
  position: absolute; bottom: calc(100% + 10px); right: 0; z-index: 60;
  width: 288px; padding: 8px; border-radius: 18px;
  background: rgba(255, 255, 255, .96);
  -webkit-backdrop-filter: blur(22px) saturate(160%); backdrop-filter: blur(22px) saturate(160%);
  box-shadow: 0 0 0 1px rgba(10, 30, 87, .08), 0 -2px 10px rgba(10, 30, 87, .04),
              0 24px 48px -16px rgba(10, 30, 87, .3);
  color: var(--ph-text-1);
  opacity: 0; transform: translateY(8px) scale(.97); transform-origin: bottom right;
  transition: opacity .18s ease, transform .26s cubic-bezier(.2, .8, .2, 1);
}
.ph-region.is-open .ph-region-menu { opacity: 1; transform: none; }
.ph-region-h { padding: 8px 10px 10px; font-size: 15px; line-height: 20px; color: var(--ph-text-2); }
.ph-region-opt {
  display: grid; grid-template-columns: 28px minmax(0, 1fr) 20px; align-items: center; gap: 12px;
  width: 100%; padding: 10px; border: 0; border-radius: 12px;
  background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer;
  transition: background-color .15s ease;
}
.ph-region-opt .ph-flag { width: 28px; height: 28px; }
.ph-region-opt:hover,
.ph-region-opt:focus-visible { background: #f1f4fa; outline: none; }
.ph-region-opt[aria-checked="true"] { background: #e8effb; }
.ph-region-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ph-region-name { font-size: 16px; line-height: 21px; font-weight: 600; }
.ph-region-money { font-size: 15px; line-height: 20px; color: var(--ph-text-2); }
.ph-region-tick { width: 20px; height: 20px; color: var(--ph-accent); opacity: 0; transform: scale(.6);
                  transition: opacity .2s ease, transform .25s cubic-bezier(.2, .9, .25, 1.3); }
.ph-region-opt[aria-checked="true"] .ph-region-tick { opacity: 1; transform: none; }
/* When the row stacks, the switcher follows the text to the left edge. */
@media (max-width: 760px) {
  .ph-footer .foot-legal-row { align-items: flex-start; }
  .ph-region { align-items: flex-start; }
  .ph-region-menu { right: auto; left: 0; transform-origin: bottom left;
                    width: min(288px, calc(100vw - 48px)); }
}
@media (prefers-reduced-motion: reduce) {
  .ph-region-menu, .ph-region-tick, .ph-region-chev { transition: none; }
  .ph-region.is-changed .ph-region-btn .ph-flag { animation: none; }
}

/* ---- CTA band ---- */
.cta-band { padding: var(--ph-sec-top) 0 var(--ph-sec-bottom); text-align: center; }
/* .section-tight is declared earlier in the file, so on a .cta-band the rule above
   simply won than it. A scaffold's closing band has one line and two buttons; at full
   section padding it is the tallest thing on a page that has nothing to say. */
.cta-band.section-tight { padding: calc(var(--ph-sec-top) / 2) 0 calc(var(--ph-sec-bottom) / 2); }
.cta-band h2 { margin: 0; font-size: var(--ph-h2-size); line-height: var(--ph-h2-lh);
               font-weight: var(--ph-w-600); letter-spacing: var(--ph-h2-tr); text-wrap: balance; }
/* --ph-lead-lh, not the body's box. This rule declared a lead SIZE and no line-height,
   so the closing paragraph of 128 pages sat 21px in a 21px box — a ratio of 1.000,
   below even the booklet's 1.05 floor for a heading, on a paragraph. It is the one the
   new design-language gate caught on its first run, which is what that gate is for.
   --ph-lead-size and --ph-lead-lh already move together at every width. */
.cta-band p { margin: 16px 0 0; font-size: var(--ph-lead-size);
              line-height: var(--ph-lead-lh); color: var(--ph-text-2); }
.cta-actions { margin-top: 34px; display: flex; align-items: center; justify-content: center;
               gap: 24px; flex-wrap: wrap; }

/* ---- Quiz band ---- */
.quiz-band { background: var(--ph-surface); border-radius: var(--ph-r27); padding: 44px 48px;
             display: flex; align-items: center; justify-content: space-between; gap: 32px; flex-wrap: wrap; }
.quiz-band .t { font-size: var(--ph-fs-24); font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-24-600); }
.quiz-band p { margin: 6px 0 0; font-size: var(--ph-fs-17); color: var(--ph-text-2); }

/* ---- Dark-section overrides ---- */
.section-black .card, .section-black .card-gray { background: #1c1c1e; color: var(--ph-surface); }
.section-black .card-title, .section-black .card h3, .section-black .card-gray h3 { color: var(--ph-white); }
.section-black .body-text, .section-black .card p, .section-black .card-gray p { color: var(--ph-tertiary); }
.section-black .eyebrow { color: var(--ph-on-brand); }
.section-black .acc, .section-black .acc-item { border-color: rgba(255,255,255,.16); }
.section-black .acc-item summary { color: var(--ph-white); }
.section-black .acc-item .acc-body { color: var(--ph-tertiary); }
.section-black table td { border-color: rgba(255,255,255,.16) !important; color: var(--ph-surface); }
.section-black .tag { border-color: rgba(255,255,255,.28); color: var(--ph-surface); }
.section-black a { color: var(--ph-on-brand); }
.section-black .btn.ghost { background: transparent; color: var(--ph-white) !important;
                            border-color: rgba(255,255,255,.4); }
/* FOUR classes, and that is the whole point of the rule. `.section-black .btn.ghost` and
   `.btn.ghost:hover` are BOTH (0,3,0) — a TIE, decided by source order, and this block is
   the later one, so its `background: transparent` was quietly beating the hover fill. The
   three ghost buttons inside the black section on /store/cart/ therefore did nothing at
   all under the cursor. Found 2026-08-20 by an adversarial check of the hover fix, not by
   a gate; it is the exact shape CLAUDE.md already records for `.atile-film` and for
   `.atile-pic--inset`, arriving a third time. Do not "simplify" this back to three
   classes, and do not reorder the file to fix it — the next edit would move it back. */
.section-black .btn.ghost:hover  { background: var(--ph-accent-hover); border-color: transparent; }
.section-black .btn.ghost:active { background: var(--ph-accent-active); border-color: transparent; }
.section-black .hero-fact .v { color: var(--ph-white); }

/* ---- Motion ----
   The site had six transitions in seven hundred lines. Everything below the film
   was, in the literal sense, a still: nothing moved when you pointed at it, nothing
   acknowledged a press, and a photograph arrived in exactly the state it would stay
   in forever. That reads as dead even when every page is correct.

   The vocabulary here is deliberately narrow, because the token set forbids the
   usual one: `elevation YOK` — the measured system has no shadow scale, so
   nothing may lift, glow or drop a shadow to feel alive. What is left is scale,
   position and opacity, which is enough, and which is the reason this reads as
   restraint rather than as a theme.

   All of it is switched off wholesale by the reduced-motion block below. */

/* A picture inside a clipped box is the one element that can move without moving
   anything else — the box holds the layout, the image drifts inside it. This is the
   single gesture that does most of the work. */
.card-img img, .hero-img img { transition: transform 620ms var(--ph-ease); }
a:hover .card-img img,
a.card-gray:hover .card-img img { transform: scale(1.035); }

/* The chevron is already the promise of movement; it should keep it. */
.link-more:after { display: inline-block; transition: transform var(--ph-dur-fast) var(--ph-ease); }
.link-more:hover:after { transform: translateX(3px); }
a.card-gray .card-title:after, a.card .card-title:after {
  display: inline-block; transition: transform var(--ph-dur-fast) var(--ph-ease);
}
a.card-gray:hover .card-title:after, a.card:hover .card-title:after { transform: translateX(3px); }

/* Press feedback is a COLOUR now, and it lives on each control beside its hover. A
   control still answers the finger on touch, where there is no hover to have felt
   anything at all — it answers with --ph-accent-active instead of a scale.
   Both halves of that are deliberate:
     · no transition is declared on a button, so the tone change is instant;
     · a button's :active carries NO transform, only the press fill. An ICON
       control may scale (scale .95); a pill does not.
   What stood here was a 160ms background fade on .btn/.ph-nav-cta/.tag plus
   transform: scale(.975) on their press. They were written when this site had six
   transitions in seven hundred lines and the whole page read as dead. Bugra asked
   on 2026-08-20 for an instant button dynamic, so both are gone.
   ⚠️ This is the one change in that batch a person may want back: a fade of five
   points of green over 160ms is a judgement call, and reinstating it is one
   declaration here — the :hover and :active colours do not depend on it. */

/* The band settles rather than appears: it arrives fractionally over-size and comes
   to rest as it fades in. Held inside overflow:hidden so no edge is ever exposed,
   and slower than everything else because it is the page taking a breath. */
html.ph-reveal main > section.band.ph-r > * {
  transform: scale(1.045);
  transition: opacity var(--ph-dur-reveal) var(--ph-ease),
              transform 1100ms var(--ph-ease);
}
html.ph-reveal main > section.band.ph-in > * { transform: scale(1); }

/* The step marks come up after the copy they belong to, one behind the next, so the
   sequence reads as a sequence rather than as four things that appeared together.
   Transform only — the marks keep their layout boxes, so the overlap check the
   layout gate runs is untouched. */
html.ph-reveal .ph-r .step-mark { opacity: 0; transform: scale(.86); }
html.ph-reveal .ph-in .step-mark {
  opacity: 1; transform: none;
  /* DECIDED 2026-08-13 (Bugra): the motion durations stand as tuned. tokens
     motion.duration.entrance caps entrances at 400ms and this site runs 420 here, 620
     on the section reveal and 1100 on a band settling — measured deviations, kept on
     purpose because the longer curves are what make the page read as weight rather
     than as a snap. Recorded here so the next audit finds a decision, not a defect. */
  transition: opacity 260ms var(--ph-ease), transform 260ms var(--ph-ease);
}
html.ph-reveal .ph-in .step:nth-child(2) .step-mark { transition-delay: 70ms; }
html.ph-reveal .ph-in .step:nth-child(3) .step-mark { transition-delay: 140ms; }
html.ph-reveal .ph-in .step:nth-child(4) .step-mark { transition-delay: 210ms; }
html.ph-reveal .ph-in .step:nth-child(5) .step-mark { transition-delay: 280ms; }
html.ph-reveal .ph-in .step:nth-child(6) .step-mark { transition-delay: 350ms; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

/* A centred hub hero keeps its centred composition, so the image sits beneath the
   copy as a figure rather than becoming a second column. */
.hero-figure { margin: 56px auto 0; max-width: 980px; border-radius: var(--ph-r27);
               overflow: hidden; }   /* elevation YOK — see .hero-img above */
@media (max-width: 900px) { .hero-figure { margin-top: 36px; } }

/* ---- TODO scaffolds ------------------------------------------------------ */
/* A page that is routed but unwritten. It has to be unmistakable at a glance —
   a scaffold that reads as finished is worse than a dead link, because a dead
   link announces itself and an empty page does not. */
.todo-banner {
  margin: 20px 0 4px; padding: 12px 18px; border-radius: var(--ph-r12, 12px);
  /* Was three invented oranges (#FFF4E5 / #F0C48A / #6B4708) and 15px on a 1.5 RATIO.
     The design system has a warn set; this is the one thing on the site that is a
     warning, so it uses it. 14/18 is the ladder rung, and the leading is the fixed
     offset tokens require rather than a ratio that drifts off the grid at every size. */
  background: var(--ph-warn-bg); border: 1px solid var(--ph-warn-ink);
  color: var(--ph-warn);
  font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
}
.todo-banner strong { letter-spacing: .06em; }
@media (prefers-color-scheme: dark) {
  .todo-banner { background: #3A2A10; border-color: #7A5A20; color: #F5D9A8; }
}

/* ---- Mega menu ----------------------------------------------------------- */
/* Each panel lives inside its own .ph-item, after its trigger. Desktop takes it out
   of flow so it hangs under the bar as a card; the mobile drawer leaves it in flow
   so it reads as an accordion. One DOM, two behaviours, no duplication — and that is
   still true with the chevron between the two, because nothing here selects the
   panel as an adjacent sibling. */
/* The trigger is an <a href> — clicking Store goes to /store/. Every declaration
   below already existed for the <button> it used to be and is kept unchanged so the
   bar renders to the same pixel; the ones that read as no-ops on an anchor (border,
   background, cursor) stay because removing them is a diff nobody can check by eye.
   text-decoration is the one thing added: .ph-nav a already says none, and it is
   repeated here because the element's TYPE is the load-bearing part of this rule. */
.ph-trigger {
  font: inherit; font-size: var(--ph-fs-12); line-height: inherit;
  background: none; border: 0; padding: 0; cursor: pointer;
  color: rgba(29,29,31,.88); text-decoration: none;
  /* Measured, not guessed. Chrome's UA sheet sets letter-spacing:normal on form
     controls, so as a <button> this text rendered at 0 tracking while body's
     0.204px (--ph-tr-17-400) reached "Longevity Hub" and the CTA beside it. An
     anchor inherits instead, which widened every trigger by 0.2px per character
     and pushed the whole row 10.8px wider at 1440. Pinned to what ships. The bar
     therefore still mixes two trackings — that is a design question, and it is in
     the report, not resolved here. */
  letter-spacing: normal;
}
.ph-trigger:hover { color: var(--ph-black); }
.ph-trigger.active { color: var(--ph-accent-link); font-weight: var(--ph-w-600); }
.ph-panel[hidden] { display: none; }

/* The chevron. Once the label navigates it can no longer be the thing that expands
   the panel, so wherever a tap follows the link this is the control that opens it.
   It carries no visible word — its accessible name is the trigger's own signed-off
   label — because no copy on this site is written by a script.
   display:none is the default and the >=901px bar never overrides it, because there
   is no room. Re-measured on /hub/ 2026-08-13, after .ph-nav-inner moved onto the
   content line and the 901–1023 band took --ph-gutter for its link gap: free space
   per side is 16.5px at 901, 33.5px at 935, 46px at 960 and 30px at 1024 (the gap
   steps back to 28 there). Eight 12px chevrons are 96px before any gap, so showing
   them here squeezes the bar shut across its whole lower range. A wide TOUCH screen
   gets the first-tap-opens path in shared.js instead, which costs no width at all.
   The superseded figures were 0px at 901, 29px at 940, 106px at 1024. */
.ph-toggle {
  display: none;
  background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--ph-text-2);
}
/* Rotation only — scale, position and opacity are the whole motion vocabulary here
   and tokens.json records elevation YOK, so nothing may lift or glow. A transform
   moves no layout box, which is also what keeps audit_layout clean. */
.ph-toggle svg { display: block; transition: transform var(--ph-dur-fast) var(--ph-ease); }
.ph-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }

@media (min-width: 901px) {
  /* 32, matching the base rule — with the links' own 8px padding that is 48px between
     labels, against a target of 46. It sat at 28 here while the base rule said
     something else, and the base declaration never won above 900px, which is every
     desktop: that mismatch is why our row read as a cluster rather than a spread.
     Two declarations of one number is how the first one goes stale unnoticed — and this
     is the second time in one session that this exact media query was the reason a
     change appeared not to work. */
  .ph-links { gap: 32px; }
  .ph-item { position: static; display: flex; align-items: center; }
  /* The panel's EDGES sit on the page's content edges, so its card lines up with the
     headline under it. 100% here is .ph-nav's width — .ph-item is position:static above
     precisely so the containing block is the full-width bar — which is the same width
     .wrap resolves its percentages against, so this is the content BOX of .wrap-wide
     expressed for an absolutely positioned box: the container's width less two margins,
     capped at the content max, then centred by the translate. Was
     `min(1180px, 92vw)`, which is 40px inboard of the content at 1440 and 23px outboard
     at 1024. Widest panel measured 781px of content, so 805px — the narrowest this ever
     computes, at a 901px viewport — clears it with 24px to spare and no child overflows. */
  /* THE PANEL IS A FULL BLEED SHEET, 2026-09-16 by request.

     It was a centred card 10px under the bar, on the page's content width, with
     a border, an 18px radius and a two part drop. What was asked for is the
     behaviour the search glass already has: the sheet reaches both edges of the
     window, the page under it is frosted rather than covered, and the two
     overlays the bar can open stop being two different objects.

     `left: 0; right: 0` resolves against .ph-nav, because .ph-item is
     position:static in this block — the same containing block that used to make
     the centring arithmetic work, doing less work now. The shadow, the border
     and the radius go with the card; a sheet that meets the window has no edge
     to raise. The ground and the blur are in phenome-glass.css beside the
     search sheet's, so the two cannot drift apart.

     The drop is the search sheet's own keyframe, so the menu and the search
     unroll at the same speed on the same curve. */
  .ph-panel {
    position: absolute; top: 100%; left: 0; right: 0; transform: none;
    width: auto; max-width: none; margin-top: 0;
    background: var(--ph-white); border: 0;
    border-radius: 0;
    box-shadow: 0 1px 0 rgba(16, 33, 75, .06);
    max-height: calc(100vh - var(--ph-nav-h)); overflow-y: auto;
    overscroll-behavior: contain;
    animation: ph-search-drop .42s cubic-bezier(.2, .8, .2, 1) both;
  }
  /* The same container formula as .ph-nav-inner, .wrap-wide and
     .ph-search-sheet-inner, so the panel's first column starts on the
     wordmark's left line and its last ends on the Clinic pill's right one. */
  .ph-panel-inner {
    width: 100%; max-width: calc(var(--ph-content-max) + var(--ph-margin) * 2);
    margin-inline: auto; padding-inline: var(--ph-margin);
    padding-block: 34px 44px;
  }
  /* The product card the client's frame draws — see the measurements at .ph-card-img
     below for where every number comes from and why it is scoped to this block.
     One class is enough here: `.ph-links a`'s padding and hairline are declared in the
     ≤900 drawer block, so nothing outranks this at these widths. */
  .ph-card { background: var(--ph-surface); border-radius: var(--ph-r12);
             padding: 14px; text-align: center; }
}
/* The one place the bar cannot afford the page's own margin, and it is measured, not
   guessed. The row — wordmark, nine links, Clinic — needs 836px at gap:28. Between 901
   and 1023 the page margin is 48, so the content line gives viewport − 96: 805px at 901,
   836px only at 932. Below that "Longevity Hub" wraps to a second line inside a 44px
   bar. That break is NOT new — `min(1180px, 92vw)` reached 836 at 909, so 901–908 wraps
   on the shipped site today; putting the bar on the content line would have widened it
   to 901–931. Closing it in the container would mean taking the nav back off the
   content line, which is the defect. So the row gives up 8px of gap instead, and only
   in the band where the margin steps down: --ph-gutter is 20, tokens' own gutter atom,
   and it takes the row's requirement to 780px — which fits from 876 up, i.e. with 25px
   of slack at the narrowest viewport this bar is ever shown at. Above 1023 the gap
   stays 28. */
@media (min-width: 901px) and (max-width: 1023px) {
  .ph-links { gap: var(--ph-gutter); }
}
/* `ph-panel-in` is gone with the centred card it animated: it faded a box in
   from translate(-50%, -6px), and a full bleed sheet is neither centred by a
   transform nor something that should appear from nowhere. The sheet uses
   `ph-search-drop` (declared with the search overlay below) and its contents
   follow a beat behind on `ph-search-rise`, which is the same pair the search
   glass opens with. */
.ph-panel-eyebrow,
.ph-panel-cards,
.ph-panel-cols { animation: ph-search-rise .5s cubic-bezier(.2, .8, .2, 1) both; }
.ph-panel-cards { animation-delay: .06s; }
.ph-panel-cols { animation-delay: .1s; }
@media (prefers-reduced-motion: reduce) {
  .ph-panel, .ph-panel-eyebrow, .ph-panel-cards, .ph-panel-cols { animation: none; }
}

.ph-panel-eyebrow {
  font-size: var(--ph-fs-12); letter-spacing: .08em;
  color: var(--ph-text-2); margin-bottom: 18px;
}

/* Product cards. §6b: a test may be explored anywhere and bought only on its own
   product page, so these link to the product and carry no purchase control. */
.ph-panel-cards {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 14px; margin-bottom: 26px;
}
/* This shipped with NO CARD: computed background rgba(0,0,0,0), radius 0, padding 0 — the
   card's whole identity had slid onto the image wrap, which was wearing the card's #F5F5F7
   and the card's 12px radius while the card itself was nothing but a stack of three spans.
   The client's own frame draws it. Measured off docs/design-frames/store-panel.png, which
   renders its 1360px frame 1:1 inside 46px of padding, so these are frame pixels:

     card   238 × 150 · ground #F5F5F7 · radius 12 · padding 14 on all four sides
     well   210 × 76  · radius 6 · runs the card's full inner width · own ground
     row    5 cards, 14px gaps, 56px in from the panel's edges
     copy   name then price, both centred on the card's axis

   Cross-checked against testing-panel.png, which is the same component six-up: the card
   is 196 wide there and the padding is still 14, the radius still 12, the well still 76
   tall and still full inner width, the gap still 14. So 14/12/6 are the constants and the
   width is whatever the container gives — which is what `auto-fit, minmax(150px, 1fr)`
   already does, and its 14px gap already matches the frame exactly.

   The well's ground is --ph-pill, not the frame's literal #EEF1F9. That value is a Figma
   image placeholder tint and is in no token; the ladder's own answer for a white panel is
   $invert — "Sayfa beyazsa merdiven ters döner: L0 white · L1 surface · L2 pill" — which
   is exactly this stack, and it puts the well one ramp step from the card as $minDelta
   requires. The frame's placeholder is a rectangle; ours holds a real 3:2 kit photograph,
   so the well keeps the photograph's own ratio rather than the placeholder's 2.76:1. That
   is the one place the geometry departs from the frame, and cropping a kit is what §8.3
   and the tile's "görsel kırpılmaz" both forbid.

   No shadow and no border. The border it used to carry — rgba(0,0,0,.07) on the image
   wrap — was a fourth separation mechanism in a system that has three, and with the card
   now painted the well needs none of them: #E8E8ED on #F5F5F7 is the separation. The
   hover moves the NAME, the way a.card-gray:hover .card-title does; the ground cannot
   move, because one step up from #F5F5F7 is #E8E8ED and that is the well's own colour.

   The card box itself is declared in the ≥901 block below, with .ph-panel's own, and the
   reason is arithmetic rather than taste. At 390 the drawer's content box is 342 and the
   longest product name — "Comprehensive" — sets a 101.3px min-content, so three cards
   carrying 14px of padding want 3 × (101.3 + 28) + 20 = 408px. Measured with the padding
   applied at every width: the three columns came out 129.3 / 105.3 / 92.7 — three sizes
   in one row, because `1fr` is minmax(AUTO, 1fr) and each track grew to its own longest
   word — and the grid's right edge sat 5px past the drawer's own content edge. Any
   padding over 3.2px overflows at three columns, and two columns clears it but adds 240px
   to the panel, ahead of the links that are what the drawer is FOR. The drawer is already
   this component's second presentation, not a narrow copy of the first — different card
   count, different gap, different link size, and .ph-panel is not even a card there — and
   the frame is a 1360px desktop frame. So the card is drawn where the frame draws it. */
.ph-card { display: block; color: var(--ph-text-1) !important; }
.ph-card:hover { text-decoration: none; }
.ph-card:hover .ph-card-name { color: var(--ph-accent-link); }
.ph-card-img {
  display: block; background: var(--ph-pill);
  border-radius: var(--ph-r6);
  overflow: hidden; margin-bottom: 10px;
}
.ph-card-img img { width: 100%; height: auto; display: block; }
/* 14/18 from the ladder, not 13px with a 1.35 ratio. Two rules broken in two lines: 13
   is not a rung, and tokens' type.lineHeightRule is a FIXED OFFSET — size + 4 up to 21,
   size + 8 above — never a ratio, because a ratio drifts off the grid at every size. */
.ph-card-name { display: block; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
                font-weight: var(--ph-w-600); }
.ph-card-price { display: block; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
                 color: var(--ph-text-2); }

/* ======================================================== scroll-pinned moment (pin)
   A film holds still while its points arrive one at a time, then the page carries on.

   HEIGHT AND STICKINESS, NEVER A WHEEL LISTENER. The outer .pin is tall — one viewport
   per point plus one to read the last one in — and .pin-stage is `position: sticky`
   inside it. The browser's own scroll runs the whole time at its own speed: the
   scrollbar tells the truth, a trackpad fling behaves, PageDown works, and nothing
   calls preventDefault on anything. A section
   that swallows wheel events is how the effect got its reputation, and it is also
   unusable with a keyboard.

   THE FRAME DOES NOT MOVE. Eyebrow, heading and the closing control are fixed inside
   the stage; only the points change. That is the point of the device — the argument
   stays put and the evidence arrives.

   THE STAGE IS THE BAND — THERE IS NOTHING BEHIND IT. The film is 16:7 and shorter
   than a screen, so the earlier build centred it inside a full-height stage and let
   the section's black show above and below. That reads as letterboxing, and Bugra,
   2026-08-17: "videonun üstüne ve altında siyah bant var, o kısmı keselim." So the
   STAGE now takes the film's own height and sticks centred in the viewport, and the
   section carries no ground colour at all (section-keep, not section-black). Nothing
   is cropped to achieve it — the band keeps 16:7, it simply stopped sitting on a
   letterbox. What the reader sees above and below the pinned band is the page. */
.pin-sec { padding: 0; }
/* --pin-h is the film's height, and therefore the stage's. 43.75vw is 7/16 of the
   width, so a full-bleed 16:7 band. The floor keeps the eyebrow, heading, points and
   control inside it once the viewport is too narrow for 16:7 to be tall enough — below
   that the film covers a taller box instead, which crops the SIDES rather than the top
   and bottom Bugra rejected. The cap keeps it inside one screen so it can stick. */
.pin { position: relative; --pin-h: clamp(480px, 43.75vw, 100vh); }
/* --pin-step is how much scroll ONE point costs, and now it says so: the travel is the
   section's height minus the stage's, so the height is band + steps × step. It used to
   be written as (steps + 1) × 46vh against a full-viewport stage, which came out at
   32.5vh a point — the number below is that measured cost, kept identical so the pace
   Bugra approved does not change with the geometry. */
.pin { --pin-step: 33vh;
       height: calc(var(--pin-h) + var(--pin-steps) * var(--pin-step)); }
.pin-stage {
  position: sticky; top: calc((100vh - var(--pin-h)) / 2); height: var(--pin-h);
  display: flex; align-items: center;
}
/* The band fills the stage exactly — no aspect-ratio of its own, because the stage
   already has the shape. Two boxes both claiming the ratio is how the letterbox got
   there in the first place. */
.pin-band { position: absolute; inset: 0; overflow: hidden; z-index: 0; }
.pin-film {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; pointer-events: none;
}
/* The film is bright and carries a face; white type on it is unreadable without a
   ground. Measured, not guessed — see the contrast check in the session notes. */
/* Lighter than the first cut — it was carrying the contrast on its own and the film
   had gone to mud. Measured after: heading 9.9:1, body 6.6:1, both well past 4.5, with
   the picture still legible. */
.pin-scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(90deg, rgba(0,0,0,.72) 0%, rgba(0,0,0,.46) 52%, rgba(0,0,0,.10) 100%);
}
.pin-inner {
  position: relative; z-index: 2; width: 100%;
  max-width: calc(var(--ph-content-max) + var(--ph-margin) * 2);
  margin-inline: auto; padding-inline: var(--ph-margin);
}
.pin-head .eyebrow { color: var(--ph-on-brand); }
.pin-head .sec-title { color: var(--ph-white); max-width: 20ch; }
/* The section's own paragraph, in the fixed frame with the heading — the argument the
   points arrive under. Held to the same measure as the points so the block reads as
   one column rather than two widths. */
.pin-lede { margin-top: 16px; max-width: 34rem; font-size: var(--ph-fs-19);
            line-height: var(--ph-lh-19-loose); color: rgba(255,255,255,.86); }

/* The points stack in ONE place and cross-fade. Grid rather than absolute positioning,
   so the tallest point sizes the box and none of them can overlap the heading. */
.pin-pts { display: grid; margin-top: 40px; min-height: 8.5rem; }
.pin-pt {
  grid-area: 1 / 1; max-width: 34rem;
  opacity: 0; transform: translateY(14px);
  transition: opacity 260ms var(--ph-ease), transform 260ms var(--ph-ease);
  pointer-events: none;
}
.pin-pt.on { opacity: 1; transform: none; }
.pin-pt-t {
  font-size: var(--ph-h3-size); line-height: var(--ph-h3-lh);
  font-weight: var(--ph-w-600); color: var(--ph-white);
}
.pin-pt-b {
  margin-top: 12px; font-size: var(--ph-fs-21); line-height: var(--ph-lh-21);
  color: rgba(255,255,255,.82);
}
.pin-cta {
  display: inline-flex; margin-top: 40px;
  color: var(--ph-white); border-color: rgba(255,255,255,.42);
}
/* No :hover of its own on purpose. .pin-cta ships as class="tag pin-cta" on an <a>,
   so a.tag:hover (0,2,1) reaches it and fills it the way every other outlined pill
   on the site fills. The border-only hover that used to sit here was (0,2,0) — it
   would now lose the border-color it sets and win nothing else, which is a rule that
   reads as live and does nothing. On the film's dark ground the blue fill with white
   ink is Can's 2026-08-13 ruling, not a new colour. */

@media (max-width: 799px) {
  /* 7/16 of 390px is 171px — a film too short to read a heading on, let alone four
     points. On a phone the band takes the screen and the film covers it. Still no
     letterbox: the stage is the band here too, it is simply a screen tall. */
  .pin { --pin-h: 100vh; }
  .pin-inner { padding-inline: var(--ph-margin); }
  .pin-head .sec-title { max-width: none; }
  .pin-scrim { background: linear-gradient(180deg, rgba(0,0,0,.66), rgba(0,0,0,.58)); }
  .pin-pts { min-height: 11rem; }
}

/* A READER WHO ASKED FOR LESS MOTION GETS THE WHOLE THING AT ONCE. No pinning, no tall
   section, no cross-fade, no film — the poster, the heading and all four points stacked
   and readable. The content is never behind the effect. */
@media (prefers-reduced-motion: reduce) {
  .pin { height: auto; }
  .pin-stage { position: static; height: auto; padding: var(--ph-sec-top) 0 var(--ph-sec-bottom); }
  .pin-film { display: none; }
  .pin-stage { background: #000 url("/site/assets/video/gut-why.webp") center / cover; }
  .pin-pts { display: grid; gap: 32px; min-height: 0; }
  .pin-pt { grid-area: auto; opacity: 1; transform: none; transition: none; }
}

/* ==================================================== horizontal card rail (railx)
   Built in this site's tokens. Bugra asked on 2026-08-17 for this section in this
   form: ten cards pulled in from the right, picture on top, caption underneath — the
   name in the primary tone, the description in the secondary, one paragraph.

   At 1440: card 576 wide, image 3:2, 24 between, caption 17/25 with a 600-weight
   lead. Every number here is on this site's own scale.

   THE BROWSER DOES THE SCROLLING. `overflow-x` already handles a trackpad's horizontal
   gesture, a shift-wheel, a touch drag, a keyboard arrow and a screen reader's focus
   walk; a hand-written carousel is worse at all five. The arrows nudge the same
   scroller, so the rail has one implementation and not two. scroll-snap gives the card-to-card feel without owning the motion.

   IT BLEEDS RIGHT. The track's negative margin lets cards continue past the content
   column to the viewport edge, so the row reads as "there is more" rather than as a
   grid that happens to be cut. The padding puts the first card back on the grid. */
/* --railx-lead is the rail's own left inset, on TOP of the page's content margin, and
   it is declared on the SECTION so the heading can read the same number. Bugra,
   2026-08-17: "kartların başlangıcı birazdaha sağdan olsun … daha sağa al ve fontu da
   aynı hizaya al." One axis for the whole section — the heading, its lede and the first
   card all start on it. 120 is on tokens.space.scale.
   200 after three passes with Bugra — 64, then 120, then this. It is NOT on
   tokens.space.scale, and that is deliberate and recorded: the scale governs gaps
   BETWEEN things, and this is a composition inset for one section, tuned by eye so
   the rail sits about a quarter of the window in.
   Set it to 0 and the section goes back to flush with every other heading on the page;
   that is the whole way back. */
section:has(.railx) { --railx-lead: 200px; }
/* The head follows the cards, not the page grid — otherwise the section reads as a
   heading that belongs to something else sitting above a rail. */
/* PADDING, NOT MARGIN. render_section writes `style="margin-left:0"` onto every .lede,
   and an inline style beats any selector — so a margin rule moved the heading and left
   the sentence under it behind. Padding is a property no inline style here touches. */
section:has(.railx) > .wrap-wide > :is(h2, .lede, .eyebrow) {
  padding-left: var(--railx-lead);
}
.railx { position: relative; margin-top: 40px; }
.railx-track {
  display: flex; gap: 24px; list-style: none; margin: 0; padding: 0;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scroll-behavior: smooth;
  scrollbar-width: none; -ms-overflow-style: none;
  /* FULL BLEED TO BOTH VIEWPORT EDGES, first card still on the grid.
     Pulling back by --ph-margin alone was not enough: .wrap-wide caps at
     --ph-content-max and centres, so above 1440 there is more gutter than the margin
     and the rail stopped 240px short of the edge on a 1920 screen. Bugra wanted the
     browser's right edge to crop the row.
     --railx-inset is the real distance from the viewport edge to the content column at
     any width; the track pulls out by it and pads back by it, so it spans the window
     while its first card lines up with the heading above. */
  /* --railx-lead is extra room before the FIRST card, on top of the content inset.
     Bugra, 2026-08-17: "kartların başlangıcı birazdaha sağdan olsun, bizimki biraz sola
     fazla yaklaşmış." 64 is on tokens.space.scale. It is a separate number from the
     inset on purpose — one line to tune, and the day it should go back to flush with
     the heading it becomes 0 rather than an unpicking. */
  --railx-inset: max(var(--ph-margin), calc((100vw - var(--ph-content-max)) / 2));
  /* THE LEAD GOES ON THE PADDING ONLY. Added to the inset it moved the negative margin
     too, so the card stayed exactly where it was and the track grew 64px past BOTH
     viewport edges — which put a horizontal scrollbar on the page. The margin's job is
     to reach the window; the padding's job is to place the first card. Different jobs,
     different numbers. */
  margin-inline: calc(var(--railx-inset) * -1);
  padding-left: calc(var(--railx-inset) + var(--railx-lead));
  padding-right: var(--railx-inset);
  /* scroll-snap aligns a card's start edge to the SCROLLPORT's edge — the border box,
     not the padding box — so without this the browser scrolls the track by the padding
     to snap card one flush to the window, and the rail opens left of its own heading. */
  scroll-padding-left: calc(var(--railx-inset) + var(--railx-lead));
  scroll-padding-right: var(--railx-inset);
}
.railx-track::-webkit-scrollbar { display: none; }
@media (prefers-reduced-motion: reduce) { .railx-track { scroll-behavior: auto; } }

/* The rail takes focus, so it has to SHOW focus. Outline only — no shadow, and the
   offset keeps it clear of the cards' own corners. */
.railx-track:focus-visible {
  outline: 2px solid var(--ph-accent); outline-offset: 4px; border-radius: var(--ph-r8);
}
.railx-card {
  /* Larger than the first cut — Bugra, 2026-08-17: "visuallar daha büyük olsun". At
     1440 two cards and a peek still fit inside 1260. */
  flex: 0 0 660px; scroll-snap-align: start; min-width: 0;
}
.railx-media {
  aspect-ratio: 3 / 2; border-radius: var(--ph-r18); overflow: hidden;
  background: var(--ph-pill);
}
.railx-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The caption is ONE paragraph: the name carries the weight and
   the colour, the sentence after it stays secondary. Two separate blocks would read as
   a heading and a body — this is a label and its gloss. */
.railx-cap {
  margin-top: 20px; max-width: 34rem;
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17-loose);
  /* --ph-tr-17-600 was never declared — one use, no definition, so the
     declaration was dropped in silence and the caption inherited body tracking.
     The ladder has no 17/600 rung, and rather than invent one (see .opener on
     the missing 28/400) this points at the rung that exists. Zero visual delta:
     after the glass retheme --ph-tr-17-400 is 0em, which is what it was
     inheriting anyway. */
  letter-spacing: var(--ph-tr-17-400); color: var(--ph-text-2);
  /* The description carries the SAME weight as the name and differs only in colour —
     Bugra asked to see it at 600 rather than 400. tokens has no 500, so 600 is the
     rung above 400 and there is nothing between them. */
  font-weight: var(--ph-w-600);
}
.railx-cap b { color: var(--ph-text-1); font-weight: var(--ph-w-600); }

.railx-nav { display: flex; justify-content: center; gap: 12px; margin-top: 44px; }
.railx-btn {
  width: 44px; height: 44px; border-radius: 50%; border: 0; padding: 0;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  background: var(--ph-pill); color: var(--ph-text-1);
  transition: background 200ms var(--ph-ease), opacity 200ms var(--ph-ease);
}
.railx-btn:hover { background: var(--ph-line); }
/* At an end the control is spent, and it says so rather than sitting there live and
   doing nothing — the fake-control rule, applied to a state instead of a link. */
.railx-btn[disabled] { opacity: .35; cursor: default; }
.section-gray .railx-btn { background: var(--ph-white); }

@media (max-width: 1279px) { .railx-card { flex-basis: 68vw; } }
@media (max-width: 799px)  {
  /* On a phone the lead is dropped: 64px of a 390px screen is a sixth of the card. */
  section:has(.railx) { --railx-lead: 0px; }
  /* A single card with the next one peeking. */
  .railx-card { flex-basis: 86vw; }
  .railx-track { gap: 16px; }
  .railx-cap { margin-top: 16px; }
  /* A finger already drags the rail, so the arrows are a pointer affordance only. */
  .railx-nav { display: none; }
}

/* ------------------------------------------------------------------- marks
   A row of settled facts, read in one pass: a small disc holding a check, and the
   sentence under it. Measured off the Ring frame's "Get the highlights." band — four
   across the content width, disc 45px, sentence two lines. It replaced four white
   cards each holding a single line, which read as a document that had lost its
   formatting rather than as a designed row.
   No card, no border, no shadow: the disc IS the mark, and the fact stands on the
   page's own ground. */
.marks { display: grid; gap: 32px; margin-top: 40px; }
.marks-2 { grid-template-columns: repeat(2, 1fr); }
.marks-3 { grid-template-columns: repeat(3, 1fr); }
.marks-4 { grid-template-columns: repeat(4, 1fr); }
.marks-5 { grid-template-columns: repeat(5, 1fr); }
.mark-i {
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--ph-pill); color: var(--ph-accent-link);
}
.mark-i svg { width: 18px; height: 18px; }
.mark p {
  margin: 16px 0 0; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
  color: var(--ph-text-1);
}
.section-black .mark-i { background: rgba(255,255,255,.10); color: var(--ph-on-brand); }
.section-black .mark p { color: var(--ph-tertiary); }
@media (max-width: 900px) {
  /* Two across on a phone rather than one: these are short facts, and a single
     column turns four of them into a scroll. */
  .marks { grid-template-columns: 1fr 1fr; gap: 24px; }
}

/* --------------------------------------------------------------- spec table
   One label, all of its values under it, across the full content width. The deck
   stores a spec sheet flat — one record per VALUE, each repeating its label — and
   printed one row per record the Ring's Tech Specs read "Size and Weight" four times,
   "Sensors" five times and "Battery" twice down the left edge: 27 rows for 8
   specifications. Bugra, 2026-08-17: "aşağı doğru uzayan bir word dosyası … sol
   taraftaki başlıklar defalarca yazılmış."
   Two tracks, not a squeezed 760px column: the label holds a quarter and the values
   take the rest, which is what the frame draws and what stops a four-value spec
   wrapping into a paragraph. */
/* TWO COLUMNS OF SPECIFICATIONS, not one long fall. Ten grouped rows stacked in a
   single 900px column ran 1009px deep — a spec sheet you scroll rather than scan, which
   is the same complaint that started this section. Side by side they are five and five,
   read in one screen. The table takes the full content width again because two columns
   need it; the 900px cap existed only to stop ONE column stretching a two-word label
   away from its value.
   The row keeps its own label|values split inside each cell, so the structure is the
   same at every width — only how many rows sit beside each other changes. */
.spec-table {
  margin-top: 32px; display: grid; grid-template-columns: 1fr 1fr;
  column-gap: 48px;
}
.spec-row {
  display: grid; grid-template-columns: minmax(120px, 1fr) 2fr; gap: 24px;
  padding: 20px 0; border-top: 1px solid var(--hair);
}
.spec-k {
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
  font-weight: var(--ph-w-600); color: var(--ph-text-1);
}
.spec-v { display: grid; gap: 6px; }
.spec-v span {
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17); color: var(--ph-text-2);
}
@media (max-width: 900px) {
  /* One column of rows below the drawer width: 328px per cell cannot hold a label
     beside a four-word value, and the row would break into a paragraph. The label
     goes above its values instead, which is what the mobile frame draws. */
  .spec-table { grid-template-columns: 1fr; }
  .spec-row { grid-template-columns: 1fr; gap: 10px; }
}

/* A card's own link, inside the card and pinned to its foot, so two cards side by
   side put their links on one line however unequal their copy is. It used to be
   rendered as a loose chip after the grid — two identical buttons under two cards
   with nothing tying either to either. */
.card .card-more, .card-gray .card-more { margin-top: auto; padding-top: 16px; }
.grid .card, .grid .card-gray { display: flex; flex-direction: column; }

/* ------------------------------------------------------------------ buy block
   The product page's ONE decision unit: media left, a single stacked column right.
   The wireframe draws the whole purchase decision as one visual the visitor reads
   once and decides inside — Bugra, 2026-08-16. It used to ship as six stacked
   full-width bands, one of them black, so the decision was interrupted three times
   by a change of ground. Every product page uses this same block; which ROWS appear
   differs by product, the box never does. That is "two components doing the same job
   must be identical at every width" applied where it is easiest to break.

   Fluid, not snapped: the two tracks are a fraction and a fixed column, so the block
   reflows continuously instead of jumping at a breakpoint. One media query, at the
   drawer width, where the frame itself stacks. */
.buy-head { text-align: center; }
.buy-head .lede { margin-inline: auto; }
/* The hero's own link, centred under the lede like the rest of the head. It routes
   back to the product's explore page — see buy_block, which was dropping it. */
.buy-links { margin-top: 20px; display: flex; justify-content: center; }
/* 44px of hit area, not 21. tokens.control is the floor and check_design_language
   counts every element under it on a phone — restoring this link took that rule from
   6 to 12 in one build, six pages at 186x21. inline-flex rather than a block so the
   underline still hugs the words; the box grows around them. Scoped to .buy-links,
   never to a bare `a`: the one time a 44px rule was written broadly on this site it
   landed on all eight mega-menu panels and collapsed every product card in them. */
.buy-links a { display: inline-flex; align-items: center; min-height: 44px; }
.buy {
  display: grid; grid-template-columns: 1fr 600px; gap: 56px;
  /* TOP-ALIGNED, not centred. Centred put the photograph's top edge 215px below the
     first line of the decision column, so the eye started in two different places and
     the two halves read as two things rather than one. Bugra, 2026-08-17: it should
     sit level with "Material." The two columns now share a top edge. */
  align-items: start; margin-top: 56px;
}
.buy-media {
  /* The slot keeps its box and the content adapts — never the other way round.
     3:2 for every product page, not one ratio per product. The frames draw three
     (520x349 on the test kits, 600x560 on the supplements, 488x526 on the Ring) and
     following them would give three different media boxes for one job, which is the
     rule this project has already paid for: "two components doing the same job must
     be the same size, always, at every width". 3:2 is the one the six test kits draw
     and the one every photograph in build/kits/ actually is, so it is the ratio that
     needs no crop. A square box here left the landscape ring shot floating with dead
     white above and below it. */
  /* 488/526 — the ratio the wireframe's own Ring frame draws, and the one that makes
     a landscape photograph balance a tall decision column instead of leaving a third
     of the left side empty. At 3:2 the media was 403px against a 769px column and the
     block read as lopsided the moment it was top-aligned. Near-square it is 651px,
     which is flush with the test kits' column and 118px short of the Ring's.
     The 38% side crop was checked against the real assets before choosing it, not
     assumed: both the worn-ring photograph and every kit box carry their subject
     centred with margin, so nothing is cut — the kit box actually reads larger. */
  aspect-ratio: 488 / 526; display: flex; align-items: center; justify-content: center;
  /* The corner is the brand's, not the browser's. Every other framed thing on this
     site is rounded — cards, plates, tiles, the media well — and a square-cornered
     photograph beside a rounded option card reads as an oversight, which is exactly
     what the pro-guard exists to catch. r18 is tokens.radius.card, the same rung
     .buy-card uses, so the picture and the card beside it agree. */
  border-radius: var(--ph-r18); overflow: hidden;
}
/* --- the media slot as a carousel -------------------------------------------
   Same box, several frames. The arrows ship HIDDEN and shared.js unhides them, so a
   reader without JavaScript gets the first photograph and no dead controls.
   "Glassy" here is a translucent ground plus a backdrop blur — NOT a shadow.
   tokens.elevation says ELEVATION SISTEMI YOK and the mega-menu panel is this site's
   only exception, so the arrow reads as glass by what is behind it, not by lifting. */
.buy-carousel { position: relative; }
.buy-carousel img { position: absolute; inset: 0; }
.buy-carousel img[hidden] { display: none; }
.buy-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  /* IT HAS TO READ ON BOTH. The Ring's frames are dark photographs and the test kits'
     are a pale grey plate; a white glass disc vanished on the second. The disc keeps
     its light glass — that is what makes it belong to the photograph rather than sit
     on top of it — and gains a faint dark ring so its edge survives on a light ground.
     One control, both cases, no per-page variant. */
  border: 1px solid rgba(255,255,255,.55);
  outline: 1px solid rgba(0,0,0,.10); outline-offset: -1px;
  background: rgba(255,255,255,.42);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
  color: var(--ph-black); cursor: pointer; padding: 0;
  opacity: 0; transition: opacity 200ms var(--ph-ease), background 200ms var(--ph-ease);
}
/* 44px is tokens.control — the touch minimum — so these pass the touch-44 rule that
   already governs every other control on this site. */
.buy-arrow.prev { left: 14px; }
.buy-arrow.next { right: 14px; }
.buy-carousel:hover .buy-arrow, .buy-arrow:focus-visible { opacity: 1; }
.buy-arrow:hover { background: rgba(255,255,255,.52); }
/* A pointer can hover to reveal; a touch screen cannot, so there the arrows stay. */
@media (hover: none) { .buy-arrow { opacity: 1; } }
.buy-dots {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; justify-content: center; gap: 2px;
}
/* The indicator doubles as the timer. A resting frame is a 7px dot; the one on screen
   stretches into a bar and fills across it for exactly as long as that frame has left,
   so the change is announced rather than sprung. Taking hold of the carousel fills the
   bar and leaves it — there is no longer a countdown to show. */
/* The BUTTON is the target, the SPAN inside it is the indicator. A 7px dot is 7px to
   a finger — touch-44 counted four of them the first time this shipped. 44px is
   tokens.control, the minimum every other control here already meets. */
.buy-dot {
  width: 44px; height: 44px; padding: 0; border: 0; background: none;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.buy-dot > span {
  display: block; width: 7px; height: 7px; border-radius: 4px; overflow: hidden;
  /* Same problem as the arrows: a white dot on a pale plate is not a dot. A dark
     shadow is out (elevation YOK), so the track itself carries a neutral that reads
     on either ground and the FILL stays white. */
  background: rgba(0,0,0,.22);
  transition: width 260ms var(--ph-ease);
}
.buy-dot.on > span { width: 34px; }
.buy-dot i {
  display: block; height: 100%; width: 100%; background: rgba(255,255,255,.92);
  transform: scaleX(0); transform-origin: left center;
}
/* NO DURATION LITERAL HERE, AND THAT IS DELIBERATE. --car-ms is written by shared.js,
   which owns the interval, so the bar and the timer cannot drift apart — and this
   stylesheet keeps no authored duration above tokens.motion.duration.entrance, which
   check_design_language measures. A progress meter's length is DATA (how long is
   left), not an entrance curve, so it does not belong in the motion scale at all. */
.buy-dot.on i { transition: transform var(--car-ms) linear; transform: scaleX(1); }
/* Held by the reader: fill it and stop counting. */
.buy-carousel.held .buy-dot.on i { transition: none; transform: scaleX(1); }
@media (prefers-reduced-motion: reduce) {
  .buy-dot, .buy-arrow { transition: none; }
  .buy-dot.on i { transition: none; transform: scaleX(1); }
}

/* `cover`, and it crops nothing: every product photograph on this site is 1400x933,
   which is this slot's own 3:2 — measured, not assumed. It is `cover` rather than
   `contain` so a future asset at another ratio fills the box instead of floating in
   it with bars, which is the slot rule: the slot keeps its box, the content adapts. */
.buy-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.buy-col { min-width: 0; }
.buy-opt + .buy-opt { margin-top: 32px; }
/* Label and its support line sit on the SAME size, the bold half carrying the noun —
   the brand system's "destek satırı başlıkla aynı puntoda 400". */
.buy-label {
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
  letter-spacing: var(--ph-tr-17-400); color: var(--ph-text-1);
}
.buy-label b { font-weight: var(--ph-w-600); }
.buy-sub {
  margin-top: 6px; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
  color: var(--ph-text-2);
}
.buy-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 16px; }
.buy-choice {
  border: 1px solid var(--ph-line); border-radius: var(--ph-r8);
  padding: 12px 16px; background: var(--ph-white);
}
/* The selected state is a BORDER COLOUR and nothing else. It was written as
   `border-color` plus `box-shadow: inset 0 0 0 1px` — under a comment saying "a
   border, not a shadow" — and check_design_language failed it on six elements the
   first run. An inset box-shadow is still a box-shadow, tokens.elevation still says
   ELEVATION SISTEMI YOK, and the mega-menu panel is still the only exception on this
   site. Nothing here may lift, glow or ring itself to look chosen. */
.buy-choice.on { border-color: var(--ph-accent); }
.buy-choice .n {
  display: block; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
  font-weight: var(--ph-w-600); color: var(--ph-text-1);
}
.buy-choice .p {
  display: block; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
  color: var(--ph-text-2);
}
.buy-swatches { display: flex; gap: 14px; margin-top: 16px; }
/* A real border, not an inset shadow — see .buy-choice.on. The silver finish is close
   enough to the page that it needs an edge to read as a disc at all; border-box keeps
   the disc 36px so the three swatches stay one size. */
.buy-swatch {
  width: 36px; height: 36px; border-radius: 50%; background: var(--sw);
  display: block; box-sizing: border-box; border: 1px solid rgba(0,0,0,.08);
}
/* The ring around the chosen finish is drawn OUTSIDE the swatch so the colour stays
   true — an inset ring would eat 3px of the thing being chosen. */
.buy-swatch.on { outline: 2px solid var(--ph-accent); outline-offset: 3px; }
.buy-chips { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }
.buy-chips .tag { min-width: 56px; justify-content: center; }
.buy-chips .tag.on { border-color: var(--ph-accent); color: var(--ph-accent); }
.buy-alt {
  display: flex; justify-content: center; width: 100%; margin-top: 14px;
}
/* The test kits' variant: the whole decision inside one bordered card, price at the
   top. The Ring has no card and its price sits at the bottom — that is the only flag
   between the two, and both are the same component at the same width in the same
   track. A border and not a shadow: elevation YOK, and .ph-prod already argues this
   for a card sitting on white. */
.buy-card {
  border: 1px solid var(--ph-line); border-radius: var(--ph-r18);
  background: var(--ph-white); padding: 32px 36px;
}

/* ---- the glass variant ------------------------------------------------------
   REVERSIBLE BY ONE TABLE ROW: dropping the page from build_all.py's skins={...} removes the class and the
   card is the flat panel above again. Nothing here overrides structure — only
   surface — so taking it off cannot change a single measurement.

   THE ONE PLACE ELEVATION IS ALLOWED. The client's own design system records exactly
   one exception to elevation YOK: "Gölge SADECE satın alınabilir kartta" — shadow only
   on a card you can buy from — and states its value, 2px 4px 12px rgba(0,0,0,.08).
   That value is used unchanged. It is not a decorative lift; on a near-white ground a
   near-white card has nothing else to separate it, which is presumably why the one
   exception is the one card that must never be missed.

   GLASS ON A LIGHT GROUND, which is the hard case. There is no dark scenery to blur,
   so the effect cannot come from the blur alone: it comes from three things together —
   a vertical gradient so the surface catches light at the top and settles at the
   bottom, a bright rim that is brightest where the light lands, and the blur picking up
   the hero's own #F5F5F7→white gradient behind it. saturate() lifts the photograph's
   colour where the card overlaps it at narrow widths.
   backdrop-filter is progressive: a browser without it gets a slightly more opaque
   card and loses nothing structural. */
.buy-card.glass-light {
  background: linear-gradient(180deg, rgba(255,255,255,.92), rgba(255,255,255,.66));
  border-color: rgba(255,255,255,.85);
  -webkit-backdrop-filter: blur(22px) saturate(1.6);
  backdrop-filter: blur(22px) saturate(1.6);
  /* the sanctioned elevation, then the rim highlight that makes it read as a pane
     rather than as a tinted box — one declaration, because a second box-shadow
     property would replace this one rather than add to it. */
  box-shadow: 2px 4px 12px rgba(0,0,0,.08),
              inset 0 1px 0 rgba(255,255,255,.95);
}
@supports not (backdrop-filter: blur(1px)) {
  .buy-card.glass-light { background: rgba(255,255,255,.96); }
}
/* The panels INSIDE the glass step down rather than up: on a translucent surface a
   solid grey block reads as a hole punched through it. */
.buy-card.glass-light .buy-choice { background: rgba(255,255,255,.55); border-color: rgba(0,0,0,.08); }
.buy-card.glass-light .buy-choice.on { border-color: var(--ph-accent); background: rgba(255,255,255,.78); }
.buy-card.glass-light .buy-badge { background: rgba(0,0,0,.045); }

/* ---- aurora: the hero goes to a ground the glass can live on ------------------
   READING designs/pdp/ceramic-ring/hero/v5-buyblock-autolayout.html SETTLED THIS.
   Its glass works because its BODY is a grey-blue radial field and its card is
   rgba(255,255,255,.24)→.08 at blur(40px) — the pane carries LIGHT and the scene
   behind it is mid-tone. A dark card on a white page is the inverse of that and it is
   why the first two attempts read as tinted boxes: glass is a relationship between a
   pane and what is behind it, and a flat white page is not something to be behind it.

   So this skin changes the SECTION. The hero takes a deep ground with two soft pools
   of colour in it, and the card becomes a light pane floating on that. Both pools are
   the brand's own accent and neutral at low alpha — no new colour enters the system.
   Reversible with every other skin: build_all.py's skins={...} picks one, and `false`
   returns the flat white panel. */
/* ---- the film ground --------------------------------------------------------
   The same dark hero, with a looping abstract film behind the glass instead of the
   painted gradient. The glass finally has real movement to refract, which is the whole
   argument for it.
   NOT the homepage film. BRIEF §6.1 bans scroll-driven `currentTime` because browsers
   cannot seek reliably — that is why site/assets/film/ is a canvas frame sequence. This
   one never seeks: it loops, it is muted, it carries no information, and it stops for
   anyone who asked for less motion. app_phone.mp4 is the existing precedent.
   A scrim over it, because a moving ground under type is unreadable without one and the
   contrast has to hold at every frame of the loop, not on average. */
.hero.on-film { position: relative; overflow: hidden; }
.hero-film {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0; pointer-events: none;
}
.hero.on-film > .wrap-wide { position: relative; z-index: 2; }
.hero.on-film::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(8,9,14,.62), rgba(8,9,14,.74));
}
@media (prefers-reduced-motion: reduce) {
  /* The poster still shows — the ground is not lost, only its motion. */
  .hero-film { display: none; }
  .hero.on-film {
    background-image: url("/site/assets/video/hero-aurora.webp");
    background-size: cover; background-position: center;
  }
}

.hero.on-dark {
  background:
    radial-gradient(58% 62% at 74% 18%, rgba(0,113,227,.30), transparent 68%),
    radial-gradient(52% 58% at 12% 88%, rgba(134,134,139,.28), transparent 70%),
    linear-gradient(168deg, #171922 0%, #0E1016 58%, #131620 100%);
  color: var(--ph-white);
}
.hero.on-dark .eyebrow { color: var(--ph-on-brand); }
.hero.on-dark .hero-title { color: var(--ph-white); }
.hero.on-dark .lede { color: rgba(255,255,255,.72); }
/* The media well loses its light plate here — a pale grey rectangle on a deep ground
   is a hole, and the photograph has its own ground baked in. */
.hero.on-dark .buy-media { background: rgba(255,255,255,.04); }

/* THE CARDLESS COLUMN HAS TO INVERT TOO. The Ring's buy block is the option-group
   variant — no .buy-card, so none of the card's dark rules reach it — and on a dark
   hero its labels stayed at --ph-text-1 and its price panel stayed at --ph-surface with
   white type on it. Measured on the film ground: the "Material." label read at 1.4:1
   and £179 was white on pale grey. A skin that only dresses one of two variants is not
   a skin; it is a page that happens to look right.
   The price panel becomes glass here for the same reason the card does — a solid light
   block on a moving dark ground is a hole punched through it. */
.hero.on-dark .buy-label { color: var(--ph-white); }
.hero.on-dark .buy-sub { color: rgba(255,255,255,.78); }
.hero.on-dark .buy-col .ticks li { color: rgba(255,255,255,.92); }
.hero.on-dark .buy-col .ticks svg { color: var(--ph-white); }
.hero.on-dark .buy-choice {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.24);
}
.hero.on-dark .buy-choice.on {
  background: rgba(255,255,255,.18); border-color: rgba(255,255,255,.70);
}
.hero.on-dark .buy-choice .n { color: var(--ph-white); }
.hero.on-dark .buy-choice .p { color: rgba(255,255,255,.78); }
.hero.on-dark .buy-chips .tag {
  color: rgba(255,255,255,.92); border-color: rgba(255,255,255,.28);
}
.hero.on-dark .buy-chips .tag.on { color: var(--ph-white); border-color: var(--ph-white); }
.hero.on-dark .buy-alt {
  color: var(--ph-white); border-color: rgba(255,255,255,.28);
}
.hero.on-dark .buy-swatch { border-color: rgba(255,255,255,.35); }
.hero.on-dark .buy-swatch.on { outline-color: var(--ph-white); }
.hero.on-dark .buy-price {
  background: linear-gradient(150deg, rgba(255,255,255,.24) 0%, rgba(255,255,255,.06) 100%);
  -webkit-backdrop-filter: blur(26px) saturate(1.35);
  backdrop-filter: blur(26px) saturate(1.35);
  border-radius: var(--ph-r12);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.30);
}
.hero.on-dark .buy-price .stat-num { color: var(--ph-white); }
.hero.on-dark .buy-price .buy-sub { color: rgba(255,255,255,.82); }

/* The LIGHT pane, on that ground. v5's proportions, this site's tokens.

   ONE PANE, TWO GROUNDS. `aurora` paints the ground and `video` films it; the CARD is
   identical in both, so it is written once with :is() rather than twice. Writing it
   twice is how `glass-video` shipped with no rule at all — the section got its film,
   the card fell through to .buy-card's plain white, and the page showed an empty white
   panel on a dark hero. A skin value with no matching rule is invisible until someone
   looks.

   SHARP, not frosted. Bugra, 2026-08-17: "daha keskin görünsün, daha modern." A tighter
   corner (r18 -> r12, both on tokens.radius.scale), less blur so the ground reads as an
   image rather than as fog, and a steeper gradient so the pane has a light side and a
   dark side instead of one even wash.

   NO OUTER STROKE: "en dıştaki stroke'u tamamiyle kaldırabilir misin, dış çizgisi
   olmasın." A drawn outline announces an edge; real glass has none — you read its
   boundary from the change in what is behind it. The top edge survives as a reflection,
   not a line: the inset highlight runs 1px along the top and fades at the sides. */
.buy-card:is(.glass-aurora, .glass-video) {
  border-radius: var(--ph-r12);
  border: 0;
  background: linear-gradient(150deg, rgba(255,255,255,.26) 0%, rgba(255,255,255,.05) 100%);
  -webkit-backdrop-filter: blur(26px) saturate(1.35);
  backdrop-filter: blur(26px) saturate(1.35);
  /* the design system's sanctioned card shadow, and the rim that makes it a pane */
  box-shadow: 2px 4px 12px rgba(0,0,0,.08),
              inset 0 1px 0 rgba(255,255,255,.34);
  color: var(--ph-white);
}
@supports not (backdrop-filter: blur(1px)) {
  .buy-card:is(.glass-aurora, .glass-video) { background: rgba(40,42,52,.90); }
}
/* The controls inside follow the card down a rung, or a 12px card holding 18px panels
   reads as two different design languages stacked. */
.buy-card:is(.glass-aurora, .glass-video) .buy-choice { border-radius: var(--ph-r8); }
.buy-card:is(.glass-aurora, .glass-video) .buy-badge  { border-radius: var(--ph-r6); }

.buy-card:is(.glass-aurora, .glass-video) :is(.stat-num, .ticks li, .buy-choice .n) {
  color: var(--ph-white);
}
/* Measured off the RENDERED PIXEL, not off the CSS colour: the pane is translucent, so
   the ratio depends on what shows through it. --ph-tertiary reads 1.86:1 here and
   fails; white at 82% holds above 4.5:1 at the pane's lightest point. */
.buy-card:is(.glass-aurora, .glass-video) :is(.buy-sub, .buy-note, .buy-choice .p) {
  color: rgba(255,255,255,.82);
}
.buy-card:is(.glass-aurora, .glass-video) .ticks svg { color: var(--ph-white); }
.buy-card:is(.glass-aurora, .glass-video) .buy-badge {
  background: rgba(255,255,255,.16); color: rgba(255,255,255,.86);
}
.buy-card:is(.glass-aurora, .glass-video) .buy-choice {
  background: rgba(255,255,255,.10); border-color: rgba(255,255,255,.24);
}
.buy-card:is(.glass-aurora, .glass-video) .buy-choice.on {
  background: rgba(255,255,255,.18); border-color: rgba(255,255,255,.70);
}

/* ---- something for the glass to refract --------------------------------------
   GLASS ONLY READS AS GLASS WHEN THERE IS SOMETHING BEHIND IT. backdrop-filter blurs
   what is behind the element, and what was behind this card was a flat near-white
   ground — blurring a flat colour returns the same flat colour, so the first dark
   build was a tinted panel, not glass. Bugra saw it immediately: "koyu gri arka planlı
   bir kart var, o glass look yok."
   So the buy row gets a light field to refract: two very soft radial pools in the
   brand's own accent and a warm neutral, drawn BEHIND the card and clipped to the row.
   They are barely visible on their own — under 8% — and that is the point: they exist
   to be bent, not to be seen. No new colour enters the system; both pools are the
   accent and the surface token at low alpha. */
/* SCOPED TO THE SKINNED PAGES, and it was not — this shipped on `.buy`, so every
   product page grew a faint tint behind its buy row including the Ring, whose light
   build was meant to be untouched. Bugra caught it in a side-by-side: "ring'in şu
   kısmına bir arka plan gelmiş, bir koyuluk var." Third time in one session that a
   global reached a page nobody had asked about; the rule now in CLAUDE.md came from
   the second. A page with no skin gets NOTHING. */
.buy { position: relative; isolation: isolate; }
.hero.on-dark .buy::before,
.buy:has(> .buy-col > .buy-card.glass-light)::before {
  content: ""; position: absolute; inset: -14% -10%; z-index: -1; pointer-events: none;
  background:
    radial-gradient(40% 52% at 70% 24%, rgba(0,113,227,.11), transparent 70%),
    radial-gradient(36% 48% at 84% 76%, rgba(134,134,139,.14), transparent 72%);
  /* THE FIELD MUST HAVE NO EDGE OF ITS OWN. A radial pool centred near the box's
     border is CLIPPED by that border, and the cut reads as a faint rectangle drawn
     around the card — measured at 50px out from the card's right edge, the ground
     stepped from rgb(10,11,16) to rgb(7,8,13) on a hard line. Bugra saw it before any
     measurement: "kartın çevresinde belli belirsiz bir çerçeveleme var, çok saydam ama
     belli oluyor."
     The mask fades the whole layer toward its own edges, so there is no boundary left
     to see. The pools also moved inward so they finish inside the box rather than
     relying on the mask to hide a cut. */
  -webkit-mask-image: radial-gradient(closest-side, #000 55%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 55%, transparent 100%);
}

/* ---- the smoked variant -----------------------------------------------------
   The same card in dark glass, white type. Sitting on a LIGHT page it is the harder
   of the two: the ground behind it is #F5F5F7→white, so the tint has to carry the
   contrast on its own rather than borrowing any from the scenery. .82 is the floor
   that keeps white type over the lightest thing the card can ever sit on — a white
   hero — above 4.5:1; anything more translucent and the contrast becomes a function
   of what happens to be behind it, which is not a thing you can promise.
   Same sanctioned elevation, same rim logic, brighter rim because a dark pane needs
   a harder edge to read as glass and not as a filled box. */
.buy-card.glass-dark {
  background: linear-gradient(155deg, rgba(30,30,38,.80), rgba(14,14,20,.87));
  border-color: rgba(255,255,255,.16);
  -webkit-backdrop-filter: blur(26px) saturate(1.5);
  backdrop-filter: blur(26px) saturate(1.5);
  box-shadow: 2px 4px 12px rgba(0,0,0,.08),
              inset 0 1px 0 rgba(255,255,255,.22);
  color: var(--ph-white);
}
@supports not (backdrop-filter: blur(1px)) {
  .buy-card.glass-dark { background: rgba(24,24,28,.95); }
}
/* The type inside inverts to the tones the site already uses on a dark ground, so
   this borrows .section-black's palette rather than inventing a second dark scale. */
.buy-card.glass-dark .stat-num,
.buy-card.glass-dark .ticks li { color: var(--ph-white); }
/* NOT --ph-tertiary. #86868B is the site's secondary tone on a LIGHT ground, where it
   measures 5.07:1; composited over this pane it measures 1.86:1 at the card's lightest
   corner — unreadable. The CSS colour is not the contrast: the pane is translucent and
   the light field behind it shows through, so the ratio has to be read off the RENDERED
   PIXEL, which is how this was caught. White at 72% holds 4.9:1 against the lightest
   point the card reaches. */
.buy-card.glass-dark .buy-sub,
.buy-card.glass-dark .buy-note { color: rgba(255,255,255,.80); }
.buy-card.glass-dark .ticks svg { color: var(--ph-on-brand); }
.buy-card.glass-dark .buy-badge {
  background: rgba(255,255,255,.14); color: rgba(255,255,255,.82);
}
/* Panels step DOWN on glass, never up — the same rule as the light variant, mirrored. */
.buy-card.glass-dark .buy-choice {
  background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.14);
}
.buy-card.glass-dark .buy-choice.on {
  background: rgba(255,255,255,.11); border-color: rgba(255,255,255,.55);
}
.buy-card.glass-dark .buy-choice .n { color: var(--ph-white); }
.buy-card.glass-dark .buy-choice .p { color: rgba(255,255,255,.80); }
.buy-card .stat-num { margin: 0; }
.buy-card .buy-sub { margin-top: 8px; }
.buy-card .ticks { margin-top: 20px; }
.buy-card .btn { display: flex; justify-content: center; width: 100%; margin-top: 24px; }
/* align-self, because above 1280 the card is a flex column and a flex item's
   inline-block is blockified by the spec — so `display: inline-block` above was
   being honoured as `block` and the badge stretched to the card's full 528px,
   which reads as a disabled text field rather than as a chip. Measured on all
   six buy pages 2026-09-03. Stated as align-self rather than width:max-content
   so a long badge still wraps inside the card instead of overflowing it, and so
   the declaration is simply inert below 1280 where the card is a block. */
.buy-badge {
  display: inline-block; align-self: flex-start; margin-top: 16px;
  background: var(--ph-surface); border-radius: var(--ph-r6);
  padding: 5px 10px; font-size: var(--ph-fs-12); line-height: var(--ph-lh-12);
  color: var(--ph-text-2);
}
/* The note is the line under the CTA, and this renderer had no branch for it at all
   until 2026-08-16 — three decks carried one and all three were dropped in silence. */
.buy-note {
  margin-top: 14px; text-align: center;
  font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); color: var(--ph-text-2);
}
.buy-choices.one-col { grid-template-columns: 1fr; gap: 12px; }

.buy-price {
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  background: var(--ph-surface); border-radius: var(--ph-r14);
  padding: 20px 24px; margin-top: 28px;
}
.buy-price .stat-num { margin: 0; }
.buy-price .buy-sub { margin-top: 4px; }
.buy-price .btn { flex: 0 0 auto; }
.buy-col .ticks { margin-top: 28px; }

/* ───── THE CARD IS THE HEIGHT OF THE PICTURE. Always, on every page that runs this
   block. Bugra, 2026-08-18: "bu ikisinin uzunluğu her zaman aynı olsun, bu sayfa
   sistemini uyguladığımız her sayfada. sağdaki cam tasarım soldaki visual slide
   kartının boyunda olsun. bu çok önemli."

   It is the project's own 🔴 rule arriving at the buy block: two components doing the
   same job must be the same size at every width, and the SLOT keeps its box while the
   CONTENT adapts. Measured before this rule, at 1440: gut −8px, oral −6px, and
   comprehensive-genomic −166px, because that product's deck has five list rows and no
   subscription panel. Three pages a visitor reaches from the same menu row, three
   different card heights, for no reason a reader could name.

   BOTH FILL THE ROW, AND THE RATIO SETS THE FLOOR. .buy-media keeps `aspect-ratio:
   488/526`, which is what it contributes to sizing the row, so the row is never
   SHORTER than the picture's own ratio — that is the case on every width from 1366 up,
   and there the picture is at 488/526 exactly and the card stretches to it.

   At the narrow end of the desktop band the other case appears: at 1280 the media
   track is 496px, its ratio height 535, and gut's card — which carries a subscription
   panel genomic does not — needs 619. Holding the picture at `align-self: start` there
   left the pair 84px apart, which is the thing being fixed. So the picture stretches
   too and `object-fit: cover` takes the extra from the sides: 0.80 instead of 0.928 at
   1280 only, on the two products that have an options panel. A slightly tighter crop
   on one width beats two boxes that do the same job at two heights, which is the rule
   this block is here to satisfy.

   The purchase control goes to the FOOT of the card (`margin-top: auto`), not left
   floating under the list with the slack below it. A card that ends in white space
   reads as unfinished; a card whose decision sits on its base reads as designed, and
   the price still holds the top on every page. */
@media (min-width: 1280px) {
  .buy { align-items: stretch; }
  /* THE RATIO BECOMES A FLOOR, NOT A LOCK. `aspect-ratio` on a stretched grid item
     does not yield its height — Chrome keeps the ratio and grows the WIDTH instead,
     which pushed the picture 78px out of its own 496px track and 22px into the card's
     column at 1280. So the ratio is expressed as a min-height derived from the track
     the picture actually gets, and the box is free to grow past it.

     The track is arithmetic, not a guess: the block sits in .wrap-wide, so its width
     is min(--ph-content-max, 100vw - 2 x --ph-margin); the decision column is a fixed
     600px and the gap 56px. Written from the tokens rather than from the numbers they
     happen to hold, so a change to either cannot leave this stale. 526/488 = 1.077869
     is the ratio the paragraph above defends. */
  .buy-media {
    aspect-ratio: auto;
    min-height: calc((min(var(--ph-content-max), 100vw - var(--ph-margin) * 2)
                      - 600px - 56px) * 1.077869);
  }
  .buy-col { display: flex; flex-direction: column; }
  .buy-card { flex: 1 1 auto; display: flex; flex-direction: column; }
  /* Whichever comes first is the foot of the card: the option panels when a product
     has them, the button when it does not. Naming both means a product gaining or
     losing a subscription cannot silently change where the slack lands. */
  .buy-card > .buy-choices, .buy-card > .buy-choices ~ .btn { margin-top: 24px; }
  .buy-card > .buy-choices:first-of-type { margin-top: auto; }
  .buy-card:not(:has(> .buy-choices)) > .btn { margin-top: auto; }
}

/* THE SPLIT ENDS WHERE THE DESIGN BAND ENDS — 1280, not 900. The decision column is a
   FIXED 600px, so between 901 and 1279 the picture got whatever was left: measured at
   1100 the media was 316px wide and 341px tall, and at 901 it was 149x161 — a stamp
   beside a 611px card. Two components doing the same job, four times apart, in the
   middle of the tablet band.

   900 was also cutting a canonical band in half. D-025/D-033 draw four: Wide ≥1920,
   Desktop 1280-1919, Tablet 800-1279, Mobile 1-799. Side by side is the desktop
   arrangement and stacked is the tablet one; splitting at 900 gave the top half of
   tablet a layout no design width describes. At 1280 the media is 604x651 — the same
   box it has at 1440 and 1920 — and below it the block stacks whole. */
@media (max-width: 1279px) {
  /* The frame's own mobile stacks the identical unit: media over the column, same
     rows in the same order. No row is dropped and none is reordered. */
  .buy { grid-template-columns: 1fr; gap: 32px; margin-top: 40px; }
  .buy-media { aspect-ratio: 4 / 3; }
  .buy-price { flex-wrap: wrap; }
  .buy-price .btn { width: 100%; }
}
/* The card's inner padding has to answer to the page margin, and below 800 the
   margin drops to 24 while the card kept 36 — so on a phone the H1 sat on a 24px
   left edge and every line inside the card sat on a 60px one. Two left edges on
   one screen, on all six buy pages. 22 rather than 24 because the card's own
   1px edge and 16px radius already read as a step in from the page; matching the
   margin exactly made the card look like it had no padding at all. It also
   returns 28px of a 332px card to the copy, which at 390 is a fifth of the
   measure. Measured 2026-09-03 at 390x844. */
@media (max-width: 799px) {
  .buy-card { padding: 24px 22px; }
}

/* The track count is the PANEL's, not the stylesheet's. The eight panels are not the
   same shape — Clinic draws two columns, most draw three, Supplements draws four —
   and a hard-coded `repeat(3, 1fr)` silently wrapped the fourth column onto a second
   row under the first, which reads as a broken panel rather than as a wide one.
   shared.js publishes `--ph-cols` on this element from p.columns.length; the 3 here
   is the fallback for a panel rendered before that existed, never the rule. */
.ph-panel-cols {
  display: grid; grid-template-columns: repeat(var(--ph-cols, 3), 1fr); gap: 30px;
}
/* THE COLUMN TITLE IS A HEADING NOW, 2026-09-16 by request: 17/21 at 600 in the
   primary ink, up from 12/16 regular in the second. It was a rung BELOW the
   links it introduced, which is the one thing a heading may not be, and on a
   sheet that now runs the full width of the window a 12px grey line had nothing
   to hold its column together. The links under it are untouched at 14/18 — the
   ask was for the sub categories to grow, not for the products to. */
.ph-panel-col-t {
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
  font-weight: var(--ph-w-600); color: var(--ph-text-1);
  margin-bottom: 14px;
}
/* A second titled block stacked under a column. It is a heading in the middle of a
   list, so it needs the space above that the first one gets from the column top. */
.ph-panel-col-t2 { margin-top: 22px; }
/* A label and a link must sit on the SAME box — same size, same leading, same
   padding — or the two kinds of row in one column would not line up. Only the colour
   and the hover differ, because only one of them is a destination. */
.ph-panel .ph-panel-col a,
.ph-panel .ph-panel-col .ph-panel-label {
  display: block; padding: 5px 0; width: auto; border-bottom: 0;
  /* 14/18, not 15px on a 1.45 ratio — 15 is not a rung and a ratio is not the rule. */
  font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); color: var(--ph-text-1);
}
.ph-panel .ph-panel-col a:hover { color: var(--ph-accent-link); text-decoration: none; }
/* The design draws these rows, and there is no page behind any of them. They are
   labels, not controls: no hover, no pointer, no focus ring — because every one of
   those signals says "this goes somewhere" and none of them does. The muted colour
   is the same one column titles use, so a reader can see at a glance which rows in a
   column are reachable. NOT `href="#"` and NOT `href=""`; see shared.js. */
.ph-panel .ph-panel-col .ph-panel-label { color: var(--ph-text-2); cursor: default; }
/* Eight rows are drawn a size down from their neighbours. 12/16 is the rung below
   14/18; a ratio would have produced 11.9. */
.ph-panel .ph-panel-col a.ph-panel-sm,
.ph-panel .ph-panel-col .ph-panel-sm {
  font-size: var(--ph-fs-12); line-height: var(--ph-lh-12);
}
/* A drawn divider inside a column — it separates the two microbiome tests from the
   three testing utilities that share the column. A hairline, not a gap: the frame
   draws a line. It carries no text, so it is aria-hidden in the markup. */
.ph-panel-rule {
  display: block; height: 1px; background: var(--hair);
  margin: 10px 0; width: 100%;
}
.ph-panel-note {
  margin-left: 8px; font-size: 15px; color: var(--ph-text-2); white-space: nowrap;
}

/* The film homepage's nav is transparent white over the footage; an open panel
   needs a ground, so the nav returns to solid for as long as one is up. */
html.ph-menu-open.film-nav-white .ph-nav { background: rgba(255,255,255,.94); }
html.ph-menu-open.film-nav-white .ph-brand img { filter: none; }
html.ph-menu-open.film-nav-white .ph-trigger,
html.ph-menu-open.film-nav-white .ph-links a { color: rgba(29,29,31,.88); }
html.ph-menu-open.film-nav-white .ph-nav-cta {
  background: var(--ph-accent); color: var(--ph-white) !important;
}

@media (max-width: 900px) {
  /* The drawer row is one flex line — label, then chevron — and the panel wraps onto
     a full-width line beneath it. The panel is still inside .ph-item and still after
     its trigger, so desktop-card and mobile-accordion keep coming from this one
     arrangement with no second code path. */
  .ph-item { width: 100%; display: flex; flex-wrap: wrap; align-items: center;
             border-bottom: 1px solid var(--hair); }
  .ph-trigger {
    font-size: var(--ph-fs-17); padding: 12px 0; text-align: left;
    flex: 1 1 auto; min-width: 0;
  }
  /* .ph-links a (0,1,1) gives every anchor in the drawer a hairline and a width of
     100%. The trigger is an anchor now, so without these two it would draw a second
     line under the one .ph-item already draws, and take the whole row — pushing the
     chevron onto a line of its own. Two classes to beat (0,1,1): do not weaken this
     selector to .ph-trigger. */
  .ph-links .ph-trigger { border-bottom: 0; width: auto; }
  /* 44px is the touch-target rule, and it is also under the 45px the drawer row
     already measured (17/21 on 12px of padding), so the row height does not move.
     Aligned to the right content edge like the accordion's own marker. */
  .ph-toggle { display: flex; align-items: center; justify-content: flex-end;
               width: 44px; height: 44px; }
  .ph-panel { padding-bottom: 8px; flex: 0 0 100%; }
  .ph-panel-inner { padding: 0 0 6px; }
  .ph-panel-eyebrow { margin-bottom: 10px; }
  .ph-panel-cards { grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 18px; }
  .ph-panel-cols { grid-template-columns: 1fr; gap: 16px; }
  .ph-panel .ph-panel-col a { font-size: 16px; padding: 7px 0; }
}

/* ================= EDITORIAL RHYTHM & MOTION ==============================
   Appended last on purpose: .section-close and .cta-band are both (0,1,0), so the
   one that wins is the one declared later. ========================================= */

/* ---- Comparison table ----
   Three store category pages shipped a specification comparison as a grid of cards,
   one of them titled "(column headers)" and every row's values typed into a single
   paragraph separated by pipes: "At-home stool | Mouth swab | Private self-swab".
   Seven cards, and no way to read down a column — which is the only thing a
   comparison is for. The data was never lost, only its shape; this is the shape.

   Declared here rather than beside the other components because .section-black's
   table rules sit at :902 with an !important on border-color, and a class declared
   after them wins on everything else without having to fight for it.

   overflow-x: auto on the box and NEVER overflow-y: hidden or overflow: hidden.
   Those are the exact two computed values audit_layout's clipped-text check looks
   for on an ancestor, and every cell here is a text leaf underneath it. overflow-x
   computes overflow-y to `auto`, which is not `hidden`, which is why this is safe.
   The gate also skips its whole overflow sweep unless the DOCUMENT scrolls sideways,
   so a table scrolling inside its own box reads as correct because it is. */
.ctable-box { margin-top: 36px; overflow-x: auto; }
.ctable {
  width: 100%; border-collapse: separate; border-spacing: 0;
  font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
  letter-spacing: var(--ph-tr-17-400);
  /* 680 + two 24px phone margins is 728, so the table stops needing to scroll
     exactly where the phone margin arrives. */
  min-width: 680px;
}
.ctable thead th {
  padding: 0 12px 14px; text-align: left;
  font-size: var(--ph-fs-12); line-height: var(--ph-lh-12);
  font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-12-600);
  color: var(--ph-text-2);
}
.ctable td, .ctable tbody th {
  padding: 16px 12px; text-align: left; vertical-align: baseline;
  border-bottom: 1px solid var(--hair);
}
.ctable tbody th { font-weight: var(--ph-w-400); color: var(--ph-text-2); }
.ctable td { color: var(--ph-text-1); }
.ctable td.num {
  font-size: var(--ph-fs-19); line-height: var(--ph-lh-19);
  font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-19-400);
}
/* An em dash set at the weight of a value reads as a missing value rather than as a
   stated absence. This cell is a fact. */
.ctable td.none { color: var(--ph-text-2); }
.ctable td svg { width: 14px; height: 14px; vertical-align: -2px; color: var(--ph-accent); }

.section-black .ctable thead th,
.section-black .ctable tbody th { color: var(--ph-tertiary); }
.section-black .ctable td { color: var(--ph-surface); }
.section-black .ctable td,
.section-black .ctable tbody th { border-bottom-color: rgba(255,255,255,.16); }
.section-black .ctable td svg { color: var(--ph-on-brand); }

/* ---- The row label holds still ----
   The rail's question answered for tabular data. Below 728 the table scrolls inside
   its own box, and a four-column comparison scrolled two columns right is four
   values with nothing saying what they are. Pinning the label is the whole
   affordance — no arrows, no dots, no chrome the token set has no vocabulary for.
   The background is required, not decoration: a sticky cell is transparent and the
   scrolling values would print straight through it. */
@media (max-width: 799px) {
  .ctable tbody th, .ctable thead th:first-child {
    position: sticky; left: 0; z-index: 1; background: var(--ph-white);
  }
  .section-gray .ctable tbody th,
  .section-gray .ctable thead th:first-child { background: var(--ph-surface); }
  .section-black .ctable tbody th,
  .section-black .ctable thead th:first-child { background: var(--ph-black); }
}

/* ---- A run of arguments ----
   /hub/gut-signs/ gives each of seven numbered signs a full section: a 48-point
   heading, a hundred words, and 260px of padding, seven times, for about 2,600px of
   scroll. It is a list set as a chapter each. Eight more hub guides do the same at
   three to six items — 37 sections across nine pages.

   Merging them is the obvious fix and it is the wrong one: those sections carry the
   ids the ON THIS PAGE contents lists link into on fourteen pages, and linkcheck
   skips # hrefs by design, so a dropped id breaks silently. So this changes nothing
   structural and only takes back the air. The items keep their sections, their ids,
   their grounds and their heading size, so the outline the contents list describes is
   still the outline the page has.

   Half, because .section-tight already halves and the site should have one tighter
   value rather than two. A separate class rather than reusing .section-tight, because
   fix_scaffold_rhythm owns that token on the 42 scaffolds and the two intents must
   stay distinguishable. */
.section-run   { padding-top: calc(var(--ph-sec-top) / 2);
                 padding-bottom: calc(var(--ph-sec-bottom) / 2); }

/* ---- The close ----
   Every real page ends the same way twice: an onward grid of three, then a centred
   line with two buttons, 260px of padding each — 145 sections of identical closing
   rhythm across the site. They are one movement, not two, and this is what makes them
   read as one. Padding only; the ground stays wherever fix_section_rhythm puts it. */
.section-close { padding-top: calc(var(--ph-sec-top) / 2);
                 padding-bottom: calc(var(--ph-sec-bottom) / 2); }
.section-close + .section-close { padding-top: calc(var(--ph-sec-top) / 4); }

/* ---- Rail ----
   Below 900px every card row on this site becomes a single column, so a four-up row
   of destinations turns into four full-width blocks and about 1,200px of scroll for
   what was one glance. The row loses the one thing it was: a set you compare.

   On a phone the honest form of a set is a rail you push sideways. 78% leaves the next
   card showing at the edge, which is the whole affordance — no arrows, no dots, no
   chrome the token set has no vocabulary for. Flex stretch also gives the row equal
   card heights, which the stacked version never had.

   Three things here are not style and must not be "tidied":
   · It bleeds with a negative margin plus matching padding, NEVER 100vw. A viewport
     unit ignores the scrollbar, and the moment the document scrolls sideways
     audit_layout stops skipping its overflow sweep and reports every pre-existing
     near-miss on the page at once.
   · overflow-x only. Writing overflow-y:hidden makes the used overflow-y compute to
     "hidden", which is the exact value the clipped check looks for — a card whose text
     ran one line long would be reported as clipped by its own scroller.
   · The peek comes from gap, never from negative margins between children. Flex
     children are in the overlap candidate list. */
/* 900, not 799, and the difference is a bug. .grid-2/3/4 collapse to one column at
   900; the rail engaged at 799. Between them — and 800 is one of the four canonical
   design widths — a three-up product row was three 704px slabs stacked, which is
   exactly the stack this exists to prevent. The comment above has always said 900. */
@media (max-width: 900px) {
  .grid.rail {
    display: flex; overflow-x: auto;
    scroll-snap-type: x mandatory; scroll-padding-inline: var(--ph-margin);
    gap: var(--ph-gutter);
    margin-inline: calc(-1 * var(--ph-margin)); padding-inline: var(--ph-margin);
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
  }
  .grid.rail::-webkit-scrollbar { display: none; }
  .grid.rail > * { flex: 0 0 78%; scroll-snap-align: start; }
}

/* ---- The shop rail ----
   /store/ is the one page that is a CATALOGUE: 11 tests, 4 devices, 22 supplements.
   .grid.rail above is the phone answer to a set of three or four and returns to a
   grid over 900px; a set of 22 has no desktop grid form at all — it is eight rows of
   three and 6,000px of scroll for what the client's frame shows as one sideways set.
   So this rail is a rail at every width. The three rules from .grid.rail carry over
   unchanged and for the same reasons; read them above before editing anything here.
   `.grid.ph-rail` rather than `.ph-rail` alone so it can beat `.grid{display:grid}`
   on specificity without !important — the identical trick .grid.rail uses. */
.grid.ph-rail {
  display: flex; align-items: stretch;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--ph-margin);
  gap: 16px;
  margin-inline: calc(-1 * var(--ph-margin));
  padding-inline: var(--ph-margin);
  /* The focus ring is 3px at 3px offset and this box scrolls; without the padding a
     focused card's ring would be clipped by its own scroller.
     And the padding is NOT clawed back with a negative margin. It was, for one
     build: `margin-block:-8px` pulled the track 8px up into the heading above it and
     audit_layout reported an 8px overlap on /store/ at all four widths — it compares
     the boxes of consecutive siblings and does not care that the pixels are empty.
     16px of extra breathing room is not worth a red gate. */
  padding-block: 8px;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.grid.ph-rail::-webkit-scrollbar { display: none; }
.grid.ph-rail > * { flex: 0 0 300px; scroll-snap-align: start; }
@media (max-width: 799px) { .grid.ph-rail > * { flex: 0 0 78%; } }

/* The card. A surface has to differ from the surface it sits on — by colour, border
   or shadow. These sit on WHITE, so colour is unavailable without inverting them to
   grey, and that would leave the black accent card as the only non-grey one and
   destroy the "one black card on white" the frame is built around. Shadow is out:
   tokens.json records `elevation YOK`, and the one recorded exception — "gölge
   SADECE satın alınabilir kartta" — does not reach here, because these cards carry a
   link to a product page, not a purchase control. The border is what is left, and it
   is the frame's own answer. */
.ph-prod {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  padding: 32px 20px 0;
  background: var(--ph-white);
  border: 1px solid var(--ph-line);
  border-radius: var(--ph-r18);
  color: inherit; text-decoration: none; overflow: hidden;
  transition: background var(--ph-dur-fast) var(--ph-ease);
}
a.ph-prod:hover { background: var(--ph-surface); text-decoration: none; }
a.ph-prod:hover .ph-prod-name { color: var(--ph-accent-link); }
.ph-prod-name { margin: 0; font-size: var(--ph-fs-21); line-height: var(--ph-lh-21);
                font-weight: var(--ph-w-600); letter-spacing: var(--ph-tr-21-600);
                text-wrap: balance; }
.ph-prod-meta { margin: 6px 0 0; font-size: var(--ph-fs-14);
                line-height: var(--ph-lh-14); color: var(--ph-text-2); }
.ph-prod-go   { margin-top: 10px; color: var(--ph-accent-link); }
.ph-prod-art  { display: block; width: 100%; margin-top: auto; aspect-ratio: 20 / 21; }
.ph-prod-art svg { display: block; width: 100%; height: 100%; }

/* The accent is a CARD that goes black, never a section. The frame's ground is white
   throughout; `section-black` on this page is what produced the whole-black Tests
   band the client saw. --ph-black is annotated "footer ground only" in tokens, and
   .tile.tile-dark is the standing precedent for spending it on a card. */
.ph-prod.is-dark { background: var(--ph-black); border-color: var(--ph-black); }
.ph-prod.is-dark .ph-prod-name { color: var(--ph-white); }
.ph-prod.is-dark .ph-prod-meta { color: var(--ph-tertiary); }
.ph-prod.is-dark .ph-prod-go   { color: var(--ph-on-brand); }
a.ph-prod.is-dark:hover { background: #27272a; }

.ph-rail-head { display: flex; align-items: center; justify-content: space-between;
                gap: 24px; flex-wrap: wrap; }
.ph-rail-head > .sec-title { margin: 0; flex: 1 1 20ch; min-width: 0; }
/* A hero that plays. Same box as .hero-img's picture — same radius, same cover —
   so nothing in the split hero moves. Muted + playsinline + loop is the only
   combination that autoplays on iOS; the poster carries the first frame so the
   slot is never empty while 500KB arrives, and preload="metadata" keeps it off
   the critical path. This is NOT the homepage film: that is a canvas frame
   sequence for the reasons in BRIEF §6.1, and this is a five-second loop with no
   scroll coupling at all. */
.hero-img video {
  display: block; width: 100%; height: auto;
  border-radius: inherit;
  /* The box is the HERO's ratio, never the video file's. `height:auto` alone let a
     16:9 clip set its own height and /app/'s hero card came out 583x328 against
     /devices/' 583x388 — two cards doing the same job at two sizes, which is the
     one thing that makes a set of pages look unbuilt. Can, 2026-08-13: "kart
     boyutlarını asla değiştirme, aynı hiyerarşide ve aynı görev sorumluluğunda
     olan kartları… bunu benim sana söylemem gerekmiyor". The ratio is the hero
     asset spec from build/imagery (1400x930); cover scales the clip to fill it
     without distorting it. */
  aspect-ratio: 1400 / 930;
  object-fit: cover;
}

/* The quiet half of a two-voice section heading. `display:block` puts it on its
   own line: Can, 2026-08-13, looking at /store/ — "gri yazı alt satıra geçse
   nasıl olur… evet böyle yapalım, bu daha iyi". Three headings on one page. */
.ph-dim { color: var(--ph-text-2); display: block; }
.ph-rail-nav { display: flex; gap: 10px; flex: 0 0 auto; }
/* tokens control.carouselArrow is the spec for exactly this control: sizes [44, 56],
   radius circle, bg #D2D2D7, "gölgesiz, çizgisiz". This was 38px on --ph-surface — 38 is
   a real control height in the token set but it is not one of carouselArrow's two, and
   #F5F5F7 is the section ground, so on /store/ the arrows were a control the same colour
   as the page. 44 is carouselArrow's smaller size and --ph-touch-min at once, which is
   what makes the touch rule below possible. --ph-line IS #D2D2D7, and its declaration at
   the top of this file already reads "rule · divider · arrow ground". */
.ph-rail-btn {
  width: var(--ph-btn-h); height: var(--ph-btn-h);
  border: 0; border-radius: 50%; background: var(--ph-line);
  color: var(--ph-text-1); font-family: var(--ph-font);
  font-size: var(--ph-fs-17); line-height: 1; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.ph-rail-btn[disabled] { color: var(--ph-tertiary); cursor: default; }
/* This was `@media (hover: none)` with no width bound, and the reason given was the only
   reason: at 38px the control was under --ph-touch-min, so rather than break the token it
   was not offered to a finger at all. At 44 that reason is gone and the arrows are the
   only thing on /store/ that says 22 supplements continue off-screen.
   799 rather than nothing, and it is the site's own number: below 800 `.grid.ph-rail > *`
   goes from 300px to 78%, which is the width at which the rail stops being a set you
   glance at and becomes one you push — the same switch .grid.rail makes, for the same
   reason. Measured at 390: showing the nav there wraps it onto its own line and adds 68px
   to every rail head, three times on /store/, to duplicate a gesture the 78% peek already
   advertises. At 800 the nav sits on the head's own line and costs 6px. */
@media (hover: none) and (max-width: 799px) { .ph-rail-nav { display: none; } }

/* The category row. Ground is --ph-surface for all eight rather than eight hues at
   10%: build/menu_marks.py exists because the client's note on the multi-coloured
   version was that it looked "rengarenk" and broke the theme. */
/* A grid, not a wrapping flex row. `space-between` + `flex-wrap` spreads the LAST
   row across the full width, so eight tiles at 390px came out 3 / 3 / 2-with-a-hole.
   A grid keeps every column on the same axis at every width. */
.ph-cats { list-style: none; margin: 0; padding: 0;
/* minmax(0, 1fr), not 1fr. `1fr` is minmax(AUTO, 1fr), so the min-content width of
   the longest label ("Microbiome", "Supplements") widens its own column and the
   tiles come out different sizes. minmax(0,…) lets the label wrap instead. */
           display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 16px; }
@media (max-width: 799px) {
  .ph-cats { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px 12px; } }
.ph-cat  { display: flex; flex-direction: column; align-items: center; gap: 12px;
           color: inherit; text-decoration: none; }
/* Fluid, capped — not a fixed 84px. Eight columns inside the 800px margin leave
   74px per cell, so a rigid 84px tile pushed the last one 32px past the viewport
   and audit_layout reported an overflow at tablet and phone. The tile is now as
   wide as its cell allows, up to the design's 84. */
.ph-cat-tile { display: block; width: 100%; max-width: 84px; aspect-ratio: 1;
               border-radius: var(--ph-r21);
               background: var(--ph-surface);
               transition: background var(--ph-dur-fast) var(--ph-ease); }
a.ph-cat:hover { text-decoration: none; }
a.ph-cat:hover .ph-cat-tile { background: var(--ph-pill); }
a.ph-cat:hover .ph-cat-name { color: var(--ph-accent-link); }
.ph-cat-tile svg { display: block; width: 100%; height: 100%; }
.ph-cat-name { font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
               color: var(--ph-text-2); text-align: center; }

/* ---- Sequence ----
   The section already arrives as one block: its wrapper rises 24px and fades, and that
   gesture is on the WRAPPER rather than on the section, because audit_layout compares
   consecutive siblings' boxes and a transform on <section> would push every page 24px
   into its neighbour. Nothing here changes that. The block still rises as one; its
   lines simply resolve in the order you would read them.

   Opacity only, and only opacity. A staggered translate is the single most dangerous
   idea available here: the measured gap between .eyebrow and h2.sec-title is 16px, so
   24px of stagger produces a confirmed 8px overlap and turns the gate red on every
   page at once. Opacity moves no rect at all, at any width, under any future edit to
   these delays.

   No class and no JavaScript: `> * > *` is the wrapper's own children, which is
   exactly the set of blocks a section is made of. .band is untouched by construction —
   its only child is the photograph, which has no children of its own — and a card
   inside a .grid is a level deeper and stays with its row. 240ms is --ph-dur-base,
   declared since the token set was written and referenced by nothing; this is what it
   was for. The last line lands at 590ms, inside the wrapper's own 620ms, so the block
   is still one movement. */
html.ph-reveal main > section.ph-r  > * > * {
  opacity: 0; transition: opacity var(--ph-dur-base) var(--ph-ease);
}
html.ph-reveal main > section.ph-in > * > * { opacity: 1; }
html.ph-reveal main > section.ph-in > * > :nth-child(2)   { transition-delay:  70ms; }
html.ph-reveal main > section.ph-in > * > :nth-child(3)   { transition-delay: 140ms; }
html.ph-reveal main > section.ph-in > * > :nth-child(4)   { transition-delay: 210ms; }
html.ph-reveal main > section.ph-in > * > :nth-child(5)   { transition-delay: 280ms; }
html.ph-reveal main > section.ph-in > * > :nth-child(n+6) { transition-delay: 350ms; }
/* The tail rule is not optional. A ladder that simply runs out drops child 6 to no
   delay while child 5 is still waiting — something above arriving after something
   below. Harmless for opacity, fatal the day anyone adds transform. */

/* The opener's second voice arrives a beat behind the first, so the head reads as two
   things rather than as a wider paragraph. */
html.ph-reveal main > section.ph-in > .opener > .lede { transition-delay: var(--ph-dur-base); }

/* ---- The figure's rule draws itself ----
   .figures is the one place on this site where type goes to display size outside a
   heading, and there are only 17 rows of it. A number that is simply printed is a
   fact; a rule that draws under it as you arrive makes it read as a finding.

   An overlay on the existing border rather than the border itself, because a border
   cannot be transformed and animating its width would be animating layout. Scoped to
   html.ph-reveal, so a browser without an observer — and a reader who asked for
   stillness — sees the plain grey rule that was always there.

   Note what this costs: position:relative removes .figure from audit_layout's overlap
   candidate list. That is a deliberate, precedented reduction (.step already does it)
   and it is stated here so it is a decision rather than an accident. */
html.ph-reveal .figure { position: relative; }
html.ph-reveal .figure::before {
  content: ""; position: absolute; top: -1px; left: 0; width: 100%; height: 1px;
  /* The one place the brand's gradient family appears on a white page, and it is one
     pixel tall. §7: the symbol carries "turquoise into dark blue, green into blue,
     teal into dark green"; these three stops are lifted from phenome-logo.svg itself.
     Drawn from the left, so the line arrives turquoise and lands on Phenome Blue —
     the identity's own move, at the scale the guideline's own instruction ("images
     should not be too colourful") will tolerate on a page selling calm. */
  background: var(--ph-accent);
  transform: scaleX(0); transform-origin: left;
}
html.ph-reveal .section-black .figure::before { background: var(--ph-on-brand); }
html.ph-reveal main > section.ph-in .figure::before {
  transform: scaleX(1); transition: transform var(--ph-dur-reveal) var(--ph-ease);
}
html.ph-reveal main > section.ph-in .figure:nth-child(2)::before { transition-delay:  70ms; }
html.ph-reveal main > section.ph-in .figure:nth-child(3)::before { transition-delay: 140ms; }
html.ph-reveal main > section.ph-in .figure:nth-child(4)::before { transition-delay: 210ms; }
html.ph-reveal main > section.ph-in .figure:nth-child(5)::before { transition-delay: 280ms; }

/* ---- The showcase picture settles ----
   Same gesture as the band, same reason, and safe for the same reason: the photograph
   is inside a clipped rounded box, so it can arrive fractionally over-size and come to
   rest without any edge ever being exposed and without one layout box moving. Slower
   than the copy beside it — a picture that snaps reads as a sprite, a picture that
   settles reads as weight. */
html.ph-reveal main > section.ph-r  .showcase-img img { transform: scale(1.05); }
html.ph-reveal main > section.ph-in .showcase-img img {
  transform: none; transition: transform 1100ms var(--ph-ease);
}

/* ---- The product bar ----
   The one genuinely missing piece of product-page UX. This page is 6,400px tall and
   the only way to buy from the bottom of it was to scroll back to the top or find the
   closing band. It appears when the hero has gone and takes itself away again when you
   return, so nothing covers the page you arrived on.

   Everything in it is READ OFF THE PAGE — the name from <title>, the label and the
   href from the hero's own primary button. Nothing is authored here, and no price is
   shown: the Ring's price is written six different ways across this site and a bar
   pinned to every screen is the last place to pick one.

   Built to be the nav's sibling, not a new kind of chrome: same translucent white,
   same blur, same hairline, same height token. No shadow — `elevation YOK`. */
.ph-bar {
  position: fixed; left: 0; right: 0; top: 0; z-index: 99;
  display: flex; align-items: center; justify-content: center;
  height: 56px; background: rgba(255,255,255,.82);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid rgba(0,0,0,.07);
  opacity: 0; visibility: hidden; transform: translateY(-8px);
  transition: opacity var(--ph-dur-base) var(--ph-ease),
              transform var(--ph-dur-base) var(--ph-ease),
              visibility 0s linear var(--ph-dur-base);
}
.ph-bar.on { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
/* Same container formula as .ph-nav-inner and .wrap-wide — the bar is the nav's sibling
   and has to start on the same left line as the page under it. */
.ph-bar-inner { width: 100%; max-width: calc(var(--ph-content-max) + var(--ph-margin) * 2);
                margin-inline: auto; padding-inline: var(--ph-margin);
                display: flex; align-items: center;
                justify-content: space-between; gap: 16px; }
.ph-bar-name { font-size: var(--ph-fs-17); font-weight: var(--ph-w-600);
               letter-spacing: var(--ph-tr-17-400); color: var(--ph-text-1);
               white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 900px) { .ph-bar { height: 52px; } }
@media (prefers-reduced-motion: reduce) { .ph-bar { transform: none; } }

/* ---- The swap ----
   The bar used to sit BELOW the nav, at top: var(--ph-nav-h), which meant a reader
   past the hero was looking at two bars stacked on each other: the site's nav and a
   product bar repeating a control from a screen they had already left. Five pages had
   noticed and written their own fix — store/phenometech-ring (pdp.css), devices/band,
   devices/ring, devices/ring-lab and supplements/nad — and the other forty had not,
   which is the whole reason this is here and not there. The ring's answer is the one
   that was promoted, unchanged: the bar takes top: 0 and the nav leaves upward, so
   there is one bar at the top of the window at any moment, never two.

   shared.js sets `.is-past-hero` from the same IntersectionObserver that already
   decides when the bar is `.on`, so the two halves of the swap cannot drift out of
   step the way two independent scroll listeners would.

   translateY rather than display: the nav is in flow as a sticky element, so moving
   it changes nothing about the page's layout, and it can come back without a reflow.
   visibility is delayed by the duration so it fades before it stops being focusable,
   and it is what keeps the hidden nav's links off the Tab order in between.

   The easing is the ring's own, not this file's default: `--ease-frosted-out` is
   what all five pages had written, and it is the curve that makes the bar look like
   it is being let go rather than pushed. It comes from phenome-glass.css, which is
   linked on all 84 pages, and the fallback is here only so this rule stands alone. */
.ph-nav {
  transition: transform var(--ph-dur-base) var(--ease-frosted-out, var(--ph-ease)),
              opacity var(--ph-dur-base) var(--ease-frosted-out, var(--ph-ease));
}
body.is-past-hero .ph-nav {
  transform: translateY(-100%); opacity: 0; visibility: hidden;
  transition: transform var(--ph-dur-base) var(--ease-frosted-out, var(--ph-ease)),
              opacity var(--ph-dur-base) var(--ease-frosted-out, var(--ph-ease)),
              visibility 0s linear var(--ph-dur-base);
}
/* The one case where both belong on screen. A menu or the search overlay is opened
   FROM the nav, and on a phone the panel can be scrolled — so a panel that outlived
   the bar it hangs from would be a menu attached to nothing. While one is up the nav
   comes back and keeps its ground; shared.css already hides the product bar under the
   search overlay a few hundred lines down, so they never overlap. */
html.ph-menu-open body.is-past-hero .ph-nav,
html.ph-search-open body.is-past-hero .ph-nav {
  transform: none; opacity: 1; visibility: visible; transition-delay: 0s;
}
@media (prefers-reduced-motion: reduce) {
  /* A whole bar sliding off the top of the window is exactly the motion this asks to
     be spared. The swap still happens — it has to, or the two bars come back — it just
     happens as a fade, which is the same information without the travel. */
  .ph-nav, body.is-past-hero .ph-nav { transform: none; }
}

/* ---- Hero drift ----
   The hero is deliberately not part of the reveal — fading it in makes the site feel
   slow at exactly the moment it is being judged for speed — so it gets the one gesture
   that costs nothing: the photograph opens inside its own clipped box while the box
   holds perfectly still. Driven from script, so the transform is inline and this rule
   only clears the vestigial 620ms transition that would otherwise make every scroll
   frame lag. Scoped to a class the script adds, so nothing changes on a page that
   never runs it. */
html.ph-drift .hero-img img, html.ph-drift .hero-figure img {
  transition: none; will-change: transform;
}

/* ---- Stillness, finished properly ----
   The existing block zeroes durations but leaves END STATES intact, so a reader who
   asked for stillness currently gets the photograph snapping 3.5% larger in 0.001ms —
   worse than the smooth version they were avoiding. And a snap is motion the reader
   did not ask for even when they started the scroll.
   Nothing here needs to say anything about a control's press any more: the :active
   scale this paragraph used to defend was replaced on 2026-08-20 by a darker fill. A colour change is not motion, so a reader who asked for
   stillness keeps the press feedback rather than losing it — which is what that
   sentence was protecting in the first place. */
@media (prefers-reduced-motion: reduce) {
  a:hover .card-img img, a.card-gray:hover .card-img img { transform: none; }
  .link-more:hover:after,
  a.card-gray:hover .card-title:after, a.card:hover .card-title:after { transform: none; }
  .grid.rail { scroll-snap-type: none; }
}

/* ---- Promo tiles ----
   This is not a new invention. It is the client's own component: Store Card, node
   44:30 of Figma file IwAOva4TOtza1K2fsT50SY, whose description reads
   "Promo/özellik kartı — görsel TAM KANAMA (480×500, 0,0). Metin üstüne
   biner. Görsel kırpılmaz." Every value below is that spec: eyebrow 12/600 uppercase
   secondary, title 28/600 primary, support 14/400 primary, price 12/400 secondary,
   media ground Surface/Pill, radius 18. Nothing here was chosen to taste.

   Declared after .ctable so it wins on ground against .section-black's descendants
   without an !important, the same reason the comparison table sits where it does.

   Why it exists: six flagship tests shipped on /testing/ as six small grey text cards
   with a decorative gradient sphere where the photograph should be — while the real
   colour-coded kit photography sat in site/assets/pages/ already cut. The card was
   never wrong, it was just too small to be the thing a visitor came for.

   NO HOVER, NO SHADOW. tokens.json records `elevation YOK`, and the design system's
   single exception — "gölge SADECE satın alınabilir kartta" — is not claimed here: a
   tile carries a link to a product page, not a purchase control, so it is not a card
   you can buy from. The buttons inside bring their own press and hover from .btn.
   That also keeps audit_layout clean, because nothing here moves a layout box.

   `.tiles` DEFINES NO GRID. It is a marker only, written onto the existing
   `.grid.grid-2` so the pass can recognise its own output. The first version of this
   component shipped a parallel `.tiles-2/-3/-4` system with its own 900/599
   breakpoints alongside `.grid` (900 → 1-up) and `.grid.rail` (900 → sideways) —
   three languages for one event. The layout here is the site's own `.grid-2`, and it
   collapses when everything else does. Being a `.grid-2` also puts it out of reach of
   build_editorial's mark_rails(), which selects only `.grid-3, .grid-4`. */

/* Auto-layout, not a fixed frame. The copy block hugs its content and the media sits
   below it at a fixed ratio, so a longer blurb grows the tile downward instead of
   pushing the photograph out of it — no tile can overflow its section, which is what
   audit_layout reads as an overlap between siblings.

   The ground matches the PHOTOGRAPH, not the token, and --tile-ground is written per
   tile by build_tiles.py from the image's own corner pixel. That is prepare.py's
   reasoning one level up: it fills each product canvas with "the shot's own backdrop"
   because "trimming against a fixed threshold leaves a visible rectangle of the
   original backdrop sitting inside the page's own surface colour". The identical seam
   appears where a tile's ground meets the photograph's — measured, the five kits are
   cut on a flat #E3E3E3 and Surface/Pill is #E8E8ED, 1.9 L* apart, which shows as a
   band across the tile. The var() carries a fallback on purpose: on this project a
   var() that resolves to nothing is dropped rather than defaulted, and a tile with no
   background at all would inherit the section's. */
.tile {
  display: flex; flex-direction: column;
  background: var(--tile-ground, var(--ph-pill));
  border-radius: var(--ph-r18);
  overflow: hidden;
  text-align: center;
}
.tile-t { padding: clamp(34px, 3.2vw, 52px) clamp(24px, 2.6vw, 44px) 0; }
.tile-eyebrow {
  margin: 0 0 12px; font-size: var(--ph-fs-12); line-height: var(--ph-lh-12);
  letter-spacing: var(--ph-tr-12-600); font-weight: var(--ph-w-600);
  color: var(--ph-text-2);
}
.tile-h {
  margin: 0; font-size: var(--ph-fs-28); line-height: var(--ph-lh-28);
  letter-spacing: var(--ph-tr-28-600); font-weight: var(--ph-w-600);
  color: var(--ph-text-1); text-wrap: balance;
}
.tile-p {
  margin: 10px auto 0; max-width: 46ch;
  font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
  font-weight: var(--ph-w-400); color: var(--ph-text-1);
}
.tile-price {
  margin: 12px 0 0; font-size: var(--ph-fs-12); line-height: var(--ph-lh-12);
  font-weight: var(--ph-w-400); color: var(--ph-text-2);
}
.tile-actions { margin-top: 22px; display: flex; gap: 12px; justify-content: center;
                flex-wrap: wrap; }

/* object-fit: contain is not a preference. The client's spec says "görsel kırpılmaz",
   and cropping a kit crops what is printed on it — the process strip, the GMP and
   Halal marks. height: 100% has to beat the global img { height: auto }, which it does
   on specificity; without it the width/height attributes win at presentational-hint
   level and the aspect-ratio box collapses. */
.tile-media { margin-top: clamp(22px, 2.4vw, 34px); width: 100%; aspect-ratio: 3 / 2; }
.tile-media img { width: 100%; height: 100%; object-fit: contain;
                  object-position: center bottom; display: block; }

/* A contextual scene has no flat ground to match, so it fills its frame instead of
   sitting on one. This is the only tile a page with no product photograph can carry —
   §8.3 forbids dressing one product in another's box.

   It keeps .tile-media's 3/2 and no longer declares 16/10. Measured 2026-08-13 on the
   six live scenes: the box rendered 620 x 413.2 = 1.5006 at every one of the four
   widths, never the 1.6 it declared — the <img> carries width/height attributes of
   1400x933 and the global `img { height: auto }` let its intrinsic ratio set the box,
   so the declaration had never once applied. A slot that declares one ratio and draws
   another is the exact thing check_component_parity exists to catch, and a scene tile
   sits in the same grid as a product tile: same job, same box. Only `cover` differs,
   because a scene has no printed artwork to keep whole. */
.tile-media.scene img { object-fit: cover; }

/* There is deliberately no `.section-gray .tile` override: the ground is measured from
   the photograph, and repainting it puts the seam back. A tile still reads against a
   grey section — #E3E3E3 is 6.4 L* below #F5F5F7. */
.tile.tile-dark { background: var(--ph-black); }
.tile.tile-dark .tile-h, .tile.tile-dark .tile-p { color: var(--ph-white); }
.tile.tile-dark .tile-eyebrow, .tile.tile-dark .tile-price { color: rgba(255,255,255,.72); }
.tile.tile-dark .link-more { color: var(--ph-on-brand); }

/* A light tile inside a black section would have its title painted white by
   .section-black's heading rules and become a name nobody can see — measured on the
   first attempt as "eleven products with no name on them", and fixed there with two
   !importants. The pass refuses to build inside a black section instead, and this
   rule is the belt to that braces: same specificity race, won on order, no
   !important. */
.section-black .tile .tile-h { color: var(--ph-text-1); }
.section-black .tile.tile-dark .tile-h { color: var(--ph-white); }

/* ---- Touch targets ----
   A touch target is at least 44px. Measured: a mega-menu panel link is 14/18 with 5px of
   padding, so 28px, and `.btn.small` is 38px. Neither is a mouse problem — the design
   system's own control heights are 36 · 38 · 44 · 56, so 38 is sanctioned for a pointer —
   but the mega-menu panel is the SAME markup as the mobile drawer, where it sits in flow
   as an accordion, so on a phone those 28px links are the primary navigation.

   Grown only where there is no pointer, so nothing moves on the desktop the sizes were
   tuned for. `min-height` beats `height` in the cascade, which is why .btn.small does not
   need its height overridden. `.link-more` is deliberately not here: it is an inline link
   inside running text, and giving it a 44px box would change every line box it sits in. */
@media (hover: none) {
  .ph-panel .ph-panel-col a { min-height: 44px; display: flex; align-items: center; }
  .btn.small { min-height: 44px; }
  /* The burger was measured at 34 x 31.2px — and of 23,803 interactive elements at 390
     across the whole site with touch actually emulated, 120 of the 132 undersized
     targets were this one control, one per page. It is also the ONLY way to reach the
     navigation on a phone. The bar cluster stays optically centred at 44px (measured:
     centre 23.5 in a 47px box) and the nav row does not grow. */
  .ph-burger { min-width: 44px; min-height: 44px; }

  /* Measured at 390 with touch emulated, which is the only way these show up — a desktop
     browser narrowed to 390 still reports hover, so `@media (hover: none)` never fires and
     the failure stays invisible. Per page: ~50 footer links at 18px, the nav CTA at 28px,
     the brand link at 18px, and every "Learn more" at 21px.

     The nav CTA is the sharpest of them: it is the primary conversion control on all 124
     pages and it is 28px tall — which is not only under the 44px rule, it is not on the
     client's own control scale either (36 · 38 · 44 · 56). Its DESKTOP height is left
     alone here on purpose: changing it moves the header on every page and that is a
     decision to show, not to slip in. */
  .ph-nav-cta { min-height: 44px; }
  .ph-brand { min-height: 44px; display: inline-flex; align-items: center; }
  /* 44px of target and the 8px gap kept, because the rule is 44 WITH 8 between. This
     lengthens the footer on a phone; the more elegant answer is an accordion, and that
     is markup and script rather than CSS. */
  .ph-footer .col a { min-height: 44px; display: flex; align-items: center;
                      margin-bottom: 8px; }
  /* Only where a "Learn more" sits in its own control row. Inline in running text it
     stays inline: a 44px box there would change every line box it appears in. */
  .tile-actions .link-more, .cta-actions .link-more,
  .card .link-more, .card-gray .link-more { min-height: 44px; display: inline-flex;
                                            align-items: center; }
}

/* .grid-2 goes to one column at 900. A tile is a photograph with a caption, and six of
   them in one column at 800px measures 4,380px for a single section — the scroll stops
   being a page and becomes a corridor. So the row holds two columns down to 799, which
   is NOT a new number: it is where shared.css already steps --ph-margin to 24px. The
   tile row keeps two columns for exactly as long as the page keeps its wider margin,
   and collapses when the margin does. Measured after: 1,522px at 800.
   (0,2,0) beats .grid-2's (0,1,0), so no !important and no ordering luck. */
@media (max-width: 900px) {
  .grid.tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 799px) {
  .grid.tiles { grid-template-columns: 1fr; }
}

/* ---- Category hero, centred ----
   The four /store/ category frames open on ONE centred column with an abstract mark
   under the lede, not a two-column split with a product photograph. The split version
   made a three-product category mean whichever carton was to hand, and on
   /store/microbiome/ put the identical file 22px above its own card. The mark is an
   inline <svg> from build/menu_marks.py — the same generator the mega-menu and the
   shop-all tiles use, so this introduces no new visual language. */
.hero-centred { text-align: center; }
.hero-centred > div > .lede { margin-inline: auto; }
.hero-centred .cta-actions { justify-content: center !important; }
.hero-mark { max-width: 300px; margin-top: 40px; }
.hero-mark svg { display: block; width: 100%; height: auto; }
@media (max-width: 799px) { .hero-mark { max-width: 220px; margin-top: 28px; } }

/* The comparison table sits on WHITE inside the grey section, as the four category
   frames draw it. On black it inherited .section-black's inverted rules; on grey it
   needs the card back, or the table's own hairlines disappear into the ground. */
.section-gray .ctable, .section-gray table { background: var(--ph-white);
  border-radius: var(--ph-r18); overflow: hidden; }


/* ================================================== supplement product page (supp)
   Built to docs/wireframe-v2/frames/04-product-pages/product-iron-desktop.png, whose
   copy is transcribed in build/figma-copy/group_k.json. This is NOT the test kits' buy
   block: those frames centre a head above a media/card row, these put the whole
   decision in the right column beside a single colour field. One page uses it today —
   the class is written by render.supp_hero only when build_all names the page.

   THE ACCENT IS THE PRODUCT'S. Each supplement frame is drawn in its own colour — Iron
   rose, Vitamin C tan, Curcumin gold — and it is the only place that colour appears.
   Declared as tokens on the block so a second product is three values, not a stylesheet.
   Iron's are read off the frame: the pack and the closing band are the deep red, the
   panels a tint of it. */
/* OUR PALETTE, NOT THE FRAME'S. The Iron frame is drawn in a rose-red and each of the
   other five supplement frames in a colour of its own — Vitamin C tan, Curcumin gold.
   Bugra, 2026-08-19: "figmadaki ux tasarımındaki renkleri örnek alma, bizim
   websitesinin color sisteminden çıkmayalım." So the layout is the frame's and every
   colour below resolves to a token this site already declares. Nothing is introduced.
   Six products in six invented colours would also have been six palettes on one brand,
   which is the drift the tokens exist to stop. */
.supp { --supp-ink: var(--ph-accent); --supp-tint: var(--ph-surface); --supp-field: var(--ph-surface); }

.supp.hero { padding-bottom: var(--ph-sec-bottom); }
.supp-row {
  display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: stretch;
  margin-top: 40px;
}
/* The colour field keeps its box whether or not a photograph ever lands in it — the
   slot's ratio is the frame's own, and content adapts to the slot, never the reverse. */
/* THE FIELD IS THE HEIGHT OF THE DECISION BESIDE IT. Same rule Bugra set for the test
   kits on 2026-08-18 — two components doing one job are one height at every width — and
   it lands harder here: the buy column runs past 1,300px while 600/560 gave the field
   560, so the hero was two thirds empty on the left. The ratio stays the FLOOR, the row
   sets the height, and there is nothing inside to distort. */
/* .buy-media carries the shared carousel's box — ratio, radius, clip — and this
   overrides only the two things that differ here: the height comes from the decision
   beside it rather than from a ratio, and the ground is the product's field for the
   build where no photograph exists yet. The controls, the dots and their timing are
   the shared component's and are not restated. */
.supp-media {
  aspect-ratio: auto;
  border-radius: var(--ph-r18); background: var(--supp-field);
  min-width: 0; align-self: stretch; overflow: hidden;
  min-height: calc((min(var(--ph-content-max), 100vw - var(--ph-margin) * 2) - 56px) / 2 * 0.9333);
}
.supp-col { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; }
.supp-media img { width: 100%; height: 100%; object-fit: cover; display: block; }

.supp-crumb { font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); color: var(--ph-text-2); }
.supp-badge {
  display: inline-flex; align-items: center; min-height: 32px; margin-top: 16px;
  padding: 0 14px; border-radius: var(--ph-r27);
  background: var(--supp-tint); color: var(--supp-ink);
  font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); font-weight: var(--ph-w-600);
}
.supp-name {
  margin-top: 16px; font-size: var(--ph-h1-size); line-height: var(--ph-h1-lh);
  font-weight: var(--ph-w-600); letter-spacing: var(--ph-h1-tr);
}
.supp-subtitle { margin-top: 8px; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
                 color: var(--ph-text-2); }
.supp-rate { margin-top: 12px; display: flex; align-items: center; gap: 10px;
             font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); color: var(--ph-text-2); }
/* The stars are the frame's own glyphs, in the one colour a rating uses on this site. */
/* The one colour on this page that is not a brand token: a rating's stars are amber
   everywhere, and rendering them in the brand blue would read as a UI state rather than
   as a rating. Declared once, here, so it cannot spread. */
.supp-stars { color: #F5A623; letter-spacing: 2px; }
.supp-claim { margin-top: 28px; font-size: var(--ph-h3-size); line-height: var(--ph-h3-lh);
              font-weight: var(--ph-w-600); max-width: 26ch; }
.supp-para { margin-top: 16px; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
             color: var(--ph-text-2); max-width: var(--ph-measure); }

/* Three figures on one tinted panel — the frame's own shape, and the reason the claim
   above it can stay a sentence rather than becoming a list. */
.supp-stats {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; width: 100%;
  margin-top: 28px; padding: 20px 24px;
  background: var(--supp-tint); border-radius: var(--ph-r14);
}
.supp-stat-v { font-size: var(--ph-fs-19); line-height: var(--ph-lh-19);
               font-weight: var(--ph-w-600); color: var(--supp-ink); }
.supp-stat-l { margin-top: 2px; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
               color: var(--ph-text-2); }

.supp-price { margin-top: 28px; font-size: var(--ph-h2-size); line-height: var(--ph-h2-lh);
              font-weight: var(--ph-w-600); letter-spacing: var(--ph-h2-tr); }
.supp-pnote { margin-top: 6px; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
              color: var(--ph-text-2); }
/* ---- the purchase selector, wireframe-v3 ----
   Two options under the price — one-time against subscribe-and-save — with the
   subscription pre-chosen and carrying a SAVE badge, and the Add to cart button BELOW
   them. It replaces the single "Or subscribe and save" line the v2 frames drew.
   Bugra, shown both exports side by side on 2026-08-21: build all 27 supplement pages
   with this one. The newest design wins, and 27 pages doing the same job may not split
   into two shapes.

   REAL RADIOS, NO JAVASCRIPT. The frame draws a control with a chosen state, so it ships
   as a control: two <input type="radio"> in one named group, everything driven by
   :checked. A static site should not need a script to answer a click, and the keyboard
   and the screen reader both get the real thing for nothing. The input is visually
   hidden but NOT display:none — it has to stay focusable, and .supp-opt:has(:focus-visible)
   draws the ring on the row so the focus is where the eye is.

   The whole row is a <label>, so the hit area is the full 64px-tall card rather than the
   dot — well past the touch-44 floor, unlike the thing that rule keeps catching. */
.supp-opts { width: 100%; margin-top: 20px; display: grid; gap: 10px; }
.supp-opt {
  position: relative; display: grid; align-items: center; cursor: pointer;
  grid-template-columns: 22px 1fr auto; column-gap: 12px; row-gap: 2px;
  padding: 14px 18px; border: 1px solid var(--ph-line); border-radius: var(--ph-r14);
  background: var(--ph-white);
}
/* THE INPUT IS THE WHOLE ROW, not a 1px sliver parked in a corner. It was the sliver
   first, and check_design_language's touch-44 count went 17 -> 73 in one build: 54 new
   controls one pixel square, on 27 pages. The label always made the whole 64px card
   clickable, so the behaviour was fine and the MEASUREMENT was not — and a rule whose
   baseline may only ever be lowered is not something to argue with.
   Stretched to inset:0 it is the real hit area at every width, it stays focusable, and
   opacity:0 keeps the native control from painting over the row we drew. */
.supp-opt input {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  appearance: none; -webkit-appearance: none; opacity: 0; cursor: pointer;
}
.supp-opt-dot {
  position: relative; grid-row: 1 / span 2; width: 20px; height: 20px;
  border-radius: 50%; border: 1px solid var(--ph-line); background: var(--ph-white);
}
/* The filled centre is a ::after, NOT an inset box-shadow. tokens.elevation says
   ELEVATION SISTEMI YOK and check_design_language's `shadow` rule is baseline 0 with two
   named exceptions — the mega-menu panel and the purchasable card. A box-shadow here,
   even an inset one used purely as a dot, computes as a box-shadow and turns that gate
   red. Written the long way on purpose. */
.supp-opt-dot::after { content: ""; position: absolute; inset: 3px; border-radius: 50%;
                       background: transparent; }
.supp-opt-t { font-size: var(--ph-fs-17); line-height: var(--ph-lh-17);
              font-weight: var(--ph-w-600); }
.supp-opt-s { grid-column: 2; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
              color: var(--ph-text-2); }
.supp-opt-p { grid-column: 3; grid-row: 1 / span 2; font-size: var(--ph-fs-17);
              line-height: var(--ph-lh-17); font-weight: var(--ph-w-600); white-space: nowrap; }
.supp-opt-badge {
  display: inline-flex; align-items: center; height: 20px; margin-left: 8px;
  padding: 0 8px; border-radius: var(--ph-r27); background: var(--ph-accent);
  color: var(--ph-white); font-size: var(--ph-fs-12); line-height: 1;
  font-weight: var(--ph-w-600); vertical-align: middle;
}
/* :has() rather than a sibling selector, because the dot and the text are siblings of
   the input but the BORDER belongs to the label above them. */
.supp-opt:has(input:checked) { border-color: var(--ph-accent); background: var(--ph-pill); }
.supp-opt:has(input:checked) .supp-opt-dot { border-color: var(--ph-accent); }
.supp-opt:has(input:checked) .supp-opt-dot::after { background: var(--ph-accent); }
.supp-opt:has(input:focus-visible) {
  outline: var(--ph-focus-w) solid var(--ph-focus); outline-offset: var(--ph-focus-offset);
}
.supp-opt:hover { border-color: var(--ph-accent-hover); }
.supp-cta { display: flex; justify-content: center; width: 100%; margin-top: 24px; }
.supp-sub-link { width: 100%; margin-top: 12px; text-align: center;
                 font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); color: var(--ph-accent); }
.supp-gtee {
  display: flex; gap: 10px; width: 100%; margin-top: 20px; padding: 14px 16px;
  background: var(--supp-tint); border-radius: var(--ph-r12);
  font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); color: var(--ph-text-1);
}
/* The shared tick glyph has no intrinsic size — inside a flex row it stretched to the
   panel's full height and drew a 200px diagonal across the guarantee. Sized here, at
   both the places this page uses it. */
.supp-gtee svg { flex: 0 0 auto; width: 18px; height: 18px; color: var(--supp-ink); }
.supp-tick svg { width: 18px; height: 18px; }
.supp-fine { margin-top: 14px; font-size: var(--ph-fs-13, var(--ph-fs-14));
             line-height: var(--ph-lh-14); color: var(--ph-text-2); }

/* ---- reviews ---- */
.supp-reviews { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
/* margin: 0 — a <figure> carries `margin: 1em 40px` from the UA sheet, and 80px of it
   inside a 404px grid track squeezed a three-line quotation into a nine-line column. */
.supp-review { margin: 0; background: var(--ph-white); border-radius: var(--ph-r18); padding: 28px; }
.section-gray .supp-review { background: var(--ph-white); }
.section:not(.section-gray) .supp-review { background: var(--ph-surface); }
.supp-review blockquote { margin-top: 14px; font-size: var(--ph-fs-17);
                          line-height: var(--ph-lh-17); font-weight: var(--ph-w-600); }
.supp-review figcaption { margin-top: 14px; font-size: var(--ph-fs-14);
                          line-height: var(--ph-lh-14); color: var(--ph-text-2); }
.supp-score { display: flex; align-items: baseline; justify-content: center; gap: 10px;
              margin-bottom: 32px; font-size: var(--ph-fs-21); line-height: var(--ph-lh-21); }
.supp-score span:last-child { font-size: var(--ph-fs-14); color: var(--ph-text-2); }

/* ---- why it's different ---- */
.supp-diff { text-align: center; }
.supp-diff .lede { margin-inline: auto; }
.supp-whys { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px;
             margin-top: 44px; text-align: left; }
.supp-why { background: var(--ph-surface); border-radius: var(--ph-r18); padding: 28px; }
.supp-tick {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--supp-ink); color: var(--ph-white); margin-bottom: 20px;
}
.supp-why .card-title { margin-bottom: 8px; }

/* ---- inside each sachet + what it does, side by side on one ground ---- */
.supp-inside-row { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: start; }
.supp-card { background: var(--ph-white); border-radius: var(--ph-r18); padding: 32px; }
.supp-eyebrow { font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
                font-weight: var(--ph-w-600); color: var(--supp-ink); margin-bottom: 20px; }
.supp-spec { display: flex; justify-content: space-between; gap: 24px; padding: 16px 0;
             border-top: 1px solid var(--hair); font-size: var(--ph-fs-17);
             line-height: var(--ph-lh-17); }
.supp-spec span { color: var(--ph-text-2); }
.supp-list { margin-top: 24px; }
.supp-list li { position: relative; padding-left: 22px; margin-top: 20px; list-style: none; }
/* The frame's bullet is a small accent dot, not a tick: a tick on this site asserts the
   line under it is TRUE, and these are descriptions of what the product does. */
.supp-list li::before { content: ""; position: absolute; left: 0; top: 9px;
                        width: 8px; height: 8px; border-radius: 50%; background: var(--supp-ink); }
.supp-list b { display: block; font-size: var(--ph-fs-17); line-height: var(--ph-lh-17); }
.supp-list span { display: block; margin-top: 4px; font-size: var(--ph-fs-17);
                  line-height: var(--ph-lh-17); color: var(--ph-text-2); }

/* ---- the closing band ---- */
/* The closing band is the site's black — its one dark moment, the same punctuation
   .section-black is everywhere else. Recorded 2026-08-17: black is a site colour.
   Bugra looked at a light version on 2026-08-19 and put the black back. */
.supp-end { background: var(--ph-black); color: var(--ph-white); text-align: center; }
.supp-end .sec-title, .supp-end .lede { color: var(--ph-white); }
.supp-end .lede { margin-inline: auto; }
.supp-end-price { margin-top: 24px; font-size: var(--ph-fs-19); line-height: var(--ph-lh-19);
                  font-weight: var(--ph-w-600); }
/* THE CONTROL IS .btn.ghost, NOT A COLOUR OVERRIDE. `.btn` forces
   `color: var(--ph-white) !important`, so a white pill built by overriding its
   background alone came out white-on-white — a button with no visible label, which is
   what Bugra saw. Nothing declared here could win that, and answering an !important
   with another !important would leave the next person the same trap. `.ghost` is the
   variant this site already has for exactly this: white ground, dark label, and its own
   !important on the colour so it resolves cleanly. On black it reads at 16:1. */
.supp-end-cta { display: inline-flex; margin-top: 28px; }
.supp-end-note { margin-top: 16px; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
                 color: rgba(255,255,255,.82); }
.supp-end-note + .supp-end-note { margin-top: 20px; color: rgba(255,255,255,.66); }

@media (max-width: 1279px) {
  .supp-row, .supp-inside-row { grid-template-columns: 1fr; gap: 32px; }
  .supp-media { aspect-ratio: 4 / 3; }
}
@media (max-width: 899px) {
  .supp-reviews, .supp-whys { grid-template-columns: 1fr; }
  .supp-stats { grid-template-columns: 1fr; gap: 16px; }
}

/* ==================================================== tile homepage (atiles)
   An ALTERNATIVE homepage beside the live film one and home-classic.html.

   Numbers below are at 1440x900, 2026-08-19. Bugra named three faults, all of which
   came from one mistake: a tile's copy anchors to its TOP and this block was centring
   it. Everything else he saw — tiles too tall, text in the wrong place, gaps wrong —
   followed from that.

   THE TILE IS A FIXED HEIGHT, NOT A MINIMUM. 692 for a full tile, 580 for a half, at
   1440. Written as min-height it grows with its content, which is how our half tiles
   came out 698 against 580 and why nothing lined up across a row.

   THE COPY BLOCK STARTS AT THE PADDING AND STOPS. Measured top-of-tile to top-of-
   headline: 56 on a full tile, 53 on a half — exactly the padding-top, because the
   content is the first thing in the box. Then 6 to the subhead and 17 to the controls
   on a full tile; 3 and 12 on a half. The art hangs below in what is left.

   THE SUBHEAD IS NOT GREY. Measured rgb(29,29,31) — the same colour as the headline,
   on both bands. Ours was on --ph-text-2 and that one value is most of what made the
   two pages look different at a glance. It is --ph-text-1 here, which IS #1D1D1F.

   THE CONTROLS ARE PILLS. 44px tall at 17px with 11/21 padding on a full tile, 36px at
   14px with 8/15 on a half, radius 980px. Primary is #0071E3 with white type — which is
   this site's own --ph-accent to the digit — and secondary is the same blue as a 1px
   outline on nothing. The wireframe draws chevron text links instead; this is the one
   place the tile departs from it, by brief.

   Two breakpoints govern all of it — 734/735 and 1068/1069 — and 12px is the only
   spacing constant in the grid: column gap, row gap, gap between sections, page margin.
   Type does not scale; three fixed steps. Radius is zero, and that is not an override —
   no tile selector declares a border-radius.

   The sizes live in classes, never inline: fix_type_scale.py snaps inline px onto this
   site's ladder and 56 and 28 are not on it, so an inline value would be quietly
   rewritten to 48 and 24 and the geometry would break.

   RENAMED to .atile because `.tile` ALREADY EXISTS on this site — build_tiles.py's photo
   tiles, on /index.html, /testing/ and three more — and this block sits LATER in the
   file, so for one build its rules were winning on those pages. Caught by measuring, not
   by a gate: nothing here compares a page against the build before it. */
.atiles {
  --atile-gap: 12px;
  --atile-hero-h: 580px;          /* the short-window branch; the tall ones are below */
  --atile-promo-h: 580px;
  --atile-pad-t: 48px; --atile-pad-b: 56px;
  --atile-promo-pad-t: 53px; --atile-promo-pad-b: 60px;
  display: grid; gap: var(--atile-gap);
  padding-inline: var(--atile-gap);
}
.atiles-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--atile-gap); }
.atiles-row.one { grid-template-columns: 1fr; }

.atile {
  position: relative; overflow: clip;
  display: flex; align-items: flex-start; justify-content: center;
  text-align: center;
  background: var(--ph-surface); color: var(--ph-text-1);
  height: var(--atile-promo-h);
  padding: var(--atile-promo-pad-t) 0 var(--atile-promo-pad-b);
}
.atiles-row.one > .atile, .atile.full {
  height: var(--atile-hero-h);
  padding: var(--atile-pad-t) 0 var(--atile-pad-b);
}
/* Black is a site colour — recorded 2026-08-17, "siyah renk kullanabiliriz". */
.atile.dark { background: #000; color: var(--ph-white); }
.atile.dark .atile-sub { color: var(--ph-white); }
.atile.dark .atile-eyebrow { color: var(--ph-on-brand); }

.atile-body { position: relative; z-index: 2; display: flex; flex-direction: column;
              align-items: center; padding-inline: 24px; max-width: 100%; }
/* EVERY CHILD STARTS AT ZERO. The measured gaps came out 52 and 45 where the CSS says 6
   and 17, because an <h2> takes `margin: .83em 0` from the UA sheet — 46.48px at 56px
   type — and this site's own `p` rule adds 14 above and 28 below. Both are invisible in
   the declarations here and both are larger than the values they were burying. The
   spacing on this block is the measured ladder and nothing else may contribute. */
.atile-body > * { margin: 0; }
.atile-eyebrow { font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
                 font-weight: var(--ph-w-600); letter-spacing: .06em;
                 color: var(--ph-accent); margin-bottom: 8px; }

/* ---- type: three fixed steps, measured ---- */
.atile-head { font-size: 40px; line-height: 44px; font-weight: var(--ph-w-600);
              letter-spacing: normal; }
.atile-sub  { margin-top: 3px; font-size: 21px; line-height: 25px;
              font-weight: var(--ph-w-400); color: var(--ph-text-1); }
.atile.full .atile-head { font-size: 56px; line-height: 60px; letter-spacing: -0.28px; }
.atile.full .atile-sub  { margin-top: 6px; font-size: 28px; line-height: 32px; }

/* ---- controls: pills, at two sizes ---- */
.atile-ctas { display: flex; justify-content: center; column-gap: 14px; margin-top: 12px; }
.atile.full .atile-ctas { column-gap: 17px; margin-top: 17px; }
.atile-link {
  display: inline-flex; align-items: center; justify-content: center;
  height: 36px; padding: 0 15px; border-radius: 980px;
  font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); font-weight: var(--ph-w-400);
  background: var(--ph-accent); color: var(--ph-white); border: 1px solid transparent;
}
.atile-link.ghost { background: transparent; color: var(--ph-accent);
                    border-color: var(--ph-accent); }
.atile.full .atile-link { height: 44px; padding: 0 21px;
                          font-size: var(--ph-fs-17); line-height: var(--ph-lh-17); }
/* On a dark tile the secondary lifts rather than staying at the light value. */
.atile.dark .atile-link.ghost { color: var(--ph-on-brand); border-color: var(--ph-on-brand); }

/* 🔴 THESE ELEVEN CONTROLS HAD NO HOVER AT ALL, on the front door, and the global
   a:hover then wrote the button's own fill colour onto its label: white-on-#0071E3
   became #0071E3-on-#0071E3 and the word disappeared, underline and all. That is the
   defect Bugra screenshotted on 2026-08-20 — an empty blue pill.
   The order below is deliberate and is not tidiness. `.atile-link.ghost:hover` and
   `.atile.dark .atile-link.ghost` are BOTH (0,3,0): a tie, and a tie is decided by
   source order, which is the failure this file has already paid for once. So the
   dark tile gets its own rule at four classes rather than relying on where it sits. */
.atile-link:hover  { background: var(--ph-accent-hover);  text-decoration: none; }
.atile-link:active { background: var(--ph-accent-active); }
.atile-link.ghost:hover {
  background: var(--ph-accent-hover); border-color: transparent;
  color: var(--ph-white); text-decoration: none;
}
.atile-link.ghost:active { background: var(--ph-accent-active); border-color: transparent; }
.atile.dark .atile-link.ghost:hover {
  background: var(--ph-accent-hover); border-color: transparent; color: var(--ph-white);
}
.atile.dark .atile-link.ghost:active { background: var(--ph-accent-active); }

/* There is no measured number for label pills. They
   take the ladder's own step instead — the same 12 a half tile puts between its subhead
   and its controls, 17 on a full tile — so the block keeps one rhythm rather than
   introducing a value nothing else on the page uses. */
.atile-pills { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 12px; }
.atile.full .atile-pills { margin-top: 17px; }
.atile.dark .pill-lab { background: rgba(255,255,255,.12); color: var(--ph-white); }

/* The art hangs in what the copy leaves, anchored to the tile's bottom padding — a
   media band at `top:auto;bottom:0`. Absolute so
   it can never push the tile past its fixed height. */
/* THE FILM IS THE TILE'S GROUND, UNDER THE COPY. Anchored to the bottom so the drawing
   sits in the space the copy leaves, exactly where the art band does, and clipped by the
   tile's own `overflow: clip` so it can never push the fixed height. object-fit: cover
   because the band is 3.65:1 and the widest ratio the model offers is 2.33:1 — the film
   was composed with its action in the central strip precisely so this crop takes nothing.
   z-index 1: above the tile's ground, below .atile-body's 2. */
/* A TILE THAT CARRIES A FILM TAKES THE FILM'S PAPER, NOT THE TOKEN'S. The crayon film's
   paper measures #F6F5F9 — median across six frames spanning all 25 seconds, sampled
   across the empty top band, so it is the film's colour and not one frame's grain. The
   surface token is #F5F5F7. Two points apart, and Bugra saw the seam where the film's
   top edge meets the tile above it.
   Matching the TILE to the FILM rather than the other way round: it is one value and it
   is exact, where re-grading the film would leave a point or two of drift every time it
   is re-encoded. Scoped with :has() to tiles that actually carry a film, so the token
   and every other surface on the site are untouched.

   #F7F7F9, and the number came from the SCREEN rather than from the file. Measured off
   the video's own pixels the paper is #F6F5F9; measured off a 2x screenshot of the
   rendered page it is #F7F7F9, because the decode and the display profile move it. The
   seam is a thing a person sees on a screen, so the screen is what it has to be measured
   on. Bugra read #F9F9FB sampling a screenshot in Figma — the same effect one step
   further along, which is why three readings of one colour disagree. */
.atile:has(> .atile-film):not(.dark) { background: #F7F7F9; }

/* TWO KNOBS, BOTH REVERSIBLE, AND NOTHING ELSE CHANGES WITH THEM.
     --atile-film-fit    cover = fill the band and crop what does not fit
                         contain = show the whole frame, crop nothing
     --atile-film-zoom   1 = as it comes; below 1 pulls back from it
   Tried at contain + 0.7 on 2026-08-19 and REVERTED the same minute — Bugra: "bunu neden
   bozdun, bu çok güzeldi zaten, bunu geri al." The knobs stay because the experiment was
   worth being able to run in one line and undo in one line; the defaults are the build
   he approved.
   The gap a smaller film leaves is invisible because the tile's ground and the film's
   paper are the same measured colour — that is what the #F7F7F9 work bought. */
.atiles { --atile-film-fit: cover; --atile-film-zoom: 1; }
.atile-film {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  width: 100%; height: 72%;
  object-fit: var(--atile-film-fit); object-position: center bottom;
  transform: scale(var(--atile-film-zoom)); transform-origin: center bottom;
  pointer-events: none;
}

.atile.dark .atile-film { height: 100%; object-position: center; opacity: .9; }
/* ONE FILM ON A DARK TILE IS A BAND UNDER THE COPY RATHER THAN THE WHOLE TILE, and it
   asks by name rather than by changing the rule above it. That rule was written for the
   Longevity AI tile, whose film IS the tile's ground; The Science tile is dark too, so it
   inherited height:100% and object-position:center and its globe sat straight over the
   heading and the button. Widening or narrowing the shared rule to fix one tile is the
   module-level-flag shape CLAUDE.md opens with — so the shared rule is untouched and the
   tile that wants something else says so.

   FOUR CLASSES DELIBERATELY. `.atile.dark .atile-film` is (0,3,0); a modifier on its own
   loses, and a tie would leave the outcome to source order. This project has already paid
   for that once — .atile-pic--inset lost a specificity tie and its declarations read as
   correct while the browser did something else.

   `--atile-copy` is the button's last pixel plus 24px of air, measured off the built page
   and already used by the inset picture variant, so the band starts under the copy at
   every width without a media query. `contain` is what stops the clip from ever growing
   into that copy: see build/kits/tile_video.py for why cover cannot, and why the bars it
   leaves are invisible on this tile and only on this tile. */
.atile.dark .atile-film.atile-film--band {
  /* calc, NOT `top` + `height:auto`. That pair reads as "stretch between the copy and the
     bottom" and does not: for an absolutely positioned REPLACED element, `height:auto`
     means the intrinsic height and `bottom` is then dropped as over-constrained (CSS 2.1
     10.6.5). Measured: the film came out 725px tall inside a 580px tile and hung out of
     the bottom of it. An explicit height with `bottom:0` is the same intent, honoured. */
  height: calc(100% - var(--atile-copy)); bottom: 0;
  object-fit: contain; object-position: center;
}

.atile-art { position: absolute; left: 0; right: 0; bottom: var(--atile-promo-pad-b);
             top: auto; display: flex; align-items: flex-end; justify-content: center;
             pointer-events: none; }
.atile.full .atile-art { bottom: var(--atile-pad-b); }
.atile-art svg { max-width: 100%; height: auto; }
/* A tile's product picture. On a LIGHT tile it is composed on the tile's own ground and
   sits flush, invisible as an edge. On a DARK tile it stays a picture — the kits were
   photographed on a light backdrop and there is no honest way to cut a soft drop shadow
   off printed ink, so it is inset and rounded and reads as a photograph placed on the
   tile rather than as boxes floating in black. */
/* THE PICTURE IS THE TILE, NOT A THING INSIDE IT. Bugra, 2026-08-19: a picture in a
   tile either spreads across the whole tile or dissolves into its ground, and reads
   as one whole.

   He is right and it is the whole difference between a tile and a slide. Never
   float a bordered rectangle inside a panel: the photograph goes edge to edge and the
   copy sits ON it, or the subject dissolves into the panel's own ground so the two read
   as one surface. The first build did neither — it inset a 3:2 crop with rounded corners
   and left a visible frame, and the one tile that DID work (Genetic testing) worked by
   accident, because its photograph was black and the tile was black.

   So the picture is full-bleed and the images are composed for it: each is generated
   with its TOP HALF as the tile's exact flat ground — #F7F7F9 or #000 — and the subject
   only in the bottom half, fading into that ground with no edge. The copy then sits over
   empty ground and the subject is simply the lower part of the tile. There is no seam to
   hide because there is no second rectangle.

   object-position: bottom keeps the subject anchored as the tile's ratio changes across
   the three bands; the empty half is what gets cropped, which costs nothing. */
.atile-pic { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
/* ONE TILE KEEPS THE INSET PRESENTATION IT WAS APPROVED IN. Genetic testing was built
   before the full-bleed rule and Bugra approved it as it stood — a picture sitting in
   the tile with black around it and a radius. When every tile went full-bleed this one
   went with them, which changed a design he had already signed off. The FILE was never
   touched; only how it is presented was, and that is the thing he was pointing at.
   Restored here rather than by editing the image, so it can never drift again: the
   modifier is written by the generator for that tile alone. */
.atile-pic.atile-pic--inset { inset: var(--atile-copy) 0 var(--atile-promo-pad-b); display: flex;
                    align-items: center; justify-content: center; padding-inline: 24px; }
.atile.full .atile-pic.atile-pic--inset { inset: var(--atile-copy) 0 var(--atile-pad-b); }
/* HALF THE TILE'S WIDTH, AT THE PICTURE'S OWN RATIO. Measured off the build Bugra
   approved: the image ran about 49% of the tile's width and kept its native 3:2, sitting
   in the lower third with a wide black margin either side. Restoring it by height alone
   put it back at 93% and stretched to 2.02 — right position, wrong size, which is why it
   still did not look like the thing he was pointing at. Width is the governing dimension
   here and the height follows it. */
.atile-pic.atile-pic--inset img { width: 52%; height: auto; max-height: 100%; object-fit: contain;
                        border-radius: var(--ph-r18); }
.atile-pic img { width: 100%; height: 100%; object-fit: cover; object-position: center bottom;
                 display: block; }
/* THE PICTURE HANGS UNDER THE COPY INSTEAD OF BEHIND IT. Bugra, 2026-08-20, on the
   microbiome tile: *"küçült. textle karışmasın."* At `inset: 0`
   the frame is the whole tile and `cover` fills it, so a subject that happens to be tall
   and centred rises straight through the heading — which is what he screenshotted.

   This is the SAME band `.atile-film--band` already uses: the media anchors to the
   bottom and leaves the copy its own air. The
   band starts at `--atile-copy` (the button's last pixel plus air, measured off the built
   page) so it tracks the copy at every width with no media query, and `contain` is what
   guarantees the picture can never grow back into it.

   ⚠️ It is a MODIFIER, not a change to `.atile-pic`, and the generator writes it for one
   tile. `.atile-pic` reaches seven tiles; widening this to all of them would re-crop four
   approved pictures to fix one, which is the module-level-flag shape CLAUDE.md opens with —
   and the same reasoning that gave genetic testing its own `--inset` rather than a global.

   The margins this leaves at the sides are invisible ONLY because the asset's ground is
   the tile's ground to the byte — measured, not eyeballed, because two parts in 255 draws
   the box. A picture given this modifier without a matched ground becomes exactly the
   floating rectangle the tile rule forbids. */
/* --band-grow enlarges the picture; --band-lift decides where the extra height comes
   FROM. Bugra, 2026-08-20: "şunu bi tık aşağı çekerek %25 oranında görseli büyüt", then
   "sorun değil azıcık yukarı çekebilirsin, çoğunlukla aşağı çek sadece. denge kur yani."
   So the growth is 25% and it is SPLIT: 10px of it upward, the rest downward.

   THE LESSON THIS ENCODES — docs/memory/phenome-homepage-tiles.md, written after the ring
   was shrunk about its centre and appeared to move the one edge he had called untouchable:
   when he asks for something to grow, establish WHICH EDGE IS FIXED. Here he answered it
   himself, so it is a variable rather than a guess, and a later "a bit more" is one number.

   The bottom lands NEGATIVE — about 11px past the tile's own edge at 1440 — and `.atile`
   is `overflow: clip`, so the tile trims it. What gets trimmed is measured, not hoped:
   the dish's lowest pixel sits 94px above the asset's bottom edge (14% of the frame), and
   11px of band is 18 asset rows, so only flat ground is lost. Check that again before
   raising --band-grow; past roughly 1.45 the clip starts eating the dish.

   Written as calc against the SAME tokens the resting band uses, so it tracks every width
   with no media query: at --band-grow: 1 and --band-lift: 0 it collapses back to
   `inset: var(--atile-copy) 0 var(--atile-promo-pad-b)` exactly, which is the identity
   check to run if this ever looks wrong. */
.atile-pic.atile-pic--band {
  --band-grow: 1.25;
  --band-lift: 10px;
  inset: calc(var(--atile-copy) - var(--band-lift)) 0
         calc((1 - var(--band-grow)) * 100%
              + (var(--band-grow) - 1) * var(--atile-copy)
              + var(--band-grow) * var(--atile-promo-pad-b)
              + var(--band-lift));
}
.atile.full .atile-pic.atile-pic--band {
  inset: calc(var(--atile-copy) - var(--band-lift)) 0
         calc((1 - var(--band-grow)) * 100%
              + (var(--band-grow) - 1) * var(--atile-copy)
              + var(--band-grow) * var(--atile-pad-b)
              + var(--band-lift));
}
.atile-pic.atile-pic--band img { object-fit: contain; object-position: center bottom; }
/* Where the copy stops. Half tile: 53 pad + 44 head + 3 + 25 sub + 12 + 36 cta = 173,
   plus 24 of air. Full tile: 56 + 18 eyebrow + 8 + 60 head + 6 + 32 sub + 17 + 44 = 241,
   plus 32. Measured against the built page, not estimated. */
.atiles { --atile-copy: 197px; }
.atile.full { --atile-copy: 273px; }

/* the two tiles whose content is a card rather than a drawing */
.atile-card {
  margin-top: 24px; max-width: 520px; text-align: left;
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.12);
  border-radius: var(--ph-r18); padding: 20px;
}
.atile-card-eyebrow { font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
                      color: rgba(255,255,255,.72); }
.atile-card-q { margin-top: 12px; padding-bottom: 12px;
                border-bottom: 1px solid rgba(255,255,255,.12);
                font-size: var(--ph-fs-19); line-height: var(--ph-lh-19);
                font-weight: var(--ph-w-600); text-align: center; }
.atile-card-a { margin-top: 12px; font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
                color: rgba(255,255,255,.72); }
.atile-arts { margin-top: 20px; display: flex; flex-direction: column; gap: 10px;
              width: 100%; max-width: 360px; }
.atile-art-card { display: flex; align-items: center; gap: 12px; text-align: left;
                  background: var(--ph-white); border-radius: var(--ph-r12); padding: 12px 14px; }
.atile-dot { flex: 0 0 auto; width: 10px; height: 10px; border-radius: 50%; }
.atile-art-eyebrow { font-size: 15px; line-height: 20px; letter-spacing: .04em;
                     color: var(--ph-text-2); }
.atile-art-title { font-size: var(--ph-fs-14); line-height: var(--ph-lh-14);
                   font-weight: var(--ph-w-600); color: var(--ph-text-1); }

/* ---- the two breakpoints, and only these two ---- */
@media (min-width: 1069px) and (min-height: 776px) {
  .atiles { --atile-hero-h: 692px; --atile-pad-t: 56px; --atile-pad-b: 63px; }
}
@media (min-width: 735px) and (max-width: 1068px) {
  .atiles { --atile-hero-h: 570px; --atile-promo-h: 490px;
            --atile-promo-pad-t: 44px; --atile-promo-pad-b: 48px; }
  .atile.full .atile-head { font-size: 48px; line-height: 52px; letter-spacing: -0.096px; }
  .atile.full .atile-sub  { margin-top: 4px; font-size: 24px; line-height: 28px; }
  .atile-head { font-size: 32px; line-height: 36px; letter-spacing: 0.064px; }
  .atile-sub  { margin-top: 4px; font-size: 19px; line-height: 23px; }
}
@media (min-width: 735px) and (max-width: 1068px) and (min-height: 734px) {
  .atiles { --atile-hero-h: 650px; --atile-pad-t: 61px; --atile-pad-b: 69px; }
}
@media (max-width: 734px) {
  /* One column, no page margin, and the two type ladders MERGE — below 735 a full
     tile's headline and a half tile's are the same 32/36. */
  .atiles { padding-inline: 0;
            --atile-hero-h: 500px; --atile-promo-h: 500px;
            --atile-pad-t: 39px; --atile-pad-b: 43px;
            --atile-promo-pad-t: 39px; --atile-promo-pad-b: 43px; }
  .atiles-row { grid-template-columns: 1fr; }
  .atile-head, .atile.full .atile-head { font-size: 32px; line-height: 36px; letter-spacing: 0.064px; }
  .atile-sub,  .atile.full .atile-sub  { margin-top: 4px; font-size: 19px; line-height: 23px; }
  .atile-ctas, .atile.full .atile-ctas { column-gap: 14px; margin-top: 11px; }
  .atile.full .atile-link { height: 36px; padding: 0 15px;
                            font-size: var(--ph-fs-14); line-height: var(--ph-lh-14); }
}

/* ==========================================================================
   THE SEARCH OVERLAY — .ph-search
   ==========================================================================
   The glass in the bar opened /hub/search/, a page that cannot search, and it did it
   by throwing away whatever the reader was on. This does the opposite: the bar's own
   contents step aside, a
   field takes the row, and a sheet drops under it holding somewhere to go.

   It is built as a sibling INSIDE .ph-nav-inner and positioned against .ph-nav — the
   same containing block the mega-menu panels already use, so the sheet's top edge and
   a panel's top edge are the same line by construction rather than by arithmetic.

   The chrome is the nav's, not a new kind: the same translucent white, the same blur,
   the same hairline. The one thing it does NOT borrow is the panel's shadow — the
   sheet is full-bleed and meets the page edge to edge, so there is nothing for a drop
   to separate it from. `elevation YOK` holds wherever it can. */
/* REBUILT 2026-09-15, by request.
   The bar is left alone: the wordmark, the links and the icons stay where they
   are, and a panel drops from under the bar holding one large borderless field
   and the quick links. Everything below the panel is frosted, not dimmed, so
   the page is still there but cannot be read or clicked through.
   What is deliberately missing, all by request: a placeholder, a Cancel button,
   a glyph on each link, and the section name that used to sit at a row's end.
   The links are set at the size of their own heading, and a hovered row takes a
   wash of the brand blue rather than grey. */
.ph-search {
  position: absolute; left: 0; right: 0; top: 100%;
  z-index: 2;
}
.ph-search[hidden] { display: none; }
.ph-search-sheet {
  position: relative;
  /* WHITE, 2026-09-16 by request. It was rgba(251, 252, 254, .97) — a blue an
     eighth of a step off white, invisible on its own and unmistakable next to
     the white page it drops over. The mega-menu sheet now carries the same
     value, in phenome-glass.css, so the two overlays the bar opens are one
     material. */
  background: rgba(255, 255, 255, .96);
  -webkit-backdrop-filter: saturate(180%) blur(30px);
  backdrop-filter: saturate(180%) blur(30px);
  box-shadow: 0 1px 0 rgba(16, 33, 75, .06);
  max-height: calc(100vh - var(--ph-nav-h)); overflow-y: auto;
  overscroll-behavior: contain;
  animation: ph-search-drop .42s cubic-bezier(.2, .8, .2, 1) both;
}
/* The same container formula as .ph-nav-inner and .wrap-wide, so the field
   starts on the wordmark's left line. */
.ph-search-sheet-inner {
  width: 100%; max-width: calc(var(--ph-content-max) + var(--ph-margin) * 2);
  margin-inline: auto; padding-inline: var(--ph-margin);
  padding-block: 34px 56px;
}

/* The field: no box, no rule, no fill. The glass and the type are the field. */
.ph-search-field {
  display: flex; align-items: center; gap: 14px;
  height: 56px; padding: 0;
  background: none; border: 0; border-radius: 0;
}
.ph-nav .ph-search .ph-search-glass { flex: 0 0 auto; width: 26px; height: 26px; color: var(--ph-text-2); }
.ph-nav .ph-search .ph-search-input {
  flex: 1 1 auto; min-width: 0; height: 100%;
  border: 0; background: none; padding: 0; margin: 0; box-shadow: none;
  font: inherit; font-size: clamp(24px, 2.3vw, 32px); line-height: 1.2;
  font-weight: var(--ph-w-600); letter-spacing: -0.015em;
  color: var(--ph-text-1); caret-color: var(--ph-accent);
}
.ph-search-input::-webkit-search-decoration,
.ph-search-input::-webkit-search-cancel-button { -webkit-appearance: none; }
.ph-search-input:focus,
.ph-search-input:focus-visible { outline: none; }
.ph-search-clear {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0; border: 0; border-radius: 980px;
  background: rgba(16, 33, 75, .1); color: var(--ph-text-1); cursor: pointer;
  transition: background var(--ph-dur-fast) var(--ph-ease);
}
/* A class that sets `display` outranks the UA sheet's `[hidden] { display: none }`,
   so a control that carries a display and is toggled by `hidden` has to say so. */
.ph-search-clear[hidden] { display: none; }
.ph-search-clear:hover { background: rgba(16, 33, 75, .18); }

/* The heading and its links are one size, 17, by request. The heading is told
   apart by ink and weight, never by being smaller. */
.ph-nav .ph-search .ph-search-head {
  margin: 30px 0 8px;
  font-size: 17px; line-height: 24px; font-weight: var(--ph-w-400);
  letter-spacing: normal; text-transform: none;
  color: var(--ph-text-2);
}
.ph-search-list { list-style: none; margin: 0 0 0 -12px; padding: 0; max-width: 680px; }
.ph-nav .ph-search .ph-search-row {
  display: flex; align-items: center;
  min-height: 42px; padding: 9px 12px;
  border-radius: 10px; color: var(--ph-text-1);
  font-size: 17px; line-height: 24px; font-weight: var(--ph-w-500, 500);
  text-decoration: none;
  transition: background-color var(--ph-dur-fast) var(--ph-ease), color var(--ph-dur-fast) var(--ph-ease);
}
.ph-search-row:hover { text-decoration: none; }
/* Pointer and keyboard land on the SAME state: the script marks a row on
   mouseenter and on ArrowDown, so there is one highlight and never two. */
.ph-nav .ph-search .ph-search-row.on {
  background: rgba(32, 58, 133, .08);
  color: var(--phenome-blue-500, var(--ph-accent));
}
.ph-search-label { flex: 1 1 auto; min-width: 0; }
/* The run that matched, marked so the reader can see why the row is there. */
.ph-search-label b { font-weight: var(--ph-w-600); }
.ph-nav .ph-search .ph-search-none {
  padding: 9px 12px; color: var(--ph-text-2);
  font-size: 17px; line-height: 24px;
}

/* Below the panel, the page is frosted rather than darkened.

   THE MEGA-MENU USES THE SAME VEIL, 2026-09-16 by request. It is one class with
   two names rather than two frosts that would drift: shared.js hangs
   .ph-menu-scrim on <body> while a panel is open exactly as it hangs
   .ph-search-scrim while the search is. The tint went from rgba(236, 239, 246)
   — a pale blue, which is what made the open bar read as blue however white the
   sheet above it was — to white at the same weight. */
html.ph-search-open .ph-bar,
html.ph-menu-open .ph-bar { opacity: 0; visibility: hidden; }
.ph-search-scrim,
.ph-menu-scrim {
  position: fixed; inset: 0; z-index: 98;
  background: rgba(255, 255, 255, .42);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  backdrop-filter: blur(20px) saturate(140%);
  animation: ph-search-veil .32s var(--ph-ease) both;
}
.ph-menu-scrim[hidden] { display: none; }

/* THE WELCOME BAR UNDER THE FROST, 2026-09-16 by request.

   The scrim already covered it — it is a static band and the scrim is z-index
   98 — and it still read as a hard blue stripe across an otherwise even sheet
   of frost. The reason is that a blur has nothing to work with: the band is
   ONE flat colour edge to edge, so blurring it returns the same flat colour,
   and a 38% pale veil over Phenome Blue is still unmistakably Phenome Blue.
   Everything else on the page dissolves; the one element that cannot dissolve
   is the one element whose job was to be noticed.

   So the band steps back on its own while the search is open, to the same
   pale blue grey the frost resolves the rest of the page to, and its ink and
   its code chip fade with it. The scrim then lands on a viewport that is one
   even tone, which is what "uniform" means here. It is a transition, not a
   swap, so closing the search brings the offer back rather than blinking it
   on. `html.ph-search-open` is already set for the product bar above, so this
   costs no new state. */
.ph-promo {
  transition: background-color var(--ph-dur-base, .32s) var(--ph-ease),
              color var(--ph-dur-base, .32s) var(--ph-ease);
}
/* NEUTRAL, 2026-09-16: #e7ebf4 was the pale blue the frost resolved the page to
   while the ground was still blue. The frost is white now, so the band steps
   back to the neutral rung one off white instead, and the viewport under an
   open overlay is one even tone again. The mega-menu is held to the same rule,
   because it opens over the same band. */
html.ph-search-open .ph-promo,
html.ph-menu-open .ph-promo { background: var(--ph-surface); color: rgba(16, 33, 75, .30); }
html.ph-search-open .ph-promo b,
html.ph-menu-open .ph-promo b { background: rgba(16, 33, 75, .06); color: inherit; }
.ph-search-scrim[hidden] { display: none; }
@keyframes ph-search-veil { from { opacity: 0; } to { opacity: 1; } }
/* The panel unrolls from the bar, and its contents follow a beat behind. */
@keyframes ph-search-drop {
  from { clip-path: inset(0 0 100% 0); }
  to   { clip-path: inset(0 0 0 0); }
}
.ph-search-field,
.ph-search-head,
.ph-search-list { animation: ph-search-rise .5s cubic-bezier(.2, .8, .2, 1) both; }
.ph-search-head { animation-delay: .06s; }
.ph-search-list { animation-delay: .1s; }
@keyframes ph-search-rise {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .ph-search-sheet, .ph-search-scrim,
  .ph-search-field, .ph-search-head, .ph-search-list { animation: none; }
}
@media (max-width: 900px) {
  .ph-search-sheet-inner { padding-block: 22px 36px; }
  .ph-search-field { height: 48px; gap: 12px; }
  .ph-nav .ph-search .ph-search-glass { width: 22px; height: 22px; }
  .ph-nav .ph-search .ph-search-head { margin-top: 20px; }
  .ph-search-sheet { max-height: calc(100svh - 48px); }
}

/* The product bar's name is a control, not a caption: the bar only exists once the
   hero has scrolled away, which is exactly when a reader wants the top of the page
   back, and the name is the largest thing in it. The reset is everything a <button>
   brings that a <span> did not; the type rules above it still apply. */
.ph-bar-name {
  border: 0; background: none; padding: 0; margin: 0;
  font-family: inherit; cursor: pointer; text-align: left;
}
.ph-bar-name:hover { color: var(--ph-accent-link); }

/* ================================================= MEGA MENU
   2026-09-22 by request: the dropdown, at 1440, keeps ONE picture card at the
   right end of the line (the user's condition).

     sheet     content starts on the bar's own left line, 40px under the bar
     titles    grey, regular, 16px above the first link
     column 1  "Explore …", links 24/28 at 600 on a 38px pitch; a `small`
               link closes the column at the small size, 12px below
     cols 2-3  "Shop …" / "More from …", links 600 on a 28px pitch
     gaps      88px after the first column, 44px between the others
     no bar    the open trigger is marked by its ink alone

   The small size would be 12px; this site's floor is 15, so the small links and
   the titles are 15. Declared LAST so it overrides the older panel rules; the
   ≤900 drawer keeps its own block at the foot of this section. */
@media (min-width: 901px) {
  .ph-panel-inner {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: 44px;
    padding-block: 40px 64px;
  }
  .ph-panel .ph-panel-cols {
    display: flex; align-items: flex-start;
    gap: 44px;
    min-width: 0;
    animation: none;
  }
  .ph-panel .ph-panel-col.is-lead { margin-right: 44px; }
  .ph-panel .ph-panel-col {
    flex: 0 1 auto; min-width: 0;
    display: flex; flex-direction: column; align-items: flex-start;
    animation: ph-search-rise .5s cubic-bezier(.2, .8, .2, 1) both;
    animation-delay: calc(.04s + var(--i, 0) * .05s);
  }
  .ph-panel .ph-panel-col-t {
    font-size: 15px; line-height: 20px; font-weight: 400;
    color: var(--ph-text-2); margin: 0 0 12px;
    white-space: nowrap;
  }
  .ph-panel .ph-panel-col-t2 { margin-top: 24px; }

  .ph-nav .ph-panel .ph-panel-col a,
  .ph-nav .ph-panel .ph-panel-col .ph-panel-label {
    display: inline-flex; align-items: baseline; padding: 4px 0;
    font-size: 17px; line-height: 22px; font-weight: 600;
    letter-spacing: -0.005em;
    color: var(--ph-text-1);
    white-space: nowrap;
    transition: color var(--ph-dur-fast, .15s) ease;
  }
  .ph-nav .ph-panel .ph-panel-col.is-lead a {
    padding: 5px 0;
    font-size: 24px; line-height: 28px; letter-spacing: -0.015em;
  }
  /* The column's small closing links. */
  .ph-nav .ph-panel .ph-panel-col.is-lead a.ph-panel-sm {
    padding: 4px 0;
    font-size: 17px; line-height: 22px; letter-spacing: -0.005em;
  }
  .ph-nav .ph-panel .ph-panel-col.is-lead a:not(.ph-panel-sm):not(.ph-panel-col-t) + a.ph-panel-sm { margin-top: 12px; }
  .ph-nav .ph-panel .ph-panel-col a:hover { color: var(--ph-accent-link); text-decoration: none; }
  .ph-panel .ph-panel-note {
    align-self: center;
    margin-left: 10px; padding: 0 9px;
    font-size: 15px; line-height: 22px; font-weight: 500; letter-spacing: 0;
    color: var(--ph-accent-link);
    background: color-mix(in srgb, var(--ph-accent-link) 9%, transparent);
    border-radius: 99px;
  }

  /* ---- the one picture card, at the right end of the line */
  .ph-panel-feat { flex: 0 0 auto; display: flex; gap: 20px; }
  /* Store carries three cards; they step down so the row still fits the line. */
  .ph-feat:not(:only-child) { width: clamp(150px, 12.5vw, 200px); }
  .ph-feat {
    display: flex; flex-direction: column; gap: 14px;
    width: clamp(176px, 15vw, 220px);
    color: var(--ph-text-1) !important; text-decoration: none !important;
    animation: ph-search-rise .5s cubic-bezier(.2, .8, .2, 1) both;
    animation-delay: calc(.04s + var(--i, 0) * .05s);
  }
  .ph-feat-img {
    display: block; aspect-ratio: 1; overflow: hidden;
    border-radius: 18px; background: #EEF1F8;
  }
  .ph-feat-img img {
    width: 100%; height: 100%; object-fit: cover; display: block;
    transition: transform .6s cubic-bezier(.2, .8, .2, 1);
  }
  .ph-feat-cap {
    display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
    font-size: 15px; line-height: 20px; font-weight: 600;
  }
  .ph-feat-cap svg {
    flex: none; width: 18px; height: 18px; margin-top: 1px;
    transition: transform .3s cubic-bezier(.2, .8, .2, 1);
  }
  .ph-feat:hover .ph-feat-img img { transform: scale(1.04); }
  .ph-feat:hover .ph-feat-cap svg { transform: translateX(4px); }
  .ph-feat:hover .ph-feat-name { color: var(--ph-accent-link); }
  .ph-feat:focus-visible { outline: none; }
  .ph-feat:focus-visible .ph-feat-img {
    outline: 2px solid var(--ph-accent-link); outline-offset: 3px;
  }

  .ph-links > .ph-item > .ph-trigger[aria-expanded="true"] { color: var(--ph-text-1); }
}
/* Every picture card carries a category flag at its top, the same navy pill
   Shop all puts on its cards (store/shop.css .pcard-tag). */
.ph-feat-img { position: relative; }
/* 2026-09-22 by request: a quiet corner mark, not a banner. Small frosted
   pill tucked into the corner so it never sits over the product itself. */
.ph-feat-tag {
  position: absolute; top: 7px; left: 7px; z-index: 1;
  display: inline-flex; align-items: center;
  height: 20px; padding: 0 8px; border-radius: 50px;
  background: rgba(255, 255, 255, .78); color: var(--ph-accent);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  font-size: 12px; line-height: 1; font-weight: 500; letter-spacing: .01em;
  white-space: nowrap; pointer-events: none;
}
/* A column title that is a link (Shop genetic testing = that category's shop
   all): it keeps the title's grey and size; only its hover says it goes somewhere
   (the chevron was removed 2026-09-22 by request). */
.ph-nav .ph-panel .ph-panel-col a.ph-panel-col-t.is-link {
  display: inline-flex; align-items: center;
  min-height: 0; padding: 0; margin: 0 0 12px;
  font-size: 15px; line-height: 20px; font-weight: 400; letter-spacing: 0;
  color: var(--ph-text-2); border-bottom: 0;
}
.ph-nav .ph-panel .ph-panel-col a.ph-panel-col-t.is-link:hover { color: var(--ph-accent-link); }
@media (max-width: 900px) {
  .ph-nav .ph-panel .ph-panel-col a.ph-panel-col-t.is-link { margin-bottom: 4px; min-height: 44px; }
}

/* Testing has four columns. Its text never shrinks under itself (nowrap links in
   a shrinking column overprint the next one), so below 1280 the card gives way
   and the gaps and lead size tighten until the four fit. */
@media (min-width: 901px) {
  /* Words never shrink under themselves; the cards give way instead. */
  .ph-panel .ph-panel-cols, .ph-panel-cols > .ph-panel-col { flex-shrink: 0; }
  .ph-panel-feat { min-width: 0; flex-shrink: 1; }
  .ph-panel-feat .ph-feat:not(:only-child) { flex: 0 1 auto; min-width: 120px; }
  /* 17px column links (2026-09-22) made the four a line wider; tighter gaps keep the card. */
  .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) { gap: 32px; }
  .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) > .ph-panel-col.is-lead { margin-right: 24px; }
  .ph-panel-inner:has(.ph-panel-col:nth-child(4)) .ph-feat { width: clamp(150px, 12vw, 190px); }
}
@media (min-width: 901px) and (max-width: 1365px) {
  .ph-panel-inner:has(.ph-panel-col:nth-child(4)) .ph-panel-feat { display: none; }
  .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) { gap: 32px; }
  .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) > .ph-panel-col.is-lead { margin-right: 16px; }
  .ph-nav .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) .ph-panel-col.is-lead a:not(.ph-panel-sm):not(.ph-panel-col-t) {
    font-size: 21px; line-height: 26px;
  }
}
@media (min-width: 901px) and (max-width: 1023px) {
  .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) { gap: 22px; }
  .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) > .ph-panel-col.is-lead { margin-right: 0; }
}
/* The bold 17px product columns need the gaps a little tighter up to 1099,
   and under 960 the products drop back to 15 (still bold) to fit the line. */
@media (min-width: 1024px) and (max-width: 1099px) {
  .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) { gap: 26px; }
}
@media (min-width: 901px) and (max-width: 959px) {
  .ph-nav .ph-panel .ph-panel-cols .ph-panel-col.is-shop a:not(.ph-panel-col-t) { font-size: 15px; line-height: 20px; }
}
@media (min-width: 901px) and (max-width: 1099px) {
  .ph-nav .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) .ph-panel-col.is-lead a:not(.ph-panel-sm):not(.ph-panel-col-t) {
    font-size: 18px; line-height: 24px;
  }
}
@media (min-width: 901px) and (max-width: 959px) {
  .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) { gap: 14px; }
  .ph-nav .ph-panel .ph-panel-cols:has(> .ph-panel-col:nth-child(4)) .ph-panel-col.is-lead a:not(.ph-panel-sm):not(.ph-panel-col-t) {
    font-size: 17px; line-height: 22px;
  }
}

/* Narrower desktops: the links come first, so the card gives way. */
@media (min-width: 901px) and (max-width: 1099px) {
  .ph-panel-feat { display: none; }
  .ph-nav .ph-panel .ph-panel-col.is-lead a:not(.ph-panel-sm):not(.ph-panel-col-t) { font-size: 21px; line-height: 26px; }
}
@media (prefers-reduced-motion: reduce) {
  .ph-panel .ph-panel-col, .ph-feat { animation: none; }
  .ph-feat-img img, .ph-feat-cap svg { transition: none; }
}
/* 2026-09-22 by request: the links in the columns after the lead are set at
   regular weight, one step down (15/20), at every width. The lead column's
   doors, the grey column titles and the cards keep their own size and weight. */
.ph-nav .ph-panel .ph-panel-col:not(.is-lead) a:not(.ph-panel-col-t) {
  font-size: 15px; line-height: 20px; font-weight: 400;
}
/* The product columns under a linked "Shop …" title (Testing's genetic and
   microbiome columns) are a step up and bold, so the products outrank the
   plain help links beside them. */
.ph-nav .ph-panel .ph-panel-col.is-shop a:not(.ph-panel-col-t),
.ph-nav .ph-panel .ph-panel-col.is-shop .ph-panel-label {
  font-size: 17px; line-height: 22px; font-weight: 600;
}
/* A product with no page yet (Supplements' Coming soon rows) is a label, not a
   link: same size and weight as its neighbours, in the quieter ink. */
.ph-nav .ph-panel .ph-panel-col.is-shop .ph-panel-label { color: var(--ph-text-2); }
/* A flag beside a product (Testing's Sold out) is a small mark, the same size as
   the card corner tag, so it never competes with the product name. */
.ph-nav .ph-panel .ph-panel-col.is-shop .ph-panel-note {
  align-self: center;
  margin-left: 8px; padding: 0 7px;
  font-size: 12px; line-height: 20px; font-weight: 500;
}
/* A stacked second title that is a link keeps the gap above it; the is-link
   rule's `margin: 0 0 12px` would otherwise zero it. */
.ph-nav .ph-panel .ph-panel-col a.ph-panel-col-t.is-link.ph-panel-col-t2 { margin-top: 24px; }
/* Supplements carries four product columns of long names and no card, a line
   too wide for the no-shrink rule the other panels use. Here the columns may
   shrink and a long name may wrap. */
/* The same holds for any panel of ranges without a picture card
   (.is-ranges.is-cardless, set by shared.js from the menu XOmics keeps). The
   id stays in each list so every rule keeps the weight it had. */
@media (min-width: 901px) {
  :is(#phPanel-supplements, .ph-panel.is-ranges.is-cardless) .ph-panel-cols { flex: 0 1 auto; min-width: 0; }
  :is(#phPanel-supplements, .ph-panel.is-ranges.is-cardless) .ph-panel-cols > .ph-panel-col.is-shop { flex: 0 1 auto; min-width: 0; }
  .ph-nav :is(#phPanel-supplements, .ph-panel.is-ranges.is-cardless) .ph-panel-col.is-shop a:not(.ph-panel-col-t),
  .ph-nav :is(#phPanel-supplements, .ph-panel.is-ranges.is-cardless) .ph-panel-col.is-shop .ph-panel-label {
    white-space: normal; flex-wrap: wrap; column-gap: 10px; row-gap: 4px;
  }
  :is(#phPanel-supplements, .ph-panel.is-ranges.is-cardless) .ph-panel-col.is-shop .ph-panel-note { margin-left: 0; }
}
/* The ranges side by side, no lead column. Each
   range's title is the big heading on the top row, a step above the product
   names, and the door to its range page. Every word in this panel is black
   (titles, products) except the Liposomal tag, which keeps its blue, and the rows, columns and the space
   under each title are a little looser than the other panels (both by
   request). A hover dims the word rather than turning it blue. Below the
   widest screens the gaps close up, so a name keeps its Liposomal tag on its
   line; a title too long for its column then wraps rather than running into
   the next. The !important answers apple/apple.css, which sets every panel
   title and link at 12 to 14px in grey !important on the store and product
   pages; the panel id keeps these ahead of it. */
.ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col a.ph-panel-col-t.is-link {
  font-size: 21px !important; line-height: 26px !important; font-weight: 600 !important;
  letter-spacing: 0 !important; color: #000 !important; margin-bottom: 22px;
}
.ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col.is-shop a,
.ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col.is-shop .ph-panel-label { color: #000 !important; }
.ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col.is-shop a { transition: opacity .2s ease; }
.ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col.is-shop a:hover { color: #000 !important; opacity: .6; }
.ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col.is-shop a:not(.ph-panel-col-t) { padding-top: 7px; padding-bottom: 7px; }
@media (min-width: 1366px) {
  .ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-cols { gap: 56px; }
}
@media (min-width: 901px) and (max-width: 1365px) {
  .ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-cols { gap: 44px; }
}
@media (min-width: 901px) and (max-width: 1100px) {
  .ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-cols { gap: 24px; }
  .ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col-t.is-link { white-space: normal; }
  /* Most titles take two lines here; the one that fits on one keeps the same
     height, so every column's first product sits on the same row. */
  .ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col a.ph-panel-col-t.is-link { font-size: 17px !important; line-height: 22px !important; margin-bottom: 18px; min-height: 44px; align-items: flex-start; }
}
@media (max-width: 900px) {
  .ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col a.ph-panel-col-t.is-link { font-size: 17px !important; line-height: 22px !important; margin-bottom: 4px; }
  .ph-nav :is(#phPanel-supplements, #phPanel-testing, #phPanel-devices, .ph-panel.is-ranges) .ph-panel-col.is-shop a:not(.ph-panel-col-t) { padding-top: 6px; padding-bottom: 6px; }
}
/* Testing and Devices take the same form: no lead
   column, each category title the door to its page. Their plain help column
   (More from testing, Get started) is NOT restyled: it keeps the store panel's
   Quick links sizes (grey 15 title, 15 links), and their cards stay. */
/* Devices has short columns, so its two cards still fit the line down to 1240. */
@media (min-width: 1240px) and (max-width: 1365px) {
  .ph-nav #phPanel-devices .ph-panel-feat { display: flex; }
}
/* ring.css paints every panel link accent blue on .ph-flat pages (0,2,1);
   the .ph-nav prefix above (0,3,1) keeps one ink and one hover everywhere. */

/* ---- the drawer (≤900): the same hierarchy, stacked. The lead column's doors
   stay a size up, and the card sits under the links at half the width. */
@media (max-width: 900px) {
  .ph-panel .ph-panel-col-t { font-size: 15px; font-weight: 400; color: var(--ph-text-2);
                              margin-bottom: 4px; }
  .ph-nav .ph-panel .ph-panel-col.is-lead a:not(.ph-panel-sm):not(.ph-panel-col-t) { font-size: 21px; line-height: 28px; font-weight: 600; }
  .ph-panel .ph-panel-col a { font-weight: 600; }
  .ph-nav .ph-panel .ph-panel-col a:not(.ph-panel-col-t) { font-size: 17px; }
  .ph-panel-feat {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px;
    margin-top: 20px;
  }
  .ph-panel-feat:has(> .ph-feat:nth-child(3)) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ph-feat { display: flex; flex-direction: column; gap: 10px;
             color: var(--ph-text-1) !important; text-decoration: none !important;
             border-bottom: 0 !important; padding: 0 !important; width: auto !important; }
  .ph-feat-img { display: block; aspect-ratio: 1; overflow: hidden;
                 border-radius: 12px; background: #EEF1F8; }
  .ph-feat-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .ph-feat-cap { display: flex; justify-content: space-between; gap: 8px;
                 font-size: 15px; line-height: 20px; font-weight: 600; }
  .ph-feat-cap svg { flex: none; width: 18px; height: 18px; }
}

/* ONE HEADING, ONE LOOK (2026-10-08). A display heading's held-back first line
   (.ph-display-lead, and the older .bd-dim / .xp-coach-h .dim) used to be a
   second, smaller, grey-blue headline competing with the line under it. It is
   now part of the same heading: same colour, size and weight. Supporting words
   go in an eyebrow or a subtitle, never in a second headline. !important
   because page sheets restyle the lead with higher specificity. */
.ph-display .ph-display-lead,
.bd .bd-display .bd-dim,
.xp .xp-coach-h .dim,
.xp .xp-two-h .lead,
.apx .ax-h .ax-lead {
  font-size: inherit !important; font-weight: inherit !important;
  line-height: inherit !important; letter-spacing: inherit !important;
  color: inherit !important; margin: 0 !important; opacity: 1 !important;
}
/* A heading's emphasised words keep the heading's colour: one heading, one
   colour (2026-10-08). */
:is(h1, h2, h3, h4) em { color: inherit !important; }
/* The store shelves' two sentence heading ("Genetic tests. Read your DNA
   once.") reads as one heading in one ink. */
.shop .st-h2, .shop .st-h2 b { color: var(--ph-text-1) !important; }

/* THE PRODUCT CARD (2026-10-08), one for every list of products: a range's
   rail (where supplements/energy-and-cognition/ec.css restyles it under .ec),
   the store's category shelves, and a testing or supplements page's list of
   tests (data-ph-cards, src/site/RangeGrid.jsx). White, a hairline, round
   corners; the photo on its plate, name, one line, two points, the price with
   Add to bag, and Learn more. */
.rg-card {
  display: flex; flex-direction: column; width: 100%;
  border: 1px solid rgba(16, 33, 75, .09); border-radius: 22px; overflow: hidden;
  background: #fff; color: #1d1d1f; text-align: left;
}
.rg-media {
  position: relative; display: block; aspect-ratio: 6 / 5; overflow: hidden;
  background: #e2e5ed;
}
.rg-media img {
  width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; display: block;
  transform-origin: 50% 100%; transition: transform .6s cubic-bezier(.2, .7, .2, 1);
}
.rg-card:hover .rg-media img { transform: scale(1.04); }
.rg-flag {
  position: absolute; top: 16px; left: 16px;
  padding: 5px 12px; border-radius: 99px; background: #fff;
  font-size: 12px; font-weight: 700; letter-spacing: .03em; color: #203a85;
}
.rg-body { display: flex; flex-direction: column; flex: 1; padding: 22px 24px 24px; }
.rg-name { margin: 0; font-size: 21px; font-weight: 600; line-height: 1.25; letter-spacing: -0.01em; color: #1d1d1f; }
.rg-name a { color: inherit; text-decoration: none; }
.rg-name a:hover { color: #203a85; }
.rg-lead { margin: 8px 0 0; font-size: 15px; line-height: 1.5; color: #6e6e73; }
.rg-points { display: grid; gap: 7px; margin: 14px 0 0; padding: 0; list-style: none; }
.rg-points li { position: relative; padding-left: 14px; font-size: 14px; line-height: 1.4; font-weight: 500; color: #474a52; }
.rg-points li::before {
  content: ""; position: absolute; left: 0; top: .55em;
  width: 5px; height: 5px; border-radius: 50%; background: #203a85;
}
.rg-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: auto; padding-top: 22px; }
.rg-price { font-size: 20px; font-weight: 700; letter-spacing: -0.01em; color: #1d1d1f; font-variant-numeric: tabular-nums; }
.rg-add {
  flex: none; padding: 10px 20px; border: 0; border-radius: 99px; cursor: pointer;
  background: #203a85; color: #fff;
  font: inherit; font-size: 14px; font-weight: 600; line-height: 1.3;
  transition: background-color .2s ease, transform .2s ease;
}
.rg-add:hover { background: #0a1e57; }
.rg-add:active { transform: scale(.97); }
.rg-add:focus-visible { outline: 2px solid #203a85; outline-offset: 3px; }
.rg-add.is-unavailable { background: #e8eaf0; color: #6e6e73; cursor: default; }
.rg-more {
  display: inline-flex; align-items: center; gap: 7px; align-self: flex-start;
  margin-top: 14px; font-size: 14px; font-weight: 600; color: var(--ph-accent-link, #2457c5); text-decoration: none;
}
.rg-more i {
  width: 6px; height: 6px; border-top: 1.5px solid currentColor; border-right: 1.5px solid currentColor;
  transform: rotate(45deg); transition: transform .25s ease;
}
.rg-more:hover i { transform: translateX(3px) rotate(45deg); }
/* A fixed list (data-ph-cards): a grid as wide as its count, up to four. */
.rg-cards {
  display: grid; gap: clamp(16px, 1.6vw, 24px); margin: 0; padding: 0; list-style: none;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
}
.rg-cards.is-1 { grid-template-columns: minmax(0, 360px); }
.rg-cards.is-2 { grid-template-columns: repeat(2, minmax(0, 360px)); }
.rg-cards.is-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.rg-cards.is-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.rg-cards .rg-slot { display: flex; }
@media (max-width: 1068px) {
  .rg-cards.is-3, .rg-cards.is-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .rg-cards.is-2, .rg-cards.is-3, .rg-cards.is-4 { grid-template-columns: minmax(0, 1fr); }
  .rg-name { font-size: 19px; }
}
/* A host that is itself the grid (an Explore page's Related) holds hand-set
   service cards too: the product cards join its grid, the services after. */
.rg-cards > .rg-cards { display: contents; }
.rg-cards > .rg-slot.is-static { order: 1; }
.rg-card.is-service .rg-foot { justify-content: flex-start; }
a.rg-add { display: inline-block; text-decoration: none; }
/* The testing pages' lists (genetic, microbiome, the testing hub) sit a step
   below their heading or lede. */
.dna-pcards, .rf-pcards { margin-top: clamp(32px, 3.6vw, 56px); }
/* On a store shelf a card is as wide as the shelf's product cards. */
.shop .st-home.is-range-cards .st-slot > .rg-card { width: clamp(280px, 23vw, 330px); }

/* Headings break evenly (2026-10-08): no word or two left alone on a line.
   shared.js fitHeadings() also keeps every heading to three lines. */
:is(h1, h2, h3), .ph-display > span { text-wrap: balance; }

/* The footer's policy links (2026-10-09): one quiet row over the copyright. */
.ph-footer .foot-policies { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 18px 0 0; font-size: 13px; line-height: 20px; }
.ph-footer .foot-policies a { color: var(--ph-text-2, #4a5578); text-decoration: none; }
.ph-footer .foot-policies a:hover { color: var(--ph-text-1, #10214b); text-decoration: underline; }

/* Tables in the legal pages (the UK policies, 2026-10-09): a header row on a
   light ground, hairlines between rows, and a sideways scroll on a phone
   rather than columns squeezed to a word each. */
.ph-legal-body .ph-legal-table { margin: 24px 0 28px; overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid #10214b1f; border-radius: 14px; }
.ph-legal-body .ph-legal-table table { width: 100%; min-width: 560px; border-collapse: collapse; font-size: 15px; line-height: 1.5; }
.ph-legal-body .ph-legal-table th,
.ph-legal-body .ph-legal-table td { padding: 12px 16px; text-align: left; vertical-align: top; border-top: 1px solid #10214b1f; }
.ph-legal-body .ph-legal-table thead th { border-top: 0; background: #f4f6fb; font-weight: 600; color: #10214b; }
.ph-legal-body .ph-legal-table td:first-child { font-weight: 500; color: #10214b; }
/* How wide a table has to stay follows how many columns it has (policyPage.js
   says how many): two fit a phone as they are, four and five need more room
   than three before a column is a word wide. */
.ph-legal-body .ph-legal-table[data-cols="1"] table,
.ph-legal-body .ph-legal-table[data-cols="2"] table { min-width: 0; }
.ph-legal-body .ph-legal-table[data-cols="4"] table { min-width: 640px; }
.ph-legal-body .ph-legal-table[data-cols="5"] table { min-width: 700px; }
/* A cell left empty (a line of a form to fill in) keeps the height of a line. */
.ph-legal-body .ph-legal-table td:empty::after { content: "\00a0"; }
