/* Peptide Corpus — partner portal page layer
   ==================================================================================================
   WHAT THIS IS. The page layer for the seven partner screens, in the same role that web/site.css
   plays for the landing page and web/doc.css plays for the 800 generated pages: the design system
   in design/ supplies the tokens and the primitives, and this file supplies the arrangements that
   only these screens need.

   NOTHING HERE IS A NEW COLOUR. Every value resolves through a semantic token from
   design/tokens/colors.css, so both themes hold with no [data-theme="dark"] branch in this file.
   No token was added; nothing in design/ was edited.

   ── The pt- prefix, and why every class carries it ────────────────────────────────────────────
   Three of these screens are www pages and therefore already have doc.css: /partners, and the two
   states of the demo frame. Those use the real .wrap / .doc / .card / .btn / .site-head /
   .site-foot from doc.css, exactly as the demo does today — copying rather than reinventing, per
   the brief. The other four are portal-host pages with no doc.css at all, and are self-contained
   the way app/signin.html is.

   So this file must be ADDITIVE on one host and SELF-SUFFICIENT on the other, which it can only be
   if it never redefines a name doc.css owns. Hence pt-. In particular .pt-btn is a second button
   class and that is deliberate, not an oversight: it is the button for the portal host, drawn from
   the same tokens as doc.css's .btn so the two are indistinguishable on screen. If the portal ends
   up sharing doc.css after all, delete .pt-btn and use .btn — do not leave both live on one page.

   ── Not defined here, on purpose ──────────────────────────────────────────────────────────────
   Every .pc-* class. The demo's rank cards, its rail, its sortbar and its ledger belong to
   web/doc.css and are in use. The demo frame links doc.css and inherits them untouched; this file
   frames the demo and does not reach inside it.
   ================================================================================================== */

/* ── The two lockups ──────────────────────────────────────────────────────────────────────────
   BOTH ARE THE SAME MARK PLUS HTML TEXT, and that is deliberate rather than lazy.

   `brand/peptide-corpus-logo-*.svg` is the canonical lockup and the right file for a PNG export,
   an email or a slide. In a page it is the wrong tool: it is a fixed two-colour artwork, so it
   needs one file per theme and it cannot inherit --accent. The header already solved this — the
   mark is inline SVG taking `fill: var(--accent)`, the words are real text — so the wordmark
   themes, scales with the type ramp, and stays selectable and searchable.

   PerfectCart gets the identical treatment. It does NOT get a mark of its own: one geometry
   across the family is the point, and the previous logos/perfectcart.png was a raster with a dark
   background baked in, which read as a dark tile dropped onto a light page. */
.pt-lockup { display: inline-flex; align-items: center; gap: var(--space-3); }
.pt-lockup .brand-mark { width: 34px; height: 34px; flex: none; }
.pt-lockup .mark-bg { fill: var(--accent); }
.pt-lockup .mark-tri {
  fill: none; stroke: var(--on-accent); stroke-width: 8;
  stroke-linejoin: round; stroke-linecap: round;
}
.pt-lockup-words { display: grid; gap: 1px; }
.pt-lockup-name {
  font-size: var(--text-xl); font-weight: var(--weight-extrabold);
  letter-spacing: var(--tracking-tight); line-height: 1.05; color: var(--text);
}
.pt-lockup-name b { color: var(--accent-text, var(--accent)); font-weight: var(--weight-extrabold); }
/* The endorsement line. --text-subtle would fail contrast on --bg and colors.css reserves it for
   decoration, so this takes --text-muted and earns its quietness from size and tracking instead. */
.pt-lockup-by {
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--text-muted);
}
.pt-lockup-lg .brand-mark { width: 44px; height: 44px; }
.pt-lockup-lg .pt-lockup-name { font-size: var(--text-2xl); }

/* ── Shared primitives for the portal host ────────────────────────────────────────────────────
   The portal has no doc.css, so it needs its own wrap, button and card. Same tokens, same shapes
   and the same radii as the public side; a partner moving between the two should not be able to
   tell which host drew the page. */

.pt-wrap { width: 100%; max-width: 1160px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 36px); }
.pt-narrow { max-width: 880px; }

/* doc.css's .sr-only and .skip, under pt- names because the portal host does not load doc.css.
   The three www screens use the doc.css originals; only the portal screens use these. */
.pt-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.pt-skip {
  position: absolute; left: var(--space-3); top: -60px; z-index: 100;
  padding: var(--space-3) var(--space-4);
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.pt-skip:focus { top: var(--space-3); }

.pt-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2);
  min-height: var(--control-h-lg); padding: 0 var(--space-5);
  border: 1px solid transparent; border-radius: var(--radius-sm);
  font-family: inherit; font-size: var(--text-base); font-weight: var(--weight-bold);
  cursor: pointer; text-align: center;
  transition: background var(--dur-fast) var(--ease-standard),
              border-color var(--dur-fast) var(--ease-standard),
              color var(--dur-fast) var(--ease-standard);
}
.pt-btn:hover { text-decoration: none; }
.pt-btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.pt-btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.pt-btn-secondary { background: var(--surface); border-color: var(--border-strong); color: var(--text); }
.pt-btn-secondary:hover { border-color: var(--accent); color: var(--accent-text, var(--accent)); }
.pt-btn-sm { min-height: var(--control-h-sm); padding: 0 var(--space-3); font-size: var(--text-sm); }
.pt-btn-block { width: 100%; }
.pt-btn[disabled] { opacity: .6; cursor: not-allowed; }

.pt-link {
  color: var(--accent-text, var(--accent)); font-weight: var(--weight-semibold);
  text-decoration: underline; text-underline-offset: 2px;
}
/* "Forgot your password?" is a button, not a link — it sends the reset mail from this page rather
   than navigating. It has to LOOK like a link, so the button chrome comes off explicitly. */
button.pt-link {
  padding: 0; border: 0; background: none; font: inherit;
  font-size: var(--text-xs); cursor: pointer;
}
button.pt-link[disabled] { opacity: .6; cursor: not-allowed; }

/* A stated absence, which on this product is a value and not an empty cell. Dashed, and never
   mistaken for a filled field — see rule 7 in AGENTS.md.
   IT IS NO LONGER QUIET, AND "quiet" IS WHAT THIS COMMENT USED TO SAY. --text-muted was not a
   contrast bug — it is #5f6b7d, measured in colors.css at 5.05:1 on --bg and 5.40:1 on --surface,
   comfortably past the 4.5:1 AA floor. It was a philosophy bug. The dashed outline is what says
   "absent"; once it has said it, the colour does not need to whisper it a second time. Set fainter
   than the values beside it, a named absence reads as the hedge this product exists to refuse —
   "No dimensions scored" is a FINDING, and it was being drawn as an apology for one. So it sits on
   --text, the same weight as a filled field, and the border alone carries the difference. */
.pt-absent {
  display: inline-flex; align-items: center;
  padding: 2px var(--space-2); border: 1px dashed var(--border-strong); border-radius: var(--radius-pill);
  font-size: var(--text-xs); font-weight: var(--weight-semibold); color: var(--text);
}

/* yes / no in the comparison table.
   NEITHER IS A HUE, and both of the obvious choices were measured and rejected:
     --success on --bg   3.29:1  green "yes". Under the 4.5:1 AA floor in LIGHT mode, and there is
                                 no --success-text the way there is a --warning-text.
     --text-subtle       3.44:1  grey "no". Also under it, and colors.css says outright that
                                 --text-subtle is decoration only and meaningful small print
                                 belongs on --text-muted. A cell in a pricing table is meaningful.
   So the pair is weight and tone instead: "yes" at full strength, "no" one step back. The words
   carry the meaning on their own, the partner column is already tinted by the rule below, and the
   table survives forced colours and anyone who cannot separate hues. */
/* A MARK, WITH THE WORD STILL THERE FOR ANYTHING THAT IS NOT LOOKING. Eleven rows of the words
   "yes" and "no" is eleven rows a reader has to actually read; a tick and a cross are scanned in
   one pass down the column, which is the only reason this table exists. The word stays in the
   cell as .pt-sr-only text, so a screen reader hears "yes" and not the name of a glyph — and a
   mark drawn with a mask has no alt text to lose. */
.pt-yes::after, .pt-no::after {
  content: ""; display: inline-block; width: 16px; height: 16px;
  vertical-align: -.18em; background: currentColor;
  mask: var(--pt-mark) center / contain no-repeat;
  -webkit-mask: var(--pt-mark) center / contain no-repeat;
}
.pt-yes { color: var(--accent-text, var(--accent)); font-weight: var(--weight-bold);
  --pt-mark: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.6 5.2 5.2L20 6.6"/></svg>'); }
.pt-no  { color: var(--text-subtle); font-weight: var(--weight-regular); --pt-mark: var(--ico-x); }

/* ==================================================================================================
   PUBLIC PAGE — /partners
   Additive over doc.css. Only the pieces that page needs and no other page has.
   ================================================================================================== */

/* The two hero buttons. A row on desktop, a stack on a phone, and the stack is full width so the
   invite-code door is not a 40%-wide target at 360px. */
.pt-cta-row { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: var(--space-6) 0 var(--space-4); }
@media (max-width: 420px) {
  .pt-cta-row { display: grid; }
  .pt-cta-row > .btn, .pt-cta-row > .pt-btn { width: 100%; }
}

/* The price, stated once as a band rather than buried in a paragraph. Three figures, three cells,
   so a vendor scanning for "what does this cost" finds it in one pass. */
.pt-price {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--space-3); margin: var(--space-8) 0;
  padding: var(--space-5); background: var(--surface);
  border: 1px solid var(--border); border-left: 3px solid var(--accent);
  border-radius: var(--radius-md);
}
.pt-price div { min-width: 0; }
.pt-price b {
  display: block; font-family: var(--font-mono); font-size: var(--text-2xl);
  font-weight: var(--weight-extrabold); letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
}
.pt-price span { display: block; margin-top: var(--space-1); font-size: var(--text-sm); color: var(--text-muted); }

/* THE SECOND OFFER, SET APART FROM THE FIRST RATHER THAN RUNNING ON FROM IT. Two price bands in
   one section read as a single price carrying seven figures unless something separates them, and
   the two are bought separately — PerfectCart sends a basket, the white label puts the partner's
   name on the app. The rule and the inset do that work.

   The nested .pt-price is deliberately the same band, and .pt-price-5 lowers ONE number rather
   than restating the component. It carries five cells — a setup fee, a monthly minimum and three
   seat rates — and the 150px floor fits only four of them at this width: 722px of card content
   less 40px of padding is 682px, and five 150px tracks with four 12px gaps want 798px. So the
   fifth wrapped onto a row of its own, which reads as an afterthought bolted to a settled price
   rather than as the third member of a set. Measured, not guessed.

   118px is the floor because it is the largest round number that fits five and cannot fit six:
   floor((682 + 12) / (118 + 12)) = 5, and six would want 768px. The cells then share the row at
   126.8px each, which is why the labels below are bare rates — "per Pro seat beyond the minimum"
   does not fit in 126px and said the same four words three times over. The mechanic moved to
   .pt-wl-note, where it is stated once. */
.pt-wl {
  margin-top: var(--space-8); padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}
/* TWO OFFERS LIVE IN HERE, NOT ONE, and each needs the rule-and-inset the region as a whole gets.
   `.pt-wl` carries the affiliate app and then the white label — one is added to PerfectCart and
   replaces its two figures, the other is bought on its own — so three price bands now stack in one
   section. Two of them abutting reads as a single price carrying nine figures, which is the exact
   thing the border above was added to stop. The separator is on the SECOND offer and every one
   after it (`+`), never on the first, which already sits under `.pt-wl`'s own rule.

   `> h3` became `.pt-offer > h3` when the offers gained their wrapper. Left as it was it would have
   matched nothing, so the second heading would have kept its top margin and the first would have
   lost the zero — a dead rule and a misaligned heading from one unchanged line. */
.pt-wl .pt-offer + .pt-offer {
  margin-top: var(--space-8); padding-top: var(--space-6);
  border-top: 1px solid var(--border);
}
.pt-wl .pt-offer > h3 { margin-top: 0; }
.pt-wl .pt-price { margin-block: var(--space-5); }
/* FIVE CELLS BREAK 2-THEN-3, NOT 4-THEN-1, AND THAT IS THE WHOLE REASON THIS RULE EXISTS.
   auto-fit steps 5, 4, 3, 2, 1 as the band narrows, so at a tablet width it put four cells on one
   row and left "$8 per Ultra seat" alone underneath — measured at 768px, where the band is 580px
   inside and the 150px floor fits four. A lone trailing cell reads as an afterthought bolted onto a
   settled price rather than as the third member of a set.

   The five are 2 + 3: two fixed costs, then three seat rates. Spanning a 6-column grid 3/3/2/2/2 is
   what puts that grouping on screen at the width where a single row stops fitting. Above 820px all
   five share one row and the spans are handed back.

   min-width steps rather than max-width, matching .pt-stack and .pt-pipe below. */
/* FOUR CELLS BREAK 3-THEN-1 ON THEIR OWN, WHICH IS THE SAME DEFECT .pt-price-5 BELOW WAS WRITTEN
   FOR, ONE CELL SMALLER. Measured on the compiled demo at a 736px viewport: the band's content box
   is 632px, and four 150px tracks with three 12px gaps want 636. Four pixels short, so auto-fit
   steps to three and drops "$2 back to you" onto a row of its own — a lone trailing cell reading as
   an afterthought bolted to a settled price, and this is the cell a partner is least likely to
   expect, because it is the one figure on the screen that we pay them.

   The four are 2 + 2: two costs, then two rates. 2x2 is what that grouping looks like at any width
   where one row does not fit, and it never leaves one cell alone. Above 900px all four share a row.

   min-width steps rather than max-width, matching .pt-price-5 and .pt-stack below. */
