/* 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); 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); }
.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); 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, quiet, and
   never mistaken for a filled field — see rule 7 in AGENTS.md. */
.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-muted);
}

/* 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); 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); }

/* 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. */
.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);
}
.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);
}
.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 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); }
/* 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 {
  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); }
.pt-field input:focus-visible, .pt-field textarea: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); 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);
}
.pt-app-head-in { display: flex; align-items: center; gap: var(--space-4); min-height: 62px; padding: var(--space-2) 0; 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); }
.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); }

.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. */
.pt-kv { display: grid; gap: var(--space-3); margin: 0; }
.pt-kv > 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-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; }

/* 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-2); margin: 0 0 var(--space-4); padding: 0; list-style: none; }
.pt-bars li { display: grid; grid-template-columns: minmax(0, 1fr) 64px 30px; gap: var(--space-2); 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; }

.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;
}

/* 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); }

/* 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 {
  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)); }
.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 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);
}

/* ── 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; }
}


/* ─────────────────────────────────────────────────────────────────────────────────────────────
   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; }