.pt-price-4 { grid-template-columns: 1fr; }
@media (min-width: 560px) {
  .pt-price-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 900px) {
  .pt-price-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.pt-price-5 { grid-template-columns: 1fr; }
@media (min-width: 560px) {
  .pt-price-5 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .pt-price-5 > :nth-child(-n + 2) { grid-column: span 3; }
  .pt-price-5 > :nth-child(n + 3)  { grid-column: span 2; }
}
/* THE RESET REPEATS THE :nth-child SELECTORS INSTEAD OF SAYING `> *`, AND THAT IS SPECIFICITY
   RATHER THAN STYLE. `.pt-price-5 > *` is (0,1,0) and the two rules above it are (0,2,0), so the
   universal one loses no matter how late it comes — the cells kept `span 3`/`span 2` against a
   five-track grid, overflowed into implicit rows, and laid out 1 + 2 + 2 with cells 402px wide in a
   722px band. The computed `grid-template-columns` read `126px x5` the whole time, which is why
   this has to be caught by measuring where the cells actually ARE and not by reading the track
   list off the container. */
@media (min-width: 820px) {
  .pt-price-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .pt-price-5 > :nth-child(-n + 2),
  .pt-price-5 > :nth-child(n + 3) { grid-column: auto; }
}
.pt-wl-note { font-size: var(--text-sm); color: var(--text-muted); }
/* `.pt-shot` IS A BREAKOUT AND THIS IS THE ONE PLACE IT MUST NOT BREAK OUT. On /partners it is a
   direct child of the prose column and is meant to widen past it to min(1160px, viewport): that is
   the whole component. Here it sits inside .pt-demoend, which is a padded bordered card 788px wide
   on a desktop viewport — so the unscoped rule renders the figure 1075px wide and pushes it 144px
   through the card's own border on BOTH sides. Measured, not guessed. The image fills the card's
   content width instead, which is what a figure inside a card should do anyway. */
.pt-wl .pt-shot { width: 100%; margin-inline: 0; }

/* Listed vs Partner. Two narrow value columns and one wide label column, fixed rather than
   auto, so "yes"/"no" line up as columns instead of drifting with the label above them. */
.pt-compare { table-layout: fixed; min-width: 460px; }
.pt-compare th:first-child, .pt-compare td:first-child { width: auto; }
.pt-compare th:not(:first-child), .pt-compare td:not(:first-child) { width: 92px; text-align: center; }
.pt-compare tbody th { font-weight: var(--weight-medium); }
/* The partner column is the one being sold; a tint marks it without adding a colour. */
.pt-compare th:last-child, .pt-compare td:last-child { background: var(--selected-tint); }
/* Row labels WRAP. doc.css sets `white-space: nowrap` on every `.doc th`, which is right for the
   two column heads and wrong for the eleven row heads under them: "Buyer age-confirmed and agreed
   to your terms" cannot fit 276px on one line, so it ran under the tick beside it. */
.pt-compare tbody th { white-space: normal; }

/* BOTH COLUMNS, OR THE TABLE ARGUES ONE SIDE. The 460px floor put the Partner column — the one
   being sold — to start at x=387 on a 375px screen, off the edge, reachable only by a sideways
   drag nothing signalled. A reader saw a list of features and one column of ticks, which reads as
   "listed already does this". Narrow value columns cost a few pixels of tick; a column the reader
   never finds costs the comparison. */
@media (max-width: 560px) {
  .pt-compare { min-width: 0; }
  /* 74px is measured, not chosen: "Partner" sets its own column, and doc.css keeps a column head on
     one line, so the floor is that word (62px) plus the padding either side. Declaring anything
     narrower under `table-layout: fixed` does not narrow the table — the head overflows and the
     scroller gets it back as a sideways drag, which is the bug this rule is here to remove. */
  .pt-compare th:not(:first-child), .pt-compare td:not(:first-child) { width: 74px; padding-inline: 5px; }
  .pt-compare tbody th { padding-inline: 8px; }
}

/* The four ways to connect. Each is a card with a number, a claim, and — the point of the
   section — what it PROVES, which is a different field from what it costs you. */
.pt-shapes { display: grid; gap: var(--space-3); margin: var(--space-5) 0; }
/* min-width: 0 because these are grid items. Without it a grid item is at least its min-content
   wide, and the nowrap tag on card three pushed the whole COLUMN — and therefore all four cards —
   14px past the viewport at 360. */
.pt-shape {
  min-width: 0;
  padding: var(--space-5); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-md);
}
/* Wraps, so the "where most partners will sit" tag drops under the heading on a phone instead of
   sitting beside it and widening the card. */
.pt-shape > h3 {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-2); font-size: var(--text-lg); font-weight: var(--weight-bold);
}
.pt-shape > h3 > i {
  flex: none; font-style: normal; font-family: var(--font-mono);
  font-size: var(--text-sm); font-weight: var(--weight-bold); color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
/* The glyph sits on a flex row set to align-items: baseline, and a box with no text in it has no
   useful baseline — it would hang below the numeral. Centred against the heading instead. */
.pt-shape > h3 > .pt-glyph { align-self: center; }
.pt-shape p { margin: 0 0 var(--space-3); max-width: 62ch; }
/* Label left, value right, on a grid rather than a run of sentences — "You do" and "Proves" are
   the two comparable facts across all four shapes and they have to line up to be compared. */
.pt-shape dl { display: grid; grid-template-columns: 84px minmax(0, 1fr); gap: var(--space-2) var(--space-4); margin: 0; }
.pt-shape dt {
  font-size: var(--text-xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-muted);
  padding-top: 2px;
}
.pt-shape dd { margin: 0; font-size: var(--text-base); line-height: var(--leading-relaxed); }
@media (max-width: 520px) {
  .pt-shape dl { grid-template-columns: minmax(0, 1fr); gap: var(--space-1) 0; }
  .pt-shape dd { margin-bottom: var(--space-3); }
  .pt-shape dd:last-child { margin-bottom: 0; }
}
/* Where most partners will sit. One card marked, for the same reason only the winner's total goes
   green on a rank card: a mark every card carries is decoration. */
.pt-shape.is-common { border-color: var(--accent); background: color-mix(in srgb, var(--accent-soft) 55%, var(--surface)); }
.pt-shape-tag {
  display: inline-flex; align-items: center; margin-left: auto;
  padding: 3px var(--space-2); border-radius: var(--radius-pill);
  background: var(--accent); color: var(--on-accent);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); white-space: nowrap;
}

/* ── What sits behind the basket ──────────────────────────────────────────────────────────────
   A card grid, because the argument here is BREADTH — six things a competitor's directory does not
   have — and breadth reads as a grid and does not read as a paragraph. Three across on a wide
   screen, two on a tablet, one on a phone.

   THE STATUS PILL IS THE LOAD-BEARING PART. docs/pro-launch-plan.md exists because a card once
   sold eight features of which one existed. Everything on this grid is live; everything not live
   is in the dimmed list below it, labelled, in the same treatment site.css uses for .plan-soon.
   If you add a card here, it ships today or it goes in the list. */
/* BREAKS OUT OF THE PROSE COLUMN, the same way and by the same arithmetic as .pc-shell in doc.css.
   `.wrap.doc` is 860px because that is a comfortable measure for sentences; eight cards inside it
   give three columns and a lone orphan on row three. At the full 1160 they land four and four. */
/* ONE, TWO OR FOUR COLUMNS, NEVER THREE, and that is the whole reason this is not auto-fit.
   There are eight cards. auto-fit picked three at around 1024px and left a two-card orphan row
   with a hole in it. Every count here divides eight, so the grid is always complete: 4+4, 2+2+2+2,
   or a single column. If a ninth card is ever added, this rule has to be revisited — that is the
   cost of pinning the counts, and it is worth it. */
.pt-stack {
  display: grid; grid-template-columns: minmax(0, 1fr);
  gap: var(--space-3); margin: var(--space-6) 0;
  width: min(1160px, calc(100vw - 2 * clamp(18px, 4vw, 36px)));
  margin-inline: calc((100% - min(1160px, 100vw - 2 * clamp(18px, 4vw, 36px))) / 2);
}
@media (min-width: 620px)  { .pt-stack { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1120px) { .pt-stack { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* Flex column, not grid, so the status pill can be pushed to the bottom with margin-top: auto.
   Grid items stretch to the tallest in the row, so the pills then sit on one baseline across the
   row instead of floating at whatever height each card's prose happens to end. */
/* SAME BREAKOUT ARITHMETIC AS .pt-stack, copied deliberately rather than shared. The figure sits
   directly under the eight cards and any difference between the two widths reads as a mistake at
   the one edge a reader's eye is already following down the page. If the grid's width changes,
   this changes with it.

   The frame is what keeps a 16:9 recording from looking like a hole in the page: same surface,
   same border and same radius as a card, so it belongs to the row above it rather than floating. */
.pt-shot {
  margin: var(--space-6) 0;
  width: min(1160px, calc(100vw - 2 * clamp(18px, 4vw, 36px)));
  margin-inline: calc((100% - min(1160px, 100vw - 2 * clamp(18px, 4vw, 36px))) / 2);
}
/* NO BORDER AND NO PANEL, because the recording is a cut-out rather than a rectangle. A frame
   drawn round it outlines the 16:9 box the laptop does not fill, and the machine then reads as
   floating inside an empty rectangle instead of sitting on the page. The transparency is what
   makes the figure belong to the page; a border takes that straight back. */
.pt-shot img {
  display: block; width: 100%; height: auto;
}
/* Reads as a sentence about the picture, not as a second caption bar: the measure is held to the
   prose column even though the image above it is not, because a caption running the full 1160 is a
   line nobody finishes. */
.pt-shot figcaption {
  max-width: 62ch; margin: var(--space-3) auto 0;
  font-size: 14px; line-height: 1.5; color: var(--text-muted); text-align: center;
}

/* ── The white label, teased under the handoff recording ───────────────────────────────────────
   A POINTER, NOT A PANEL. The offer itself is #white-label further down the page, with the
   mechanics and the "you read none of it" promise; this only says the product exists, at the one
   spot on the page where a reader has just watched PerfectCart work and a second offer can be
   raised without interrupting the first. If it ever grows into the argument, the argument is then
   in two places on one page and one of them goes stale — which is the whole reason the gated
   screens' copy became a module.

   QUIETER THAN .pt-price ON PURPOSE, and the accent rule is the only thing they share. This sits
   directly beneath the recording that carries the primary pitch; a box with the weight of the
   price band would sell the second product over the first, three sections before the first one
   has made its case.

   A HANGING INDENT, NOT A GRID. .pt-soon li carries the long version of this note: the rows are
   `<b>Lead.</b> rest…`, an element plus a bare text node, and a grid container makes EVERY child a
   grid item — the anonymous text run included — so the sentence after the lead drops into the
   marker column and renders one word per line. text-indent keeps the row in normal inline flow and
   does not care how many elements the markup uses. `.pt-need` on this same page is the grid form
   and has that trap live; every list authored into it needs its single wrapping span. This one
   does not, and that is the reason to reach for this shape rather than that one. */
.pt-tease {
  margin: var(--space-8) 0; padding: var(--space-5) var(--space-6);
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--accent); border-radius: var(--radius-md);
}
/* ── The mockup, in the width the measure was already refusing ──────────────────────────────────
   The copy is capped at 62ch and this card runs the full prose column, so about a third of the box
   was empty at desktop. The picture goes there rather than below, which costs no height at all on
   the widths that have the room and stacks on the widths that do not.

   THE COPY IS WRAPPED IN ONE ELEMENT BECAUSE THIS IS A GRID. Every child of a grid container is an
   item; five loose children would take five columns. That is the same rule the note above gives for
   `.pt-tease-list li` from the other direction — there it is the anonymous text run inside a row,
   here it is the rows themselves. `.pt-tease-body`, and the h3 rule below is on it rather than on
   `.pt-tease`, because a direct-child selector stops matching the moment a wrapper appears and the
   only symptom is a heading quietly getting its margin back.

   THE PICTURE COLUMN IS A SHARE, NOT A WIDTH, AND THAT WAS MEASURED RATHER THAN GUESSED. Pinned at
   320px it held its size while the text collapsed around it: at a 721px viewport the copy had 267px
   and the mockup 320, so the picture was WIDER than the argument it illustrates, at the one width
   where the argument is hardest to read — four lines for the first beat. A share keeps the desktop
   proportion (390/326 in a 788px card, which is where it was drawn) and takes the squeeze with the
   text instead of passing it on.

   240px IS A LEGIBILITY FLOOR, NOT A TASTE ONE. The whole point of the picture is that the reader
   makes out the placeholder wordmark in the sidebar; narrower than that it stops reading as
   lettering, and a purple shape on an app screenshot is then reasonably taken for OUR mark on OUR
   app — the one thing this section exists to say it is not. The share does not reach it above this
   breakpoint, so the floor is a guard on the prose column narrowing later rather than something
   live today. Below 720px it stacks rather than shrinking past it. */
@media (min-width: 720px) {
  .pt-tease {
    display: grid; align-items: center; gap: var(--space-6);
    grid-template-columns: minmax(0, 1fr) minmax(240px, 44%);
  }
}
.pt-tease-body { min-width: 0; }
.pt-tease-figure { margin: var(--space-5) 0 0; }
@media (min-width: 720px) { .pt-tease-figure { margin: 0; } }
/* No frame and no panel, for the reason .pt-shot gives: the machine is a cut-out on transparency
   rather than a rectangle, so a border drawn round it outlines the 16:9 box the laptop does not
   fill. */
.pt-tease-figure img { display: block; width: 100%; height: auto; }
.pt-tease-figure figcaption {
  margin-top: var(--space-2); font-size: var(--text-xs); line-height: 1.45;
  color: var(--text-subtle); text-align: center;
}
.pt-tease-kicker {
  margin: 0 0 var(--space-2); font-size: var(--text-xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-muted);
}
.pt-tease-body > h3 { margin: 0; }
.pt-tease-sub {
  margin: var(--space-1) 0 var(--space-4); max-width: 62ch;
  font-size: var(--text-sm); color: var(--text-muted);
}
.pt-tease-list { display: block; margin: 0 0 var(--space-4); padding: 0; list-style: none; }
.pt-tease-list li {
  margin-bottom: var(--space-2); padding-left: 26px; text-indent: -26px; max-width: 62ch;
  line-height: var(--leading-relaxed); color: var(--text-muted);
}
.pt-tease-list li:last-child { margin-bottom: 0; }
.pt-tease-list li b { font-weight: var(--weight-semibold); color: var(--text); }
/* The same tick .pt-yes draws, at the same weight, because two check marks of different builds on
   one page reads as two designers. It is a mask, so it takes the accent by paint and has no alt
   text to lose — the lead sentence beside it carries the meaning on its own. */
.pt-tease-list li::before {
  content: ""; display: inline-block; width: 26px; height: 14px;
  text-indent: 0; vertical-align: -.12em; background: var(--accent);
  --pt-tick: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.6 5.2 5.2L20 6.6"/></svg>');
  mask: var(--pt-tick) left center / 14px 14px no-repeat;
  -webkit-mask: var(--pt-tick) left center / 14px 14px no-repeat;
}
.pt-stackcard {
  min-width: 0; padding: var(--space-5);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
  display: flex; flex-direction: column; gap: var(--space-3);
}
/* The glyph. A tinted square rather than a bare icon: at 16px on a white card an outline icon
   reads as a smudge, and the square gives it a ground without adding a colour. */
.pt-stackcard-ico {
  width: 34px; height: 34px; flex: none; display: grid; place-items: center;
  border-radius: var(--radius-sm);
  background: var(--accent-soft); border: 1px solid var(--accent-soft-border); color: var(--accent-text, var(--accent));
}
.pt-stackcard-ico::before {
  content: ""; width: 17px; height: 17px; background: currentColor;
  mask: var(--pt-ico) center / contain no-repeat;
  -webkit-mask: var(--pt-ico) center / contain no-repeat;
}
/* An inline <svg> instead, for the two glyphs the design system's set has no entry for. Same
   1.6 stroke weight as the demo's .pc-ico, so the two families sit together. */
.pt-stackcard-ico > svg { width: 19px; height: 19px; }
.pt-stackcard-ico:has(> svg)::before { content: none; }
.pt-ico-book  { --pt-ico: var(--ico-book); }
.pt-ico-rung  { --pt-ico: var(--ico-rung); }
.pt-ico-info  { --pt-ico: var(--ico-info); }
.pt-ico-scale { --pt-ico: var(--ico-scale); }
.pt-ico-flask { --pt-ico: var(--ico-flask); }
.pt-ico-vials { --pt-ico: var(--ico-vials); }

.pt-stackcard h3 {
  margin: 0; font-size: var(--text-md); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
}
.pt-stackcard p {
  margin: 0; font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-relaxed);
}
.pt-stackcard .pt-live {
  margin-top: auto; align-self: start;
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 2px var(--space-2); border-radius: var(--radius-pill);
  background: var(--accent-soft); color: var(--text-on-soft); border: 1px solid var(--accent-soft-border);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}

/* ── Partner types — CURRENTLY UNUSED, HELD ON PURPOSE ───────────────────────────────────────
   No markup uses .is-open, .is-later or .pt-later today: the "Who can partner" section was
   removed from partners.html on 2026-08-21 and is deferred, not abandoned. These rules are kept
   so restoring it is a paste. Delete them only if that decision is reversed for good — and if you
   are here because a dead-CSS check flagged them, this comment is the answer.

   WHAT IT HAS TO HAVE BEFORE IT COMES BACK: a one-line offer for wellness clinics, creators and
   wholesalers. docs/partner-pipeline-plan.md decision 3 is the only place the five types are
   documented, it says nothing about what four of them get, and docs/partners-page-copy.md is
   retail-only. The section shipped with a marked placeholder for exactly that reason.

   The original rationale, kept because it is the reason for the treatment below:
   decision 3 says "only retail is built. The other four are named on the public page as opening
   later, NEVER SHIPPED AS EMPTY STUBS."

   So the four unopened ones are named, dated as unopened, and NOT clickable. A card that looks
   interactive and does nothing is the empty stub that instruction forbids; a card that plainly
   says "opening later" is the roadmap it asks for. The dimming is structural — no border, sunken
   ground — and never done by making the words hard to read. */
.pt-later {
  justify-self: start; margin-top: auto;
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 2px var(--space-2); border-radius: var(--radius-pill);
  background: transparent; color: var(--text-muted); border: 1px dashed var(--border-strong);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}
/* The one that is open spans the row, and the four that are not sit beneath it. Two reasons, and
   the second is the real one: five cards in a four-column grid leave a lone orphan, and — more
   importantly — "one of these is open and four are not" is a fact about the SHAPE of the list, so
   the layout should carry it rather than leaving a status pill to do all the work. */
.pt-stackcard.is-open { grid-column: 1 / -1; }
@media (min-width: 620px) {
  .pt-stackcard.is-open { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: var(--space-2) var(--space-4); align-items: start; }
  .pt-stackcard.is-open h3 { grid-column: 2; }
  .pt-stackcard.is-open p { grid-column: 2; max-width: 70ch; }
  .pt-stackcard.is-open .pt-live { grid-column: 2; margin-top: var(--space-1); }
}
.pt-stackcard.is-later {
  background: var(--surface-subtle); border-style: dashed; border-color: var(--border);
}
.pt-stackcard.is-later .pt-stackcard-ico {
  background: var(--surface-sunken); border-color: var(--border); color: var(--text-muted);
}

/* The roadmap. Dimmed and labelled rather than hidden — worth showing, and it must not read as
   available. Same reasoning and the same treatment as .plan-soon / .plan-list-soon in site.css. */
.pt-soon {
  margin: var(--space-6) 0; padding: var(--space-5);
  border: 1px dashed var(--border-strong); border-radius: var(--radius-md);
}
.pt-soon > h3 {
  margin: 0 0 var(--space-2); font-size: var(--text-xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-muted);
}
.pt-soon > p { margin: 0 0 var(--space-4); max-width: 62ch; font-size: var(--text-sm); color: var(--text-muted); }
.pt-soon ul { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
/* A HANGING INDENT, NOT A GRID, and the difference is not cosmetic. These rows are `<b>Journal.</b>
   Not built.` — a element plus a bare text node. A grid container turns EVERY child into a grid
   item, anonymous text runs included, so the row became three items in a two-column grid: the
   dash, the bold name, and then "Not built." wrapped onto the next row in a 9px column, one word
   per line. It rendered as a paragraph falling down a well.
   text-indent keeps the whole row in normal inline flow, so it does not care how many elements
   the markup happens to use. */
.pt-soon li {
  padding-left: 22px; text-indent: -22px;
  font-size: var(--text-sm); line-height: var(--leading-relaxed); color: var(--text-muted);
}
/* --text-subtle measures 3.44:1 on --bg and colors.css says outright it is decoration only, so
   only the dash takes it. "Not built." is meaningful small print and stays on --text-muted. The
   dimming is done by the dashed border and the label — a roadmap that must not look available
   still has to be legible. */
.pt-soon li b { font-weight: var(--weight-semibold); color: var(--text); }
.pt-soon li::before {
  content: "—"; display: inline-block; width: 22px; text-indent: 0; color: var(--text-subtle);
}

/* How a month runs. Numbered, because the order is the argument. */
.pt-flow { counter-reset: ptflow; display: grid; gap: var(--space-3); margin: var(--space-5) 0; padding: 0; list-style: none; }
.pt-flow li {
  counter-increment: ptflow;
  display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: var(--space-4);
  align-items: start;
}
.pt-flow li::before {
  content: counter(ptflow);
  display: grid; place-items: center; width: 28px; height: 28px;
  border-radius: var(--radius-pill); background: var(--accent-soft); color: var(--text-on-soft);
  border: 1px solid var(--accent-soft-border);
  font-family: var(--font-mono); font-size: var(--text-xs); font-weight: var(--weight-bold);
}
.pt-flow p { margin: 3px 0 0; }

/* What we need from you. A list of fields, not a sales list, so it reads as a form preview. */
.pt-need { display: grid; gap: var(--space-2); margin: var(--space-5) 0; padding: 0; list-style: none; }
.pt-need li {
  display: grid; grid-template-columns: 14px minmax(0, 1fr); gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-subtle); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm);
  font-size: var(--text-base); line-height: var(--leading-relaxed);
}
.pt-need li::before {
  content: ""; width: 14px; height: 14px; margin-top: 4px; background: var(--text-subtle);
  mask: var(--ico-doc) center / contain no-repeat;
  -webkit-mask: var(--ico-doc) center / contain no-repeat;
}

/* ── The workflow set ────────────────────────────────────────────────────────────────────────
   WHY THIS IS IN partners.css AND NOT IN EITHER AUTHORED PAGE. Two documents need these glyphs:
   partners.html (/partners on www) and demo-active.html (/preview on the partner host). This
   stylesheet is authored once and copied to BOTH hosts — build-partners-page.mjs writes web/
   partners.css, build-partners.mjs writes the partner host's — so a mask token defined here has
   exactly one definition. The same markup inlined into two HTML files would be two copies, which
   is how the nav on these very pages fell a menu entry behind.

   Drawn to the --ico-* family in design/components.css, not to the 1.5px set in lib/icons.mjs:
   24 grid, 2.4 stroke, round caps, `black` as the paint because a mask only reads alpha. That
   family is the one already on this page (.pt-stackcard-ico, .pt-need), and two icon weights in
   one section reads as two designers.

   These are workflow glyphs — cart, coupon, key, storefront, invoice — and the corpus set has no
   entry for any of them, which is the whole reason they are drawn here rather than imported. If
   the design system ever grows them, delete these and point --pg-* at the --ico-* names. */
.pt-glyph {
  width: 32px; height: 32px; flex: none; display: grid; place-items: center;
  border-radius: var(--radius-sm);
  background: var(--accent-soft); border: 1px solid var(--accent-soft-border); color: var(--accent-text, var(--accent));
}
.pt-glyph::before {
  content: ""; width: 17px; height: 17px; background: currentColor;
  mask: var(--pg) center / contain no-repeat;
  -webkit-mask: var(--pg) center / contain no-repeat;
}
/* The neutral reading, for a step that is THEIRS rather than ours. Same glyph, no accent — the
   colour is what carries "who does this", so it must not also be decoration. */
.pt-glyph.is-theirs {
  background: var(--surface-subtle); border-color: var(--border); color: var(--text-muted);
}
.pt-glyph-sm { width: 24px; height: 24px; }
.pt-glyph-sm::before { width: 13px; height: 13px; }

.pt-g-cart    { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="9.5" cy="20" r="1.4"/><circle cx="18" cy="20" r="1.4"/><path d="M2.5 3.5h2.8l2.5 11.6h11l2.2-8.1H6"/></svg>'); }
.pt-g-link    { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M10.2 13.4a4.6 4.6 0 0 0 6.7.4l2.5-2.5a4.6 4.6 0 0 0-6.5-6.5l-1.4 1.4"/><path d="M13.8 10.6a4.6 4.6 0 0 0-6.7-.4l-2.5 2.5a4.6 4.6 0 0 0 6.5 6.5l1.4-1.4"/></svg>'); }
.pt-g-tag     { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M11.6 2.6H21.4v9.8l-9.6 9.6a1.8 1.8 0 0 1-2.5 0l-7.3-7.3a1.8 1.8 0 0 1 0-2.5Z"/><circle cx="16.9" cy="7.1" r="1.3"/></svg>'); }
.pt-g-key     { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.8" cy="15.2" r="4.4"/><path d="m11 12 9-9M17.2 5.8l2.6 2.6M14.6 8.4l2.6 2.6"/></svg>'); }
.pt-g-store   { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M3.8 10.2V20.5h16.4V10.2M2.6 9.6 5 3.5h14l2.4 6.1ZM9.6 20.5v-6h4.8v6"/></svg>'); }
.pt-g-receipt { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5.2 2.8h13.6v18.4l-2.7-1.8-2.3 1.8-2.3-1.8-2.3 1.8-2.3-1.8-1.7 1.1Z"/><path d="M9 8.4h6M9 12.6h6"/></svg>'); }
.pt-g-card    { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><rect x="2.4" y="5" width="19.2" height="14" rx="2.4"/><path d="M2.4 10h19.2M6.2 15h3.4"/></svg>'); }
.pt-g-return  { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20.6 12a8.6 8.6 0 1 1-2.5-6.1"/><path d="M20.6 3.6v5.2h-5.2"/></svg>'); }
.pt-g-rank    { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M2.6 20.4h18.8M6.4 20.4v-6.2M12 20.4V4.6M17.6 20.4v-9.4"/></svg>'); }
.pt-g-feed    { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 3.6A16.4 16.4 0 0 1 20.4 20"/><path d="M4 10.4A9.6 9.6 0 0 1 13.6 20"/><circle cx="5" cy="19" r="1.5"/></svg>'); }
.pt-g-bolt    { --pg: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M13.4 2.4 4.6 13.8h6.2l-1.2 7.8 8.8-11.4h-6.2Z"/></svg>'); }

/* ── The macro visual: one pipeline, and WHO OWNS EACH STEP ──────────────────────────────────
   The pipeline was six sentences in an <ol>, and six sentences do not answer the question a
   partner actually has, which is not "what happens" but "where does my money go". Colour carries
   that and nothing else: an accent step is ours, a plain step is theirs. Read down the row and the
   claim in the prose — that we are never the merchant of record — is visible before it is read.

   STILL AN <ol>. The steps are ordered, they are read in order, and a list that is drawn as a rail
   is still a list. With the stylesheet off it reads as the six sentences it always was. */
.pt-pipe {
  display: grid; gap: var(--space-3); margin: var(--space-5) 0; padding: 0; list-style: none;
  grid-template-columns: minmax(0, 1fr);
  counter-reset: ptpipe;
  /* BREAKS OUT OF THE MEASURE, the same way .pt-stack does and for the same reason. This section
     sits in an 880px column because that is a comfortable measure for SENTENCES. Six cards in it
     came out 121px wide with eight-line bodies — a column of confetti rather than a pipeline. Prose
     wants a narrow measure; a row of cards wants the page. */
  width: min(1160px, calc(100vw - 2 * clamp(18px, 4vw, 36px)));
  margin-inline: calc((100% - min(1160px, 100vw - 2 * clamp(18px, 4vw, 36px))) / 2);
}
/* TWO ROWS OF THREE, NEVER SIX ACROSS. Six divides evenly into 1, 2, 3 and 6, so the grid is
   always complete — but six across is only honest at a width this page never has, and at 1160px it
   still gives five-line bodies. Three is the widest count where a step reads as a sentence. */
@media (min-width: 640px)  { .pt-pipe { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1000px) { .pt-pipe { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.pt-pipe > li {
  counter-increment: ptpipe;
  position: relative; min-width: 0;
  display: flex; flex-direction: column; gap: var(--space-2);
  padding: var(--space-4);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
}
/* Ours. The tint is the whole message, so it is the only thing that changes. */
.pt-pipe > li[data-owner="us"] {
  background: color-mix(in srgb, var(--accent-soft) 55%, var(--surface));
  border-color: var(--accent-soft-border);
}
.pt-pipe-n {
  font-family: var(--font-mono); font-size: var(--text-2xs); font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-caps); text-transform: uppercase; color: var(--text-subtle);
}
.pt-pipe-n::before { content: counter(ptpipe) " \00b7 "; }
.pt-pipe b { font-size: var(--text-sm); font-weight: var(--weight-bold); line-height: var(--leading-snug); }
.pt-pipe p { margin: 0; font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-relaxed); }
/* The connector, and it is drawn ONLY between two cards that are actually side by side. At the end
   of a row it would point into the gutter and read as a step that failed to render. */
@media (min-width: 1000px) {
  .pt-pipe > li:not(:nth-child(3n)):not(:last-child)::after {
    content: ""; position: absolute; top: 50%; right: calc(var(--space-3) * -1);
    width: var(--space-3); height: 2px; background: var(--border-strong);
  }
}
@media (min-width: 640px) and (max-width: 999px) {
  .pt-pipe > li:not(:nth-child(2n)):not(:last-child)::after {
    content: ""; position: absolute; top: 50%; right: calc(var(--space-3) * -1);
    width: var(--space-3); height: 2px; background: var(--border-strong);
  }
}

.pt-lanes { display: flex; flex-wrap: wrap; gap: var(--space-4); margin: 0 0 var(--space-3); padding: 0; list-style: none; }
.pt-lanes li { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-xs); color: var(--text-muted); }
.pt-lanes i { width: 14px; height: 14px; border-radius: var(--radius-xs); border: 1px solid var(--border); background: var(--surface); }
.pt-lanes .is-us i { background: color-mix(in srgb, var(--accent-soft) 55%, var(--surface)); border-color: var(--accent-soft-border); }

/* ── The micro visual: a proof chain ─────────────────────────────────────────────────────────
   Four connection shapes, each with a "Proves" paragraph. The paragraph says what the evidence IS;
   the chain says where it comes from, in the order it happens. Small, monospaced, and horizontal,
   because the point being made is that it is a short chain with no step we control. */
.pt-chain {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  margin: var(--space-3) 0 0; padding: 0; list-style: none;
}
.pt-chain li {
  display: inline-flex; align-items: center; gap: var(--space-2);
  font-family: var(--font-mono); font-size: var(--text-2xs); letter-spacing: var(--tracking-normal);
  color: var(--text-muted);
}
.pt-chain li span {
  padding: 2px var(--space-2); border-radius: var(--radius-pill);
  background: var(--surface-subtle); border: 1px solid var(--border-subtle);
}
/* The last link is the one in THEIR system, which is the whole argument. */
.pt-chain li:last-child span { background: var(--accent-soft); border-color: var(--accent-soft-border); color: var(--text-on-soft); font-weight: var(--weight-bold); }
.pt-chain li:not(:last-child)::after {
  content: ""; width: 12px; height: 12px; flex: none; background: var(--text-subtle);
  mask: var(--ico-chevron) center / contain no-repeat;
  -webkit-mask: var(--ico-chevron) center / contain no-repeat;
  transform: rotate(-90deg);
}
/* A chain with nothing in it. Shape 1 proves nothing, and an empty row would read as unfinished
   rather than as the point. */
.pt-chain.is-none li span { background: transparent; border-style: dashed; color: var(--text-subtle); font-weight: var(--weight-regular); }

/* ── The microscopic visual: one basket, two systems ─────────────────────────────────────────
   Shape 3 is what most partners run, and it is the one that sounds like magic until it is drawn.
   Two lanes, five exchanges, and the mono sub-line under each is the actual artefact that exists
   at that moment. Nothing here is a screenshot of anything — it is a sequence, and it is labelled
   as one, because a fabricated console is a lie that renders well. */
.pt-seq {
  display: grid; gap: 0; margin: var(--space-4) 0 0; padding: 0; list-style: none;
  border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden;
  background: var(--surface);
}
.pt-seq > li {
  display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: var(--space-3);
  align-items: start; padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border-subtle);
}
.pt-seq > li:first-child { border-top: 0; }
.pt-seq > li[data-side="you"] { background: var(--surface-subtle); }
.pt-seq-who {
  grid-column: 2; margin: 0;
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-subtle);
}
.pt-seq-what { grid-column: 2; margin: 2px 0 0; font-size: var(--text-sm); line-height: var(--leading-snug); }
.pt-seq-note {
  grid-column: 2; margin: var(--space-2) 0 0;
  font-family: var(--font-mono); font-size: var(--text-2xs); color: var(--text-muted);
  word-break: break-word;
}
.pt-seq .pt-glyph { grid-row: 1 / span 3; }

/* ── Can and cannot, per platform ────────────────────────────────────────────────────────────
   The platform section's whole value is that it is MEASURED, and the sentence that matters in
   each card is the one about what the platform refuses to do. Chips pull those out of the prose
   so the two platforms can be compared without reading both paragraphs twice. */
.pt-caps { display: grid; gap: var(--space-2); margin: var(--space-3) 0 0; padding: 0; list-style: none; }
.pt-caps li {
  display: grid; grid-template-columns: 15px minmax(0, 1fr); gap: var(--space-3); align-items: start;
  font-size: var(--text-sm); line-height: var(--leading-snug); color: var(--text-muted);
}
.pt-caps li::before {
  content: ""; width: 15px; height: 15px; margin-top: 2px; background: currentColor;
  mask: var(--pc-cap) center / contain no-repeat;
  -webkit-mask: var(--pc-cap) center / contain no-repeat;
}
.pt-caps li.is-yes { color: var(--text); --pc-cap: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m4 12.6 5.2 5.2L20 6.6"/></svg>'); }
.pt-caps li.is-no  { --pc-cap: var(--ico-x); }

/* The one step that runs backwards. It is a sentence, not a pipeline card, because it does not
   happen every month — but it earns the glyph, since a partner scanning the row of steps should
   see that one of them has a reverse. */
.pt-reverse {
  display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: var(--space-3);
  align-items: start; margin: var(--space-4) 0 0;
}

/* Section 7 — the promise that placement is not for sale. It is the load-bearing claim of the
   whole page, so it gets the shield and a border rather than sitting in the run of prose. */
.pt-keep {
  margin: var(--space-6) 0; padding: var(--space-6);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-left: 3px solid var(--accent); border-radius: var(--radius-md);
}
.pt-keep > h2 { margin-top: 0 !important; }
.pt-keep ul { display: grid; gap: var(--space-3); margin: var(--space-4) 0; padding-left: var(--space-5); }

/* ==================================================================================================
   THE DEMO FRAME — screens 3 and 4
   Everything below is the shell. Nothing below touches a .pc-* rule.
   ================================================================================================== */

/* The bar that says whose preview this is and how long it has left. Sticky UNDER the site header
   (62px, doc.css .head-in) rather than over it, so the countdown stays on screen while the vendor
   scrolls a long list of rankings — which is the only place they can read it. */
.pt-preview {
  /* 63px, not 62: doc.css gives .head-in a 62px height and .site-head a 1px bottom border, and
     the header is itself sticky at top:0. At 62 the bar rode a pixel up over that border once the
     page scrolled. */
  position: sticky; top: 63px; z-index: 30;
  background: var(--accent-soft); border-bottom: 1px solid var(--accent-soft-border);
  color: var(--text);
}
.pt-preview-in {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-4);
  padding: var(--space-3) 0;
}
.pt-preview-tag {
  display: inline-flex; align-items: center; gap: var(--space-2); flex: none;
  padding: 3px var(--space-3); border-radius: var(--radius-pill);
  background: var(--accent); color: var(--on-accent);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}
.pt-preview-who { font-size: var(--text-md); font-weight: var(--weight-bold); }
.pt-preview-clock {
  display: inline-flex; align-items: center; gap: var(--space-2);
  margin-left: auto; font-size: var(--text-sm); color: var(--text-on-soft);
  font-variant-numeric: tabular-nums;
}
.pt-preview-clock::before {
  content: ""; width: 14px; height: 14px; flex: none; background: currentColor;
  mask: var(--ico-clock) center / contain no-repeat;
  -webkit-mask: var(--ico-clock) center / contain no-repeat;
}
.pt-preview-clock b { font-family: var(--font-mono); font-weight: var(--weight-bold); }
.pt-preview-note { flex-basis: 100%; margin: 0; font-size: var(--text-sm); color: var(--text-muted); }
/* Closed. The same bar, drained of the accent, so the vendor recognises it as the thing that was
   counting down rather than a new component. */
.pt-preview.is-closed { background: var(--surface-sunken); border-bottom-color: var(--border); }
.pt-preview.is-closed .pt-preview-tag { background: var(--text-muted); color: var(--bg-elevated); }
.pt-preview.is-closed .pt-preview-clock { color: var(--text-muted); margin-left: auto; }
@media (max-width: 700px) {
  .pt-preview { position: static; }
  .pt-preview-clock { margin-left: 0; flex-basis: 100%; }
}

/* Marks the vendor's own row inside the ranking. It sits in the card's own head, in the accent,
   and it deliberately does NOT move the row: the whole argument of section 7 is that a partner is
   ranked exactly where the engine puts them. */
.pt-you {
  display: inline-flex; align-items: center;
  padding: 2px var(--space-2); border-radius: var(--radius-pill);
  border: 1px solid var(--accent); background: var(--accent-soft); color: var(--text-on-soft);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}

/* The block under the demo: what a partnership adds, the price, the door. */
.pt-demoend {
  margin: var(--space-10) 0 0; padding: var(--space-8);
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-lg);
}
.pt-demoend h2 { margin-top: 0 !important; }

/* How the connection works — sections 2 to 6 of docs/partners-page-copy.md, restored under the
   demo on /preview. It is a plain reading column, not a card: everything inside it already has a
   treatment (.pt-shapes, .pt-flow, .pt-need), and wrapping the lot in another bordered box would
   put three borders between a partner and a sentence. All this rule owns is the gap above it and
   the measure. */
.pt-mechanics { margin: var(--space-10) 0 0; }
.pt-mechanics > h2 { margin-top: var(--space-8); }
.pt-mechanics > h2:first-child { margin-top: 0; }
.pt-mechanics > p { max-width: 70ch; }


/* The expired screen's headline block. Same furniture, more weight, because on that screen it is
   the page rather than a footer to it. */
.pt-expired { margin: var(--space-6) 0 var(--space-8); }
.pt-expired-when {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-3);
  margin: 0 0 var(--space-6); padding: var(--space-4) var(--space-5);
  background: var(--surface-subtle); border: 1px solid var(--border); border-radius: var(--radius-md);
  font-size: var(--text-md);
}
.pt-expired-when time { font-family: var(--font-mono); font-weight: var(--weight-bold); }

/* ==================================================================================================
   THE APPEARANCE CONTROL — the eight screens that carry no nav
   ==================================================================================================
   colors.css has always held both palettes and every screen here has always resolved one before
   first paint. What was missing was anywhere to CHOOSE: on www the control lives in the nav, and
   AGENTS.md says the doors and the signed-in screens carry no nav on purpose — "a door is not a
   page to browse away from". So eight screens inherited the half that reads the preference and
   none of the half that sets it, and a partner on a light machine had no route to a dark one.

   SCOPED TO THE TWO CONTAINERS, WHICH IS THIS FILE'S OWN RULE RATHER THAN CAUTION. `.icon-btn` is
   a name web/doc.css owns, and the header above says this file must never redefine one: /partners
   and the two demo frames link doc.css AND this file, so a bare `.icon-btn` here would land on
   three pages that already have one and quietly win on load order. `.pt-auth-top` and
   `.pt-app-who` exist only on the eight screens that have no doc.css, so these rules are inert
   everywhere doc.css is present — and the demo frames keep taking their button from doc.css,
   unchanged.

   NOT A NEW SIZE EITHER. doc.css draws this button at a hardcoded 34px; --control-h-sm IS 34px,
   so the two hosts match by construction rather than by a number typed twice. */
.pt-auth-top .icon-btn,
.pt-app-who .icon-btn {
  display: grid; place-items: center; flex: none;
  width: var(--control-h-sm); height: var(--control-h-sm); padding: 0;
  background: var(--surface); color: var(--text-muted);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  font-family: inherit; cursor: pointer;
}
.pt-auth-top .icon-btn:hover,
.pt-app-who .icon-btn:hover { color: var(--text); border-color: var(--border-strong); }
.pt-auth-top .icon-btn svg,
.pt-app-who .icon-btn svg { width: 16px; height: 16px; }

/* BOTH GLYPHS SHIP AND CSS HIDES ONE, so the button never flashes the wrong icon while a second
   file loads. Show the destination, not the state: in light mode the moon means "go dark". */
.pt-auth-top .ico-sun,
.pt-app-who .ico-sun { display: none; }
[data-theme="dark"] .pt-auth-top .ico-moon,
[data-theme="dark"] .pt-app-who .ico-moon { display: none; }
[data-theme="dark"] .pt-auth-top .ico-sun,
[data-theme="dark"] .pt-app-who .ico-sun { display: block; }

/* The card's top row. The brand lockup carried the margin under it when it was the only thing on
   this line; the row owns that spacing now, or the gap doubles on every door. */
.pt-auth-top { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-6); }
.pt-auth-top .pt-auth-brand { margin-bottom: 0; }
.pt-auth-top .icon-btn { margin-left: auto; }

/* ==================================================================================================
   THE PORTAL — signed out (screens 2, 2b, 5)
   The app/signin.html arrangement, kept close enough to read as family. The one deliberate
   difference is that these say "Partner" in the tab AND the heading: two identical sign-in pages
   on two hosts, one of them for people tracking their own doses, is a support problem.
   ================================================================================================== */

.pt-auth {
  min-height: 100dvh;
  display: grid; place-items: center; align-content: center;
  padding: var(--space-8) var(--space-4);
  background: var(--bg); color: var(--text);
}
.pt-auth-card { width: min(420px, 100%); }
/* The account screen carries a price summary beside the form, so it is allowed to be wider. */
.pt-auth-card-wide { width: min(760px, 100%); }

.pt-auth-brand {
  display: flex; align-items: center; gap: var(--space-2);
  margin-bottom: var(--space-6);
  font-weight: var(--weight-extrabold); font-size: var(--text-md);
  letter-spacing: var(--tracking-tight);
}
/* One drawing, both themes: the square takes --accent and the triangle --on-accent, and
   colors.css flips that pair. Same as app/signin.html — no dark-mode branch needed. */
.pt-auth-brand .brand-mark { width: 26px; height: 26px; flex: none; }
.pt-auth-brand .mark-bg { fill: var(--accent); }
.pt-auth-brand .mark-tri { fill: none; stroke: var(--on-accent); stroke-width: 8; stroke-linejoin: round; stroke-linecap: round; }
.pt-auth-brand .brand-word { letter-spacing: 0.06em; }
.pt-auth-brand .brand-word b { color: var(--accent-text, var(--accent)); }
/* The word that stops this being the consumer sign-in page. It reads as part of the wordmark
   because it is part of the identity of the host, not a page title. */
.pt-auth-brand .pt-host {
  margin-left: var(--space-1); padding: 2px var(--space-2);
  border-radius: var(--radius-pill); background: var(--accent-soft); color: var(--text-on-soft);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}

.pt-auth-tabs {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
  padding: 4px; margin-bottom: var(--space-6);
  background: var(--surface-sunken); border: 1px solid var(--border-subtle); border-radius: var(--radius-md);
}
.pt-auth-tab {
  display: grid; place-items: center; min-height: var(--control-h);
  padding: 0 var(--space-2); border-radius: var(--radius-sm);
  font-size: var(--text-sm); font-weight: var(--weight-bold); color: var(--text-muted);
  text-align: center; text-decoration: none;
}
.pt-auth-tab[aria-current="page"] { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }
.pt-auth-tab:not([aria-current="page"]):hover { color: var(--text); }

.pt-auth h1 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-3xl); font-weight: var(--weight-extrabold);
  line-height: var(--leading-tight); letter-spacing: var(--tracking-tight);
}
.pt-auth-lede {
  margin: 0 0 var(--space-6);
  font-size: var(--text-md); color: var(--text-muted); line-height: var(--leading-relaxed);
}

.pt-auth-panel {
  padding: var(--space-6);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-md); box-shadow: var(--shadow-sm);
}
.pt-auth-panel h2 {
  margin: 0 0 var(--space-4); font-size: var(--text-md); font-weight: var(--weight-bold);
}
/* A second panel under the form — the explanation of what the vendor is about to see. Spaced by
   sibling rules rather than a modifier class, so a third panel needs no new CSS. The .pt-auth-alt
   case is the live one: "Already a partner? Sign in" sits between the two panels, directly under
   the invite box where the brief puts it. */
.pt-auth-panel + .pt-auth-panel,
.pt-auth-alt + .pt-auth-panel { margin-top: var(--space-6); }
.pt-panel-body {
  margin: 0; font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-relaxed);
}
.pt-panel-body + .pt-panel-body { margin-top: var(--space-3); }

.pt-field { display: grid; gap: var(--space-1); margin-bottom: var(--space-4); }
.pt-field label { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.pt-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.pt-field input, .pt-field textarea, .pt-field select {
  min-height: var(--control-h-lg); padding: var(--space-2) var(--space-3);
  background: var(--bg-elevated); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); font-size: var(--text-md); color: var(--text);
}
.pt-field textarea { min-height: 76px; resize: vertical; line-height: var(--leading-normal); }
/* THE FIVE DROPDOWNS WERE THE ONLY NATIVE CONTROLS ON THIS FORM, and they read as a different
   form. The rule above named input and textarea and not select, so twenty-six boxes were 40px
   tall in the elevated surface with our border, and the five beside them were 23px of whatever
   the platform draws -- under half a thumb on a phone. Nothing failed: every one of them worked.
   appearance: none takes the arrow with it, so the caret is drawn back on as a background image
   in currentColor, and padding-right keeps the longest option clear of it. */
.pt-field select {
  appearance: none; -webkit-appearance: none;
  padding-right: calc(var(--space-3) + 20px); cursor: pointer;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27 fill=%27none%27 stroke=%27%236b7280%27 stroke-width=%272%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27><path d=%27M6 9l6 6 6-6%27/></svg>");
  background-repeat: no-repeat; background-position: right var(--space-3) center; background-size: 16px 16px;
}
.pt-field input:focus-visible, .pt-field textarea:focus-visible, .pt-field select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pt-field input::placeholder, .pt-field textarea::placeholder { color: var(--text-subtle); }
.pt-hint { margin: 0; font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-snug); }
/* The invite code is typed off a screen or read off paper, so it is set in the mono face at a size
   that survives both, and it never autocapitalises or autocorrects. */
.pt-field input.pt-code {
  font-family: var(--font-mono); font-size: var(--text-xl); letter-spacing: 0.14em;
  text-transform: uppercase; text-align: center;
}

.pt-check { display: flex; align-items: flex-start; gap: var(--space-3); margin-bottom: var(--space-4); }
.pt-check input { width: 18px; height: 18px; margin-top: 2px; flex: none; accent-color: var(--accent); cursor: pointer; }
.pt-check label { font-size: var(--text-sm); font-weight: var(--weight-regular); line-height: var(--leading-snug); }

/* Left hidden in the markup. The build unhides one when it has something to say; a message region
   that renders empty is worse than one that is absent. */
.pt-error, .pt-notice {
  margin: 0 0 var(--space-4); padding: var(--space-3);
  border-radius: var(--radius-sm); font-size: var(--text-sm); line-height: var(--leading-snug);
}
.pt-error { background: var(--danger-soft); color: var(--danger); }
.pt-notice { background: var(--success-soft); color: var(--success); }
.pt-error[hidden], .pt-notice[hidden] { display: none; }

.pt-auth-alt {
  margin: var(--space-6) 0 0; text-align: center;
  font-size: var(--text-sm); color: var(--text-muted);
}
.pt-auth-alt a { color: var(--accent-text, var(--accent)); font-weight: var(--weight-semibold); text-decoration: underline; text-underline-offset: 2px; }
.pt-auth-legal {
  margin: var(--space-4) 0 0; text-align: center;
  font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-snug);
}
.pt-auth-legal a { color: var(--text-muted); text-decoration: underline; text-underline-offset: 2px; }

/* Account + payment: form on the left, what it costs on the right. The figures sit BESIDE the
   button rather than on the page before it, because the button is where the commitment happens. */
.pt-checkout { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: var(--space-5); align-items: start; }
@media (max-width: 700px) {
  /* The summary goes FIRST on a phone. Someone should read the charge before the form, not scroll
     past a completed form to find it. */
  .pt-checkout { grid-template-columns: minmax(0, 1fr); }
  .pt-checkout > .pt-summary { order: -1; }
}
.pt-summary {
  padding: var(--space-5);
  background: var(--surface-subtle); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.pt-summary h2 {
  margin: 0 0 var(--space-4); font-size: var(--text-xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-muted);
}
.pt-summary dl { display: grid; gap: var(--space-3); margin: 0; }
.pt-summary dl > div {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-3);
  align-items: baseline; padding-bottom: var(--space-3); border-bottom: 1px solid var(--border-subtle);
}
.pt-summary dl > div:last-child { border-bottom: 0; padding-bottom: 0; }
.pt-summary dt { font-size: var(--text-sm); color: var(--text-muted); }
.pt-summary dd {
  margin: 0; font-family: var(--font-mono); font-size: var(--text-md);
  font-weight: var(--weight-bold); text-align: right; font-variant-numeric: tabular-nums;
}
/* The one figure the button actually takes, said once and loudly. It sits apart from the three
   rate rows above it because "what recurs" and "what leaves your account when I press this" are
   different questions, and only the second one is a decision. */
.pt-due {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3);
  margin: var(--space-4) 0 0; padding: var(--space-4);
  background: var(--accent-soft); border: 1px solid var(--accent-soft-border);
  border-radius: var(--radius-md); color: var(--text-on-soft);
}
.pt-due span { font-size: var(--text-sm); font-weight: var(--weight-semibold); }
.pt-due b {
  font-family: var(--font-mono); font-size: var(--text-2xl); font-weight: var(--weight-extrabold);
  font-variant-numeric: tabular-nums;
}

.pt-summary p { margin: var(--space-4) 0 0; font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-snug); }
.pt-summary p + p { margin-top: var(--space-2); }

/* ==================================================================================================
   THE PORTAL — signed in (screen 6, both states)
   ================================================================================================== */

.pt-app-head {
  position: sticky; top: 0; z-index: 40;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border-subtle);
}
/* padding-block, NOT the shorthand. This element carries .pt-wrap for its max-width and its
   gutter, and `padding: var(--space-2) 0` set the inline axis to zero along with it -- so the
   brand sat against the left edge of the screen and Sign out against the right, on a header
   above a page inset 16 to 36px. Nothing looked broken; it looked like a full-bleed header, and
   the misalignment only reads as a mistake once you notice the two edges disagree. */
.pt-app-head-in {
  display: flex; align-items: center; gap: var(--space-4);
  min-height: 62px; padding-block: var(--space-2); flex-wrap: wrap;
}
.pt-app-brand { display: inline-flex; align-items: center; gap: var(--space-2); flex: none; color: var(--text); }
.pt-app-brand .brand-mark { width: 25px; height: 25px; flex: none; }
.pt-app-brand .mark-bg { fill: var(--accent); }
.pt-app-brand .mark-tri { fill: none; stroke: var(--on-accent); stroke-width: 8; stroke-linejoin: round; stroke-linecap: round; }
.pt-app-brand .brand-word { font-size: var(--text-sm); font-weight: var(--weight-extrabold); letter-spacing: .085em; }
.pt-app-brand .brand-word b { color: var(--accent-text, var(--accent)); }
.pt-app-brand .pt-host {
  padding: 2px var(--space-2); border-radius: var(--radius-pill);
  background: var(--accent-soft); color: var(--text-on-soft);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}
.pt-app-who { margin-left: auto; display: inline-flex; align-items: center; gap: var(--space-3); font-size: var(--text-sm); }
.pt-app-who .pt-store { font-weight: var(--weight-bold); }
.pt-app-out { color: var(--text-muted); font-weight: var(--weight-semibold); }
.pt-app-out:hover { color: var(--text); }

/* ── The console rail ─────────────────────────────────────────────────────────────────────────
   Four views of a partner's own record, one at a time, with a rail naming them.

   THE SCREEN WAS COMPLETE AND UNREADABLE, AND THOSE ARE NOT THE SAME PROBLEM. Everything a
   partner holds — the record, the handoffs, thirty-one connection fields, six credential kinds
   and the billing state — ran down one column in document order, so the first thing somebody saw
   on the day they signed up was a form longer than four screens with nothing saying what was
   above it or below it. Not one field was missing. It simply could not be surveyed, and a partner
   cannot act on what they cannot find.

   THE VIEWS STAY IN FLOW UNTIL A SCRIPT HIDES THEM, which is the whole of the fallback and is why
   nothing here hides anything. `hidden` is set by the rail's own script and by nothing else, so a
   reader whose JavaScript never arrived gets yesterday's page: long, in document order, complete.
   A CSS-only version would take the vault and the billing state away from somebody who can still
   see the menu — the one direction this must never fail in.

   THE RAIL IS WIDER THAN THE ADMIN CONSOLE'S BECAUSE IT CARRIES A LINE OF PROSE. Staff know what
   "Connection" means here. A partner on their first morning does not, and "How a basket reaches
   your cart" is the difference between a menu and a map. */
.pt-console { --pt-side-w: 248px; --pt-head-h: 63px; }

/* The header widens with the shell, because the rail moved the console’s left edge out from
   under it. .pt-wrap caps at 1160px, which was the content width before there was a rail; leaving
   it there put the brand at 169px, the rail at 36 and the first heading at 308 — three left edges
   on one screen, none of them wrong on its own. The brand now sits directly above the rail and
   Sign out above the right edge of the pane below it. Below 900px both are viewport-limited and
   this binds on nothing. */
.pt-console .pt-app-head-in { max-width: calc(1160px + var(--pt-side-w) + var(--space-6)); }

.pt-shell {
  width: 100%; max-width: calc(1160px + var(--pt-side-w) + var(--space-6));
  margin: 0 auto; padding: 0 clamp(16px, 4vw, 36px);
  display: grid; grid-template-columns: var(--pt-side-w) minmax(0, 1fr);
  gap: var(--space-6); align-items: start;
}
/* The shell centres and pads, so the wrap inside it must not do either a second time — two
   paddings would inset the console past the header it sits directly under. */
.pt-shell > .pt-main > .pt-wrap { max-width: none; padding: 0; }

.pt-side {
  position: sticky; top: calc(var(--pt-head-h) + var(--space-4)); align-self: start;
  max-height: calc(100vh - var(--pt-head-h) - var(--space-6));
  overflow-y: auto; overscroll-behavior: contain;
  padding: clamp(24px, 4vw, 40px) 0 var(--space-6);
  display: grid; gap: 2px; align-content: start;
}
.pt-side-group {
  margin: 0 0 var(--space-2); padding: 0 var(--space-3);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-muted);
}
.pt-side-link {
  display: grid; grid-template-columns: 18px minmax(0, 1fr) auto;
  gap: var(--space-3); align-items: start;
  padding: 10px var(--space-3); border-radius: var(--radius-sm); color: var(--text-muted);
}
.pt-side-link:hover { background: var(--surface-hover); color: var(--text); }
.pt-side-link[aria-current="page"] { background: var(--accent-soft); color: var(--text-on-soft); }
.pt-side-ico {
  width: 18px; height: 18px; margin-top: 1px; fill: none;
  stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
}
.pt-side-word {
  min-width: 0; font-size: var(--text-sm);
  font-weight: var(--weight-semibold); line-height: var(--leading-snug);
}
.pt-side-link[aria-current="page"] .pt-side-word { font-weight: var(--weight-bold); }
.pt-side-note {
  display: block; margin-top: 3px;
  font-size: var(--text-2xs); font-weight: var(--weight-regular);
  line-height: var(--leading-snug); color: var(--text-muted);
}
.pt-side-link[aria-current="page"] .pt-side-note { color: inherit; opacity: .82; }
/* "Needed" against Connection while nothing is on file. It is a statement about the record and
   not a nag, so it takes the warning tokens rather than the danger ones — and --warning-text
   rather than --warning, for the reason colors.css gives where those two are defined. */
.pt-side-flag {
  padding: 1px 7px; border-radius: var(--radius-pill);
  background: var(--warning-soft); color: var(--warning-text);
  font-size: var(--text-2xs); font-weight: var(--weight-bold); white-space: nowrap;
}

.pt-views { display: grid; gap: var(--space-4); }
.pt-view[hidden] { display: none; }
/* Focus lands here when a view opens, and this is also the target of a plain anchor jump when no
   script is running. Both want the heading clear of the sticky header. */
.pt-view { scroll-margin-top: calc(var(--pt-head-h) + var(--space-4)); }
.pt-view:focus { outline: none; }

/* The drawer's two controls, both invisible until the rail has somewhere to hide. */
.pt-nav-toggle {
  display: none; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: none; cursor: pointer;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
}
.pt-nav-toggle:hover { background: var(--surface-hover); }
.pt-nav-toggle svg {
  width: 20px; height: 20px; fill: none;
  stroke: currentColor; stroke-width: 1.8; stroke-linecap: round;
}
.pt-scrim { display: none; }

/* ── The console on a phone ───────────────────────────────────────────────────────────────────
   The rail becomes a drawer at the same 900px the admin console uses, and for the same reason:
   248px of rail beside a 31-field form is most of a phone.

   EVERY MOBILE RULE BELOW IS SCOPED INSIDE THE QUERY, so widening the window puts the rail back
   with no script involved. A drawer left open and then resized to desktop would otherwise hold a
   full-screen scrim over a page nothing could reach — the toggle that closes it is display:none
   at that width. */
@media (max-width: 900px) {
  .pt-shell { display: block; max-width: none; padding: 0; }
  .pt-shell > .pt-main > .pt-wrap { max-width: 1160px; padding: 0 clamp(16px, 4vw, 36px); }

  .pt-nav-toggle { display: inline-flex; }

  .pt-side {
    position: fixed; inset: 0 auto 0 0; z-index: 60;
    width: min(82vw, 300px); height: 100%; max-height: none;
    padding: var(--space-5) var(--space-3) var(--space-6);
    background: var(--bg-elevated); border-right: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
    transform: translateX(-100%); visibility: hidden;
    /* visibility flips late on the way out so the slide stays visible, and at once on the way in.
       It is here at all to keep four links out of the tab order while they are off-screen. */
    transition: transform .18s ease, visibility 0s linear .18s;
  }
  .pt-shell.is-nav-open .pt-side {
    transform: none; visibility: visible;
    transition: transform .18s ease, visibility 0s;
  }
  .pt-scrim { display: block; position: fixed; inset: 0; z-index: 55; background: rgba(7, 10, 16, .5); }
  .pt-side-link { padding: 12px var(--space-3); }
}

/* ── The console on a small phone ─────────────────────────────────────────────────────────────
   Below 560px the header's own line is the tight one: brand, host chip, company name, badge,
   appearance and Sign out do not fit, and flex-wrap answers by giving the header two rows. The
   chip and the company name go instead — the company name is the <h1> of the page directly
   beneath it, and the host chip says "Partners" on the partner host. */
@media (max-width: 560px) {
  /* Three pieces of chrome go, and they go together because each restates something the page
     already says. The host chip says "Partners" on the partner host; the company name is the
     <h1> directly beneath it; and the role badge with the company name gone says "Partner"
     attached to nobody. Keeping the badge and dropping the name is the worse half of the trade.
     nowrap is only safe once they are gone: at 320px the six children came to 419 and pushed the
     header past the screen while the panes below it sat correctly inset, so the page scrolled
     sideways to reveal nothing but Sign out. */
  .pt-app-brand .pt-host,
  .pt-app-who .pt-store,
  .pt-app-who .pt-badge { display: none; }
  .pt-app-head-in { gap: var(--space-3); flex-wrap: nowrap; }
  /* flex: none is on the brand by default, so min-width alone bought nothing: at 320px the row
     was still 7px over and Sign out sat past the gutter every other element respected. The
     wordmark is the only thing here that can lose a character and still say what it is. */
  .pt-app-brand { min-width: 0; flex: 0 1 auto; }
  .pt-app-brand .brand-word { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

  .pt-form-actions .pt-btn { width: 100%; }
}

.pt-main { padding: clamp(24px, 4vw, 40px) 0 clamp(48px, 7vw, 80px); }
.pt-title { margin: 0 0 var(--space-2); font-size: var(--title); font-weight: var(--weight-extrabold); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); }
.pt-sub { margin: 0 0 var(--space-6); font-size: var(--text-md); color: var(--text-muted); max-width: 66ch; }

/* The four panes. Two explicit columns from 900px, one below — not auto-fit, because auto-fit
   would give three columns on a wide screen and drop the record's six score bars into a 300px
   sliver. The clicks table and the record both hold their shape at 360px because neither is wider
   than its own text. */
.pt-panes { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); align-items: start; }
@media (min-width: 900px) {
  .pt-panes { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
.pt-pane {
  min-width: 0; padding: var(--space-5);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.pt-pane-head { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-4); }
.pt-pane-head h2 { margin: 0; font-size: var(--text-lg); font-weight: var(--weight-bold); letter-spacing: var(--tracking-tight); }
.pt-pane-head .pt-pane-note { margin-left: auto; font-size: var(--text-xs); color: var(--text-muted); }
.pt-pane > p { margin: 0 0 var(--space-3); max-width: 60ch; }
.pt-pane > p:last-child { margin-bottom: 0; }
.pt-pane-foot {
  margin-top: var(--space-4); padding-top: var(--space-4);
  border-top: 1px solid var(--border-subtle);
  font-size: var(--text-sm); color: var(--text-muted);
}
/* The form pane runs the full width under the two narrow ones: four fields at 320px would each be
   a single word wide. */
.pt-pane-wide { grid-column: 1 / -1; }

/* A read-only fact list. Label left, value right, on a grid so the values stack as a column —
   same reasoning as the rank card's ledger in doc.css.

   THE LABEL'S TRACK HAS A FLOOR BECAUSE `minmax(0, 1fr)` LET IT REACH ZERO, AND A ZERO-WIDTH
   `dt` STILL PAINTS ITS TEXT. An `auto` track is sized to max-content BEFORE any fr track is
   handed leftover space, so a `.pt-absent` carrying a whole sentence — "not counted yet — orders
   are reconciled at month end" — claimed the row and left the label nothing. Nothing clips a dt,
   so "Commission" went on drawing itself across the pill it had just been squeezed behind.
   Measured on the deployed /home: a 304px row with `dt` at 0px and `dd` at 292px, and with the
   floor in place 70px + 12px + 221px, the pill wrapping to two lines.

   AND THE 560px FALLBACK BELOW COULD NEVER HAVE CAUGHT IT, WHICH IS THE PART WORTH REMEMBERING.
   That block stacks the label above the value and is right to; it is a MEDIA query, so it asks
   the viewport. The thing that is actually short here is the pane, which sits in a sidebar
   layout — so the row was 304px wide at a 1066px viewport, the widest the bug gets rather than
   the narrowest. A breakpoint on the window cannot see a column inside it. */
.pt-kv { display: grid; gap: var(--space-3); margin: 0; }
.pt-kv > div {
  display: grid; grid-template-columns: minmax(min-content, 1fr) auto; gap: var(--space-3);
  align-items: baseline; padding-bottom: var(--space-3); border-bottom: 1px solid var(--border-subtle);
}
.pt-kv > div:last-child { border-bottom: 0; padding-bottom: 0; }
.pt-kv dt { font-size: var(--text-sm); color: var(--text-muted); }
.pt-kv dd { margin: 0; font-size: var(--text-sm); font-weight: var(--weight-semibold); text-align: right; }

/* A NAME OVER EACH BLOCK INSIDE A PANE. "Your record" ran six graded dimensions straight into five
   recorded facts with nothing between them, so two different kinds of statement — what a rubric
   scored, and what we hold — read as one list that changed shape halfway down. Same treatment as
   .pt-table thead th, because it is the same job: the house's small label for a column of facts. */
.pt-pane-label {
  margin: 0 0 var(--space-3);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-muted);
}
/* The second and subsequent labels carry the divider. The first sits under .pt-pane-head, which
   has its own margin and does not need a second line drawn across the card. */
.pt-pane-label-rule {
  margin-top: var(--space-4); padding-top: var(--space-4);
  border-top: 1px solid var(--border-subtle);
}

/* Score dimensions. The rung-bars shape from doc.css, rebuilt here because the portal has no
   doc.css — same three-column grid, same accent fill, same mono figure. */
.pt-bars { display: grid; gap: var(--space-3); margin: 0 0 var(--space-4); padding: 0; list-style: none; }
.pt-bars li { display: grid; grid-template-columns: minmax(0, 1fr) 96px 36px; gap: var(--space-3); align-items: center; }
.pt-bars span { font-size: var(--text-xs); color: var(--text-muted); }
.pt-bars .pt-bar { height: 6px; border-radius: 3px; background: var(--surface-sunken); border: 1px solid var(--border-subtle); overflow: hidden; }
.pt-bars .pt-bar i { display: block; height: 100%; background: var(--accent); }
.pt-bars b { font-family: var(--font-mono); font-size: var(--text-xs); text-align: right; font-variant-numeric: tabular-nums; }

/* AN UNSCORED DIMENSION HAS NO RAIL AND THEREFORE NO 96px COLUMN TO LEAVE EMPTY. The named absence
   takes the rail and the figure together and sizes to its own words; the label column is untouched,
   so the dimensions still read down a single line. Against the old fixed 30px the pill wrapped to
   two lines and overhung the card, which is how this was found.
   nowrap here and NOT on .pt-absent itself: the same pill carries a whole sentence in .pt-kv dd
   ("no vendor listing — this pane describes storefronts in the directory"), which must wrap. */
.pt-bars li.is-absent { grid-template-columns: minmax(0, 1fr) auto; }
.pt-bars li.is-absent b { font-family: inherit; }
.pt-bars .pt-absent { white-space: nowrap; }

.pt-score {
  display: inline-flex; align-items: center; padding: 3px var(--space-3);
  border-radius: var(--radius-pill); background: var(--accent); color: var(--on-accent);
  font-family: var(--font-mono); font-size: var(--text-xs); font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
}
/* THE BADGE HOLDS A NAMED ABSENCE FOR THE ONE PARTNER ACCOUNT THAT EXISTS TODAY, and the slot fills
   its inner HTML, so the wrapper cannot be given a class from the renderer — the shape is
   `<span class="pt-score"><span class="pt-absent">not scored</span></span>` and CSS is the only
   place it can be told apart. Left alone it drew a dashed pill inside a filled accent pill: two
   badges, one inside the other, claiming the score colour for the absence of a score.
   The neutralising rule comes first so a browser without :has() still gets one pill rather than
   two; :has() then gives the absence the dashed treatment it has everywhere else on this host. */
.pt-score .pt-absent { padding: 0; border: 0; font-size: inherit; color: inherit; }
.pt-score:has(.pt-absent) {
  background: transparent; color: var(--text);
  border: 1px dashed var(--border-strong); padding: 2px var(--space-2);
  font-family: var(--font-sans); font-weight: var(--weight-semibold);
}

/* Clicks by source. A table because the numbers are compared down a column, not read one by one. */
.pt-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.pt-table th, .pt-table td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border-subtle); }
.pt-table thead th {
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-muted);
  white-space: nowrap;
}
.pt-table td:not(:first-child), .pt-table thead th:not(:first-child) { text-align: right; font-variant-numeric: tabular-nums; }
.pt-table td:not(:first-child) { font-family: var(--font-mono); }
.pt-table tbody tr:last-child td { border-bottom: 0; }
.pt-table tfoot td { border-top: 1px solid var(--border); border-bottom: 0; font-weight: var(--weight-bold); padding-top: var(--space-3); }
.pt-table-scroll { overflow-x: auto; margin: 0 calc(-1 * var(--space-2)); padding: 0 var(--space-2); }

/* Handoffs per day, above the by-source table. The bars carry no axis and no gridlines on purpose:
   the exact figures are in the table directly beneath, so a second set of numbers here would be the
   same total said twice and invite the two to disagree. This is the shape, and the table is the
   record.

   `preserveAspectRatio: none` is what lets one viewBox cover any pane width — the bars stretch and
   the height stays 44. It also means the bar WIDTH is not a fixed pixel count on screen, which is
   fine for a shape and would not be for anything read as a quantity.

   A ZERO DAY IS PAINTED, at one unit and in the border colour. It has to be visibly a day we
   counted and found nothing on, because the alternative reading — that the window has a hole in it
   — is the one thing this chart exists to rule out. */
.pt-spark { margin: 0 0 var(--space-3); }
.pt-spark-svg { display: block; width: 100%; height: 44px; }
.pt-spark-bar { fill: var(--accent); }
.pt-spark-none { fill: var(--border-strong); }
.pt-spark-cap {
  display: flex; justify-content: space-between;
  margin-top: var(--space-1);
  font-size: var(--text-2xs); color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Nothing recorded yet, said as a fact rather than an apology. Dashed, matching .pt-absent and
   doc.css's .rn-none — the house treatment for a declared absence. */
.pt-empty {
  display: grid; gap: var(--space-2); padding: var(--space-5);
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
  font-size: var(--text-sm); color: var(--text-muted); text-align: center;
}
.pt-empty b { color: var(--text); font-size: var(--text-md); }

/* The onboarding form. Grouped, because it is no longer four fields: it carries the store, the
   people, the billing address and the handover, and an eighteen-field wall with no seams in it is
   the fastest way to get a half-filled one back. Native <fieldset>/<legend>, so the grouping is
   announced rather than merely drawn. */
.pt-group { margin: 0 0 var(--space-6); padding: 0; border: 0; min-width: 0; }
.pt-group:last-of-type { margin-bottom: var(--space-4); }
.pt-group > legend {
  padding: 0; margin-bottom: var(--space-1);
  font-size: var(--text-xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-muted);
}
/* The rule under the legend is drawn on a following element rather than as a legend border,
   because a border on a legend does not span the fieldset. */
.pt-group-rule { height: 1px; margin: 0 0 var(--space-4); background: var(--border-subtle); }
.pt-group > p, .pt-group-note {
  margin: 0 0 var(--space-4); max-width: 62ch;
  font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-relaxed);
}

.pt-formgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0 var(--space-5); }
.pt-formgrid .pt-field { min-width: 0; }
/* A field that should not be squeezed into a third of a row — a street address, a full URL. */
.pt-field-wide { grid-column: 1 / -1; }
/* Postcode, country and the like: real inputs sized to their real content rather than stretched
   to a third of the pane because the grid had room. */
.pt-formgrid-tight { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
/* THREE PAIRS, AND auto-fit IS WRONG FOR THEM RATHER THAN MERELY LOOSE. The bulk ladder is six
   boxes that mean something two at a time; at one pane width auto-fit lands three to a row, so a
   row then reads count, percentage, count — the last belonging to the next rung. A ladder read
   across rows that is not the ladder. Two fixed columns keep each rung whole, one below 560px. */
.pt-formgrid-pairs { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 560px) { .pt-formgrid-pairs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.pt-form-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3);
  margin-top: var(--space-2);
}
.pt-form-actions .pt-hint { flex: 1 1 24ch; }
/* The current value of a field, above the input that changes it. On a product whose whole claim is
   named absence, an empty text box must not be the only way a vendor learns nothing is on file. */
.pt-current { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-xs); color: var(--text-muted); }
.pt-current b { font-weight: var(--weight-semibold); color: var(--text); overflow-wrap: anywhere; }

/* ── A BOX THAT HOLDS MORE THAN ONE ANSWER ────────────────────────────────────────────────────
   Where you ship and what you take payment in are sets, not values, so they are a fieldset of
   checkboxes rather than a text box or a select. A <fieldset> carries the browser's own margin,
   padding and border on every engine and the reset does not reach it — left alone, each of these
   drew a second card inside the card the section already is.

   EVERY RULE BELOW IS WRITTEN THROUGH .pt-field, AND THAT IS SPECIFICITY RATHER THAN TIDINESS.
   `.pt-field input` and `.pt-field label` are both 0,1,1 and both match here — a bare .pt-opt
   would tie on the label and lose, and a bare .pt-opt input would tie on the box and win only on
   source order, which is the kind of correct that stops being correct when a rule moves. So the
   checkbox is drawn back down out of a 40px bordered text field explicitly: it inherits nothing
   from that rule that it is not told to. */
.pt-field .pt-checks { min-width: 0; margin: 0; padding: 0; border: 0; display: grid; gap: var(--space-1); }
/* A <legend> cannot be reached by <label for>, so these two fields name themselves this way
   instead. Matched to .pt-field label so the pane reads as one column of headings. */
.pt-field .pt-checks legend { padding: 0; font-size: var(--text-sm); font-weight: var(--weight-semibold); }
/* auto-fit rather than a fixed count: the widest member here is "Anywhere else, quoted per order",
   and a two-column grid at pane width wraps it to two lines beside a one-word neighbour. */
.pt-field .pt-checkgrid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: var(--space-2) var(--space-4); margin: var(--space-2) 0 var(--space-1);
}
/* The whole row is the target, not the 18px square — this is the one control on the form a
   partner uses six times in a row, and on a phone the square alone is under any thumb's minimum. */
.pt-field .pt-opt {
  display: flex; align-items: flex-start; gap: var(--space-2); min-height: 24px;
  font-size: var(--text-sm); font-weight: var(--weight-regular);
  line-height: var(--leading-snug); cursor: pointer;
}
.pt-field .pt-opt input {
  width: 18px; height: 18px; min-height: 0; margin: 1px 0 0; padding: 0; flex: none;
  background: none; border: 0; border-radius: 0;
  accent-color: var(--accent); cursor: pointer;
}
.pt-field .pt-opt input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── A SECTION IS A BOX THAT SAVES ITSELF ─────────────────────────────────────────────────────
   Seven fieldsets, seven <form>s, seven buttons. The card is drawn on the FORM rather than on the
   fieldset, and that is not a preference: a <legend> interrupts its own fieldset's top border, so
   bordering the fieldset would have put a gap in the card wall exactly where the heading sits, on
   all seven. The form is an ordinary box with nothing to interrupt.

   THE BUTTON IS QUIET UNTIL THERE IS SOMETHING TO SAVE. Seven filled accent buttons on one screen
   is seven claims of equal urgency on a form a partner is mostly reading. .is-dirty is set by the
   script on the first keystroke in a section, and it is the same class that makes the card's edge
   accent and the status line say "Not saved yet" — one signal in three places, rather than three
   things that can disagree. */
.pt-section {
  margin: 0 0 var(--space-5);
  padding: var(--space-5) var(--space-5) 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}
.pt-section .pt-group { margin: 0; }
.pt-section .pt-group:last-of-type { margin-bottom: 0; }
.pt-section:focus-within { border-color: var(--border-strong); }
.pt-section.is-dirty { border-color: var(--accent-soft-border); }

/* Bled to the card's edges so it reads as the box's own foot rather than a row that happens to be
   last, and recessed — --bg sits under --surface in both themes. */
.pt-group-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3);
  margin: var(--space-5) calc(var(--space-5) * -1) 0;
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border-subtle);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  background: var(--bg);
}
.pt-btn-section {
  background: var(--surface); border-color: var(--border-strong); color: var(--text);
}
.pt-btn-section:hover { border-color: var(--accent); color: var(--accent-text, var(--accent)); }
.pt-section.is-dirty .pt-btn-section {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent);
}
.pt-section.is-dirty .pt-btn-section:hover {
  background: var(--accent-hover); border-color: var(--accent-hover); color: var(--on-accent);
}

/* Both lines sit in the same slot and only one is ever shown, so they are sized alike — a refusal
   that is bigger than the confirmation reads as an alarm on a form where a mistyped URL is
   ordinary. The colour carries the difference. */
.pt-group-status, .pt-group-error {
  margin: 0; flex: 1 1 20ch; min-width: 0;
  font-size: var(--text-xs); line-height: var(--leading-snug);
}
.pt-group-status { color: var(--text-muted); }
.pt-section.is-dirty .pt-group-status { color: var(--accent-text, var(--accent)); }
.pt-group-error { color: var(--danger-text); }

/* The confirm dialog. .modal is components.css's; these are the two things it does not carry — a
   destructive button, and a row to put it in. */
.pt-modal .modal-body > p { margin: 0 0 var(--space-4); max-width: 46ch; color: var(--text-muted); font-size: var(--text-sm); line-height: var(--leading-relaxed); }
.pt-modal-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--space-2); }
.pt-btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.pt-btn-danger:hover { background: var(--danger); border-color: var(--danger); filter: brightness(.93); }
@media (max-width: 480px) { .pt-modal-actions .pt-btn { width: 100%; } }

@media (max-width: 640px) {
  .pt-section { padding: var(--space-4) var(--space-4) 0; }
  .pt-group-actions {
    margin: var(--space-4) calc(var(--space-4) * -1) 0;
    padding: var(--space-3) var(--space-4);
  }
  .pt-group-actions .pt-btn { width: 100%; }
}

/* ── The vault ────────────────────────────────────────────────────────────────────────────────
   Keys on file. A LIST RATHER THAN A .pt-kv, because every row carries a control: a dl is a set of
   facts and this is a set of things that can be taken away, which is a different shape to read and
   a different one to announce.

   The row is drawn twice by two different pieces of code -- server-side in partner-home-render.mjs
   for what is on file at page load, and in the browser by build-partners.mjs's vault script when a
   key is stored or removed without a reload. Both build the same four children in the same order,
   so anything here applies to both. If you change the shape, change it in all three places or the
   list restyles itself halfway down the first time somebody uses it. */
.pt-vault { display: grid; gap: var(--space-2); margin: 0 0 var(--space-5); padding: 0; list-style: none; }
.pt-vault-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-subtle); border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm); font-size: var(--text-sm);
}
.pt-vault-row > b { font-weight: var(--weight-semibold); }
/* Mono, because the four characters exist to be compared against a key held somewhere else, and
   proportional digits make that the reader's problem. */
.pt-vault-hint { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-muted); }
.pt-vault-when { margin-left: auto; font-size: var(--text-xs); color: var(--text-subtle); }
/* The Remove button never takes the accent. It is the only control on this screen whose effect
   cannot be undone by pressing it again, so it should not be the thing the eye lands on. */
.pt-btn-quiet {
  min-height: var(--control-h-sm); padding: 0 var(--space-3); font-size: var(--text-xs);
  background: transparent; border-color: var(--border-strong); color: var(--text-muted);
}
.pt-btn-quiet:hover { border-color: var(--danger); color: var(--danger); }
/* Nothing on file, said rather than left blank -- the same rule as .pt-empty, at the smaller size
   a one-line answer wants. It is the true state for most partners for most of this product's life. */
.pt-vault-empty {
  padding: var(--space-4); text-align: center; font-size: var(--text-sm);
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
}

/* What each kind is. Shut by default: a partner who knows which key they are pasting should not
   have to scroll past six definitions to reach the box, and one who does not can open it.

   .pt-tint-legend RIDES THE SAME RULES rather than carrying a copy of them. It is the same
   component doing the same job one pane down — a shut list of definitions for a closed vocabulary,
   generated from the module its control is generated from. Two blocks of identical declarations
   would be two things to keep in step, and the one that drifts is whichever nobody is looking at. */
.pt-vault-legend, .pt-tint-legend { margin: 0 0 var(--space-4); font-size: var(--text-sm); }
.pt-vault-legend > summary, .pt-tint-legend > summary {
  cursor: pointer; color: var(--text-muted);
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
}
.pt-vault-legend > summary:hover, .pt-tint-legend > summary:hover { color: var(--accent-text, var(--accent)); }
.pt-vault-legend ul, .pt-tint-legend ul {
  display: grid; gap: var(--space-2);
  margin: var(--space-3) 0 0; padding: 0 0 0 var(--space-4);
  color: var(--text-muted); line-height: var(--leading-relaxed);
}
.pt-vault-legend b, .pt-tint-legend b { color: var(--text); font-weight: var(--weight-semibold); }

/* ── The brand pane ───────────────────────────────────────────────────────────────────────────
   The logo, and its two states. A preview beside its controls, wrapping when they stop fitting -- the shot
   first in both directions, because the question a vendor arrives with is "which logo have you
   got", and the answer should not be below the box for changing it.

   IT WRAPS INTRINSICALLY AND NOT AT A BREAKPOINT. The rail this pane sits in is itself narrow on a
   tablet and wide on a desktop, so a viewport query is answering a question about the wrong box:
   the two would sit side by side in a 20rem column on a wide screen and stack in a roomy one on a
   narrow. Flex basis puts the decision where it belongs -- at the point the pair stops fitting. */
.pt-logo {
  display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: flex-start;
  margin: 0 0 var(--space-5);
}
.pt-logo-side { flex: 1 1 18rem; min-width: 0; display: grid; gap: var(--space-3); }
.pt-logo-shot {
  flex: 0 1 15rem;
  display: flex; align-items: center; justify-content: center;
  min-height: 8rem; padding: var(--space-4);
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
}
.pt-logo-shot img { max-width: 100%; max-height: 10rem; object-fit: contain; }
/* A CHEQUERBOARD, AND ONLY BEHIND A REAL IMAGE. A logo is routinely a transparent PNG, and half of
   them are drawn in white for a dark header -- so on a plain surface the commonest upload in this
   product renders as an empty box, which reads as an upload that silently failed. The two tones are
   mid-greys rather than theme tokens on purpose: they have to hold a white mark AND a black one, in
   both themes, and a ground that flips can only ever solve one of those. It is scoped with :has()
   so it never sits behind the "No logo on file" sentence, where it would be decoration over the one
   thing on this pane that is a stated fact. Browsers without :has() get the plain box, which is the
   state this rule improves rather than the state it depends on. */
.pt-logo-shot:has(img:not([hidden])) {
  border-style: solid; border-color: var(--border-subtle);
  --sq: 10px;
  background-image:
    conic-gradient(from 90deg at 50% 50%,
      rgb(0 0 0 / 7%) 25%, transparent 0 50%, rgb(0 0 0 / 7%) 0 75%, transparent 0);
  background-size: var(--sq) var(--sq);
  background-color: rgb(128 128 128 / 12%);
}
/* The address, and whether it answers yet. Mono for the host itself, the same argument as
   .pt-vault-hint: it exists to be compared, character by character, against something typed
   somewhere else, and proportional letterforms make that the reader's problem. */
.pt-brand-host {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2);
  margin: 0; font-size: var(--text-sm);
}
.pt-brand-host b { font-family: var(--font-mono); font-weight: var(--weight-semibold); overflow-wrap: anywhere; }
.pt-brand-live {
  padding: 0 var(--space-2); border-radius: var(--radius-sm);
  font-size: var(--text-xs); font-weight: var(--weight-semibold);
  background: var(--success-soft); color: var(--success-text);
}
/* A control that lives inside a .pt-field's own grid, so it takes its content's width rather than
   the column's -- a full-bleed "Use one colour" would read as this field's primary action. */
.pt-btn-inline { justify-self: start; margin: var(--space-1) 0 0; }

/* THE SWATCH IS SHOWING SOMETHING NOBODY CHOSE. <input type="color"> has no empty state: it reads
   #000000 for a box nobody has touched, so an untouched field would otherwise present black as this
   vendor's brand colour. The "On file" line above it already says the true thing in words; this is
   what stops the swatch contradicting it at a glance. Set and cleared by the injected script -- see
   BRAND_NO_BLANK in build-partners.mjs, which is derived from the field list rather than typed. */
.pt-field.is-unset input[type="color"] { opacity: 0.4; border-style: dashed; }

/* A row of stated facts at the top of the signed-in page — what is live, what is not. */
.pt-banner {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-5); padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-md); font-size: var(--text-sm);
  background: var(--warning-soft); border: 1px solid color-mix(in srgb, var(--warning) 30%, transparent);
  color: var(--warning-text);
}
.pt-banner b { font-weight: var(--weight-bold); }
/* The live state. --success as the WORDS measures 3.08:1 on --success-soft in light mode, which
   is the same trap --warning-text was added to colors.css to solve — except there is no
   --success-text to reach for. So the green stays on the border, where 3:1 is the right floor,
   and the sentence takes --text. */
.pt-banner.is-ok {
  background: var(--success-soft); border-color: color-mix(in srgb, var(--success) 40%, transparent);
  color: var(--text);
}
.pt-banner .pt-btn { margin-left: auto; }
@media (max-width: 560px) { .pt-banner .pt-btn { margin-left: 0; width: 100%; } }

/* "Partner since". A date, at the top of the page and again in Billing — the first is recognition,
   the second is a billing fact, and they are not the same job. */
.pt-since {
  display: inline-flex; align-items: center; gap: var(--space-2);
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm); color: var(--text-muted);
}
.pt-since b { font-weight: var(--weight-semibold); color: var(--text); font-variant-numeric: tabular-nums; }
.pt-since::before {
  content: ""; width: 13px; height: 13px; flex: none; background: var(--text-subtle);
  mask: var(--ico-clock) center / contain no-repeat;
  -webkit-mask: var(--ico-clock) center / contain no-repeat;
}

/* The thank-you. Its own block at the foot of the panes rather than a line in one of them: it is
   addressed to the person, and the panes above are all addressed to the account. Quiet — the
   voice rules rule out anything warmer than this, and the substance is the second sentence. */
.pt-thanks {
  grid-column: 1 / -1;
  padding: var(--space-6);
  background: var(--surface-subtle);
  border: 1px solid var(--border-subtle); border-left: 3px solid var(--accent);
  border-radius: var(--radius-md);
}
.pt-thanks h2 {
  margin: 0 0 var(--space-2); font-size: var(--text-md); font-weight: var(--weight-bold);
}
.pt-thanks p { margin: 0; max-width: 66ch; font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-relaxed); }
.pt-thanks .pt-sign {
  display: block; margin-top: var(--space-3);
  font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text);
}

/* The partner badge, as it appears on the vendor's own record once they are live. Same pill as the
   score chip so the two sit together in the header without arguing. */
.pt-badge {
  display: inline-flex; align-items: center; gap: var(--space-1);
  padding: 3px var(--space-3); border-radius: var(--radius-pill);
  background: var(--accent-soft); color: var(--text-on-soft); border: 1px solid var(--accent-soft-border);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}
.pt-badge::before {
  content: ""; width: 12px; height: 12px; flex: none; background: currentColor;
  mask: var(--ico-shield) center / contain no-repeat;
  -webkit-mask: var(--ico-shield) center / contain no-repeat;
}

/* ── Portal footer ────────────────────────────────────────────────────────────────────────────
   Deliberately not the www footer: the marketing nav has no business behind a login, and the
   policy links do. */
.pt-foot {
  border-top: 1px solid var(--border); background: var(--surface-subtle);
  padding: var(--space-6) 0 var(--space-8);
}
.pt-foot p { margin: 0 0 var(--space-3); max-width: 68ch; font-size: var(--text-sm); color: var(--text-muted); }
.pt-foot nav { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); }
.pt-foot nav a { font-size: var(--text-sm); color: var(--text-muted); }
.pt-foot nav a:hover { color: var(--text); text-decoration: underline; }

/* ── A marked placeholder ─────────────────────────────────────────────────────────────────────
   docs/partners-page-copy.md is the approved copy, and the brief is explicit: where a screen needs
   a line that is not in it, leave a marked placeholder rather than inventing one. This is that
   marker. It is deliberately loud — anything wearing it must not reach a vendor's screen, and a
   quiet placeholder is one that ships. Grep for pt-todo before launch; there should be none left.

   Built from --warning-soft and --warning-text, both of which already carry dark-theme values, so
   it needs no [data-theme] branch and adds no colour. */
.pt-todo {
  display: block; margin: var(--space-3) 0; padding: var(--space-3) var(--space-4);
  background: var(--warning-soft); border: 1px dashed var(--warning);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm); line-height: var(--leading-snug); color: var(--warning-text);
}
.pt-todo::before {
  content: "Copy needed";
  display: block; margin-bottom: var(--space-1);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps);
}

/* The no-JavaScript line. Every screen here says which part needs a script and which part does
   not, because on this product the reference material never does. */
.pt-noscript {
  margin: 0; padding: var(--space-4);
  text-align: center; font-size: var(--text-sm); color: var(--text-muted);
}

/* ── The branded app ──────────────────────────────────────────────────────────────────────────
   Plan §2 and §4. A partner sets a name, a mark and one hex; everything below either collects
   one of those three or shows what the app will make of them.

   THE PREVIEW PAINTS ITS OWN COLOURS AND READS NONE OF THE CONSOLE'S. Every custom property it
   uses is `--pv-`-prefixed and set inline on the card by partner-home-render.mjs, so a partner
   colour can never leak out into the console chrome and the console's violet can never leak in
   and flatter a preview. That prefix is also why the renderer rewrites `var(--x)` to
   `var(--pv-x)` inside any value it passes through: a derived token that referenced another
   token would otherwise resolve against OUR palette and draw a colour the app will not paint. */
.pt-brand-facts { margin: 0 0 var(--space-2); }
.pt-brand-ours {
  margin: 0 0 var(--space-5);
  font-size: var(--text-sm); line-height: var(--leading-snug); color: var(--text-muted);
}

/* The mark, beside what can be done to it. One column below 640px: the box is 200px wide and a
   file input beside it at 360px leaves neither anything to sit in. */
.pt-brand-mark { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); align-items: start; }
@media (min-width: 640px) {
  .pt-brand-mark { grid-template-columns: 200px minmax(0, 1fr); }
}
.pt-brand-markside { display: flex; flex-direction: column; gap: var(--space-3); align-items: flex-start; }

/* A FIXED BOX, WHICH IS THE HALF OF PLAN §6 C1 THIS STYLESHEET OWNS. An uploaded mark is
   arbitrary pixels: a 4000×40 strip, a 40×4000 column, a wide opaque rectangle. object-fit:
   contain inside a box of our own dimensions means the aspect ratio is the partner's business and
   the space it occupies is not. The chequerboard is a transparency check — a white-on-transparent
   wordmark is invisible against a white card and its owner has no other way to find out. */
.pt-brand-markbox {
  display: flex; align-items: center; justify-content: center;
  width: 200px; max-width: 100%; height: 96px; padding: var(--space-3);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background-color: var(--surface-subtle);
  background-image:
    linear-gradient(45deg, var(--surface-sunken) 25%, transparent 25%, transparent 75%, var(--surface-sunken) 75%),
    linear-gradient(45deg, var(--surface-sunken) 25%, transparent 25%, transparent 75%, var(--surface-sunken) 75%);
  background-size: 16px 16px; background-position: 0 0, 8px 8px;
}
.pt-brand-markbox img { max-width: 100%; max-height: 100%; object-fit: contain; }
.pt-brand-markbox .pt-brand-nomark {
  font-size: var(--text-sm); color: var(--text-muted); text-align: center;
}

/* THE SECOND SLOT PREVIEWS ON THE GROUND IT WILL BE SEEN ON, which is the whole reason the slot
   exists: a partner uploading a white wordmark cannot judge it against a light card. The colour is
   written out rather than taken from a token because this console has its own light and dark
   themes and the ground being previewed is the APP's dark header, not this page's background —
   a token would follow whichever theme the partner happens to be reading in and show them the
   wrong answer half the time. The chequerboard stays: transparency still has to be visible. */
.pt-brand-markbox.is-dark {
  background-color: #14171c;
  background-image:
    linear-gradient(45deg, #1c2027 25%, transparent 25%, transparent 75%, #1c2027 75%),
    linear-gradient(45deg, #1c2027 25%, transparent 25%, transparent 75%, #1c2027 75%);
  border-color: #2a2f38;
}
/* The sentence that introduces the second slot. A hint by weight, but it is separating two mark
   rows rather than labelling a field, so it gets the room a rule would otherwise have to. */
.pt-brand-darknote { margin: var(--space-5) 0 var(--space-4); max-width: 62ch; }

.pt-brand-markbox.is-dark .pt-brand-nomark,
.pt-brand-markbox.is-dark .pt-absent { color: #9aa3af; }

.pt-brand-colour { display: flex; gap: var(--space-2); align-items: center; }
.pt-brand-colour input[type="color"] {
  width: 48px; height: var(--control-h); padding: 2px; flex: none;
  border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface);
  cursor: pointer;
}
.pt-brand-colour input[type="text"] { width: 10ch; flex: none; font-family: var(--font-mono); }

/* Light and dark side by side from 720px. Stacked below it rather than dropped, because the one
   a partner most needs to see is whichever their own device is not showing them. */
.pt-brand-previews { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
@media (min-width: 720px) {
  .pt-brand-previews { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
.pt-brand-figure { margin: 0; min-width: 0; }
.pt-brand-figure figcaption {
  margin-bottom: var(--space-2);
  font-size: var(--text-2xs); font-weight: var(--weight-extrabold);
  text-transform: uppercase; letter-spacing: var(--tracking-caps); color: var(--text-subtle);
}

.pt-pv {
  overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-md);
  background: var(--pv-bg); color: var(--pv-text);
}
.pt-pv-bar {
  display: flex; align-items: center; gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--pv-surface); border-bottom: 1px solid var(--pv-border);
}
/* 32px tall, which is what the app draws, so what is measured here is what ships. */
.pt-pv-mark { display: flex; align-items: center; height: 32px; }
.pt-pv-mark img { max-height: 32px; max-width: 140px; object-fit: contain; }
.pt-pv-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--text-sm); font-weight: var(--weight-bold); color: var(--pv-text);
}
.pt-pv-body { padding: var(--space-4); display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-2); }
.pt-pv-h { margin: 0; font-size: var(--text-md); font-weight: var(--weight-extrabold); color: var(--pv-text); }
.pt-pv-p { margin: 0; font-size: var(--text-sm); line-height: var(--leading-snug); color: var(--pv-text-muted); }
.pt-pv-btn {
  display: inline-flex; align-items: center; height: var(--control-h-sm);
  padding: 0 var(--space-4); border-radius: var(--radius-sm);
  background: var(--pv-accent); color: var(--pv-on-accent);
  font-size: var(--text-sm); font-weight: var(--weight-semibold);
}
.pt-pv-link { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--pv-accent-text); text-decoration: underline; }

/* THE ATTRIBUTION, IN THE PREVIEW, OUTSIDE THE MARK BOX AND IN NORMAL FLOW. Same arrangement the
   app uses and for the same reason: no image can overlap a line it does not share a stacking
   context with, whatever aspect ratio it was uploaded at. Its colour is the derived muted text,
   which deriveBrand() computes rather than accepts — there is no field a partner can fill in that
   reaches it. */
.pt-pv-foot {
  margin: 0; padding: var(--space-2) var(--space-4);
  border-top: 1px solid var(--pv-border);
  font-size: var(--text-2xs); color: var(--pv-text-muted);
}

/* What the deriver changed, and what it could not fix. Empty on a colour that needed neither, and
   then it draws nothing at all rather than an empty box saying so. */
.pt-brand-report { margin: var(--space-4) 0 0; padding: 0; list-style: none; display: grid; gap: var(--space-2); }
.pt-brand-report:empty { display: none; }
.pt-brand-report li {
  padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm);
  font-size: var(--text-sm); line-height: var(--leading-snug);
  background: var(--surface-subtle); border: 1px solid var(--border-subtle); color: var(--text-muted);
}
.pt-brand-report li.is-warning {
  background: var(--warning-soft); border-color: var(--warning); color: var(--warning-text);
}
/* ── Motion ───────────────────────────────────────────────────────────────────────────────────
   Matches doc.css: nothing here animates position, but the token-driven transitions on buttons
   should still stand down for anyone who has asked. */
@media (prefers-reduced-motion: reduce) {
  .pt-btn { transition: none; }
  /* The drawer arrives and leaves without sliding. visibility keeps its delay on the way out so
     the rail is still not in the tab order the instant before it goes. */
  .pt-side { transition: visibility 0s linear .18s; }
  .pt-shell.is-nav-open .pt-side { transition: visibility 0s; }
}

/* ── Narrow screens, part two — THE RULES THAT LOSE ON SOURCE ORDER ────────────────────────────
   These four were written inside the `@media (max-width: 560px)` block up beside the app header,
   where they read as though they belong, and NOT ONE OF THEM HAD EVER APPLIED. A media query adds
   no specificity: `.pt-kv dd` inside a media block and `.pt-kv dd` in the base cascade tie at
   (0,1,1), so source order decides — and the base declarations for .pt-pane (1392), .pt-kv (1413,
   1419) and .pt-vault-when (1670) are all written further down this file than that block was. Every
   one of them lost, at every width, for as long as they existed. They are down here because that is
   the only place they can win, and moving them back beside their neighbours silently un-fixes this.

   It is the shape the clinician report's print palette records — a rule written at the specificity
   it reads at, positioned where it cannot take effect — and it fails the same way: the stylesheet
   says what it means, the comment explains why, and the browser does something else. Measured at
   360px through Chrome's own CSSOM rather than inferred: `.pt-pane` computed var(--space-5), the row
   kept `minmax(0, 1fr) auto`, and the value stayed right-aligned.

   THE COST WAS INVISIBLE UNTIL A VALUE WAS A SENTENCE. Every .pt-kv value on this screen used to be
   a date, a count or an amount, so an `auto` value column took a few characters and a dead rule
   changed nothing anybody could see. The referral pane prints "this deal is billed per seat, not per
   conversion" into a dd; at 360px that column took 264 pixels of the 360, the label beside it was
   given 10, and the word "Commission" — 70 pixels of text — was drawn 48 pixels across the top of
   the sentence explaining why a partner is owed nothing. */
@media (max-width: 560px) {
  .pt-pane { padding: var(--space-4); }

  /* Label above value rather than beside it. At 360px a right-aligned value shares its row with a
     label that has already wrapped twice, and the pair reads as two unrelated fragments. */
  .pt-kv > div { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .pt-kv dd { text-align: left; }

  .pt-vault-when { margin-left: 0; flex-basis: 100%; }
}


/* ─────────────────────────────────────────────────────────────────────────────────────────────
   GENERATED by scripts/lib/partner-enquiry-form.mjs — do not edit here.
   Four rules for the enquiry form. Everything else it needs is above, in the authored file.
   If partners-ui/partners.css ever defines .pt-request itself, delete this block in the same
   commit; two definitions of one class is how a panel quietly stops matching the page.
   ───────────────────────────────────────────────────────────────────────────────────────────── */
.pt-request {
  margin: var(--space-6) 0; padding: var(--space-6);
  background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-md);
}
.pt-request > h2 { margin-top: 0 !important; }
/* The honeypot. Off-screen rather than display:none — a bot that reads computed style skips a
   hidden field, and the whole point is that it should fill this one in. */
.pt-trap {
  position: absolute; width: 1px; height: 1px; margin: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.pt-request .pt-form-actions .btn { flex: none; }
