/* ============================================================================
   Portmasters Product Button — button.css  (Visual System v2, G1-1)
   ----------------------------------------------------------------------------
   Plain CSS port of the product Button CVA
   (packages/shared/src/components/button/style.ts, 413 lines). Consumes ONLY
   var() tokens from ../../tokens/tokens.css + tokens-semantic.css.

   Class model (mirrors the CVA axes):
     base      .pm-btn
     variant   .pm-btn--filled | --tonal | --regular | --outlined   (style.ts:18-23)
     color     .pm-btn--primary | --secondary | --neutral | --error
               | --success | --warning | --info | --white           (style.ts:24-33)
     size      .pm-btn--lg (44) | --md (36) | --sm (28)              (style.ts:34-38)
     icon-only .pm-btn--icon                                         (style.ts:39-42)
     loading   .pm-btn--loading  (+ <span class="pm-btn__label">, .pm-btn__spinner)
     disabled  [disabled], [aria-disabled="true"], .is-disabled      (style.ts:47-50 + plan#5)

   Frozen-state helpers for the matrix / screenshots (duplicate the pseudo
   rules so a static screenshot captures them):
     .is-hover  .is-active  .is-focus

   SCOPE (plan G0-4): product = 8px rectangles (--radius-button-product), NOT
   marketing pills/gradients/scale-hover. Dark mode is OUT OF SCOPE here.
   ============================================================================ */

@import url("../../tokens/tokens.css");
@import url("../../tokens/tokens-semantic.css");
@import url("../../tokens/tokens-type.css");

/* ----------------------------------------------------------------------------
   BASE  (style.ts:3-15 + plan #1 focus, #2 motion)
   rounded-lg 8px = --radius-button-product · font-semibold · text-3 (0.75rem)
   flex center · transition-colors 150ms swapped to --motion-standard (--ease)
   ---------------------------------------------------------------------------- */
.pm-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);              /* 8px = me-2/ms-2 icon margins (Button.tsx:67,69) — snapped to the scale */
  box-sizing: border-box;
  font-family: inherit;
  /* → control/button. WAS 13px/600/lh 1/no tracking. 13px is control/FIELD (the
     value inside an input), not the button label; the ramp gives buttons their
     own style at 12.5/700/-0.8%/120%. (2026-08-01 type fix) */
  font-size: var(--type-control-button-size);          /* 12.5px */
  font-weight: var(--type-control-button-weight);      /* 700 (was 600) */
  line-height: var(--type-control-button-leading);     /* 120% (was 1) */
  letter-spacing: var(--type-control-button-tracking); /* −0.8% (was none) */
  /* SINGLE-LINE LABEL — 2026-08-04, visual pass. A button's height is a FIXED
     --control-h (36px, founder ruling), so a label that wraps has nowhere to go:
     the box does not grow, overflow is visible, and the extra lines render OUTSIDE
     the pill. Measured in the stat-tile CTA at 900/1000/1100px — "Search Shipping
     Rate" became a 45px three-line block in a 36px box, so "Search" painted above
     the pill's top edge and "Rate" below it (9px of spill). It was there at 44px too,
     spilling 1px, which is why nobody saw it; flattening the height to 36 turned an
     invisible defect into a visible one.
     WHY NOWRAP AND NOT THE ALTERNATIVES. Letting the control GROW to fit two lines
     contradicts the ruling outright — a wrapped button is no longer a single-row
     control, and it is exactly the 44/45px shape the whole conversion removed.
     Truncating with an ellipsis was rejected because a CTA is the only affordance in
     its tile: "Search Shipping…" hides the action, and the label here is a bare text
     node, so text-overflow could not reach it without markup every consumer would
     have to adopt. Re-hosting the label in a taller non-control box is the same
     violation wearing a different element.
     Nowrap also FIXES the layout, it does not just clamp it: with the label
     unbreakable the button contributes its real min-content width, and the flex
     tiles that were squeezing it (.pm-stat-row__group, whose items are width:100%
     with the default min-width:auto) can no longer shrink a CTA below its own label
     — they take the space from the KPI tile beside it instead. The product renders
     this CTA on one line; this is the rule that makes that the only outcome. */
  white-space: nowrap;
  padding-block: 0;                 /* 2026-08-01: kills Chrome's UA `padding: 1px 6px` on <button>. That 1px was the S1-OFFSCALE "1px on .pm-btn" the audit found in 19 components — never authored, never on the 4px scale, and pure inheritance from the UA sheet. Height is owned by --control-h, so 0 is correct. */
  border: 0 solid transparent;      /* outlined overrides width to 1px */
  border-radius: var(--radius-button-product);   /* --r-sm 12px (moved 8→12, 2026-07-29) */
  cursor: pointer;
  text-decoration: none;            /* survives element="a" */
  -webkit-appearance: none;
  appearance: none;
  /* plan #2 — keep 150ms (100-250ms band), swap default easing for --ease */
  transition-property: color, background-color, border-color;
  transition-duration: var(--motion-duration, 150ms);
  transition-timing-function: var(--motion-standard);
  /* plan #1 — wire approved focus ring; invisible while unfocused */
  outline: none;
}

/* icon sizing parity with CVA [&>span>svg]:w-[20px] / per-size [&>svg] widths */
.pm-btn > span > svg { width: 20px; aspect-ratio: 1 / 1; }
.pm-btn > svg { aspect-ratio: 1 / 1; }

/* ----------------------------------------------------------------------------
   FOCUS RING  (plan #1) — focus-visible:ring-2 ring-offset-2 ring-[--focus-ring]
   Emulates Tailwind's offset ring: 2px background-coloured gap, then the 2px
   coloured ring. Uses --surface-1 (white) as the offset colour on light bg.
   .is-focus = frozen duplicate for static screenshots.

   CONTRAST NOTE (WCAG 1.4.11): the ring colour (--primary-500 #d342f0) does NOT
   reach 3:1 directly against the filled button fills (e.g. vs success-600 1.13:1,
   vs the primary-600 fill 1.34:1). It relies on the two-layer construction: the 2px
   --surface-1 (white) offset sits BETWEEN ring and fill, and on white/paper
   surfaces both boundaries the eye actually sees clear 3:1 — ring-vs-white is
   3.62:1, and (post-G2-1) white-vs-primary-fill is 4.85:1 (fill = --primary-600).
   Do NOT claim ring-vs-fill contrast.
   ---------------------------------------------------------------------------- */
.pm-btn:focus-visible,
.pm-btn.is-focus {
  outline: 2px solid transparent;   /* forced-colors visible ring — R1 component loop 2026-07-07 */
  outline-offset: 2px;
  box-shadow:
    0 0 0 2px var(--surface-1),
    0 0 0 4px var(--focus-ring);    /* --focus-ring = --primary-500 #d342f0 */
}

/* ----------------------------------------------------------------------------
   SIZE  (style.ts:34-38)
   2026-08-04 (FOUNDER REVERSAL): ALL THREE sizes are --control-h 36px. The lane
   below is kept because its ARGUMENT survives — only its number changed. 2026-08-01
   collapsed the 44/36/28 ladder onto one height after the audit measured 26 controls
   off the then-44 in 10 components — .pm-btn--sm
   at 28px was the dominant case (app-shell, button, map-panel, page-header,
   promo-banner). tokens.css:540-574 is explicit: "EVERY SINGLE-ROW CONTROL IS
   EXACTLY 36px. One height, everywhere, NO EXCEPTIONS." It is a founder ruling, not
   a rhythm preference. It is BELOW the WCAG 2.5.5 target and the founder was told
   that and chose it, so the old accessibility-floor argument here (BW-R3 "14 of 25
   interactive elements under 44px") is history and is not a reason to reopen this.

   2026-08-02 (control-heights lane) — the REFLOW the number-only fix left behind.
   Figma button 18:182 Primary/Default is ONE box: height 44 · radius 12 · padding
   0/12 · gap 8 (docs/2026-08-01-figma-vs-code-diff.md marks that row ✓). So --md and
   --sm both landing on 44 + 12px inline padding is not a collision, it IS the Figma
   button; --lg's 16px inline padding is the only tier that still differs, and the
   line that used to sit here ("the sizes keep their distinct INLINE padding — that is
   what still differentiates them") was simply false for md/sm. --sm survives as an
   ALIAS so no consumer breaks — the same alias-never-rename move segmented.css made
   with --h36 — and an alias must RENDER identically to what it aliases. It did not:
   the glyph and spinner ladder was INVERTED, the smallest tier drawing an 18px glyph
   against --md's 16px, a leftover from the 28px box where 18px was proportionate.
   map-panel.css:499 had already been forced to hand-override that 18px back to 16 to
   keep its card button's glyph optically matched to the label — evidence the 18 was
   wrong, not a deliberate step. Glyph and spinner now follow the box: md == sm.
   ---------------------------------------------------------------------------- */
.pm-btn--lg { height: var(--control-h); padding-inline: var(--space-4); }   /* 36px, px-4 = 16px */
.pm-btn--md { height: var(--control-h); padding-inline: var(--space-3); }   /* 36px, px-3 = 12px — the Figma button box, verbatim */
.pm-btn--sm { height: var(--control-h); padding-inline: var(--space-3); }   /* 36px (WAS 28px), px-3 = 12px — alias of --md */

/* GLYPH LADDER — FLAT, because the BOX ladder is flat (2026-08-04, visual pass).
   The height reversal collapsed lg/md/sm onto one 36px box but left the glyph ladder
   at 24/16/16, so --lg drew a 24px glyph with 6px of clearance while --md and --sm
   drew 16px with 10px — three boxes of identical size wearing three different glyph
   scales, and two icon-only buttons standing side by side read as two different
   components. A size axis that varies nothing but the glyph is not a size axis.
   16px is the number the other two rungs already hold and the one the system has
   twice been corrected TOWARD: --sm came down 18 → 16 when it became an alias of
   --md, and map-panel.css:499 had already been forced to hand-override an 18px glyph
   back to 16 to keep a card button optically matched to its label. 24 was
   proportionate to the retired 44px box, not to this one. What still varies by size
   is the INLINE padding (16 / 12 / 12), exactly as the size block above states. */
.pm-btn--lg > svg { width: 16px; }   /* WAS 24px — proportioned for the retired 44px box */
.pm-btn--md > svg { width: 16px; }
.pm-btn--sm > svg { width: 16px; }   /* WAS 18px — a LARGER glyph than --md on a now-identical 36px box */

/* ----------------------------------------------------------------------------
   ICON-ONLY  (style.ts:39-42, 390-404 + plan #8)
   p-1 aspect-square. 2026-08-02: the note that used to sit here still described the
   retired ladder ("md is 36x36 ... sm 36x36, lg 44x44"). Every icon button is
   --control-h square now — 36 x 36 since the 2026-08-04 founder reversal — because the
   size classes above all resolve to --control-h and `aspect-ratio: 1/1` squares the
   box off that height. The mins are the floor for a consumer that strips
   the height, not the thing that sets it.
   ---------------------------------------------------------------------------- */
.pm-btn--icon { padding: var(--space-1); aspect-ratio: 1 / 1; }   /* p-1 = 4px — snapped to --space ramp (M3, px-identical) */
.pm-btn--icon.pm-btn--lg { min-width: var(--control-h); min-height: var(--control-h); }  /* WAS a raw `44px` literal pair; putting it on the token is what let it follow the 2026-08-04 reversal down to 36px */
.pm-btn--icon.pm-btn--md { min-width: var(--control-h); min-height: var(--control-h); }  /* was 32px */
.pm-btn--icon.pm-btn--sm { min-width: var(--control-h); min-height: var(--control-h); }

/* ----------------------------------------------------------------------------
   LOADING  (plan #7) — keep children rendered but invisible so width never
   collapses; centre a size-matched spinner over them. Markup:
     <button class="pm-btn pm-btn--loading" aria-busy="true">
       <span class="pm-btn__label">Save</span>
       <span class="pm-btn__spinner" aria-hidden="true"></span>
     </button>
   ---------------------------------------------------------------------------- */
.pm-btn--loading { position: relative; }
.pm-btn--loading .pm-btn__label { opacity: 0; /* stays in a11y tree — visibility:hidden strips accname (WCAG 4.1.2, R2) */ }
.pm-btn__spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;          /* grid-free centring, no layout shift */
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: pm-btn-spin 0.6s linear infinite;
}
.pm-btn--lg .pm-btn__spinner { width: 16px; height: 16px; margin: -8px 0 0 -8px; }   /* WAS 24px/-12px — the spinner follows the glyph ladder, which flattened to 16 with the box (see the GLYPH LADDER note above) */
.pm-btn--md .pm-btn__spinner { width: 16px; height: 16px; margin: -8px 0 0 -8px; }
.pm-btn--sm .pm-btn__spinner { width: 16px; height: 16px; margin: -8px 0 0 -8px; }   /* WAS 18px/-9px — the same inversion as the glyph; --sm is an alias of --md and must spin at --md's size */
@keyframes pm-btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .pm-btn__spinner { animation-duration: 1.4s; }
}

/* ----------------------------------------------------------------------------
   DISABLED  (style.ts:47-50 + plan #5)
   cursor-not-allowed + pointer-events-none; survives element="a" via
   [aria-disabled="true"] since :disabled never matches on <a>.
   ---------------------------------------------------------------------------- */
.pm-btn:disabled,
.pm-btn[disabled],
.pm-btn[aria-disabled="true"],
.pm-btn.is-disabled {
  cursor: not-allowed;
  pointer-events: none;
  box-shadow: none;                 /* never show focus ring while disabled */
}

/* ============================================================================
   COMPOUND ENGINE  (variant x color)
   ----------------------------------------------------------------------------
   Every compound below only DECLARES per-state token slots; the shared rules
   here APPLY them. This keeps all 32 combos byte-for-byte consistent and makes
   the frozen-state helpers (.is-hover/.is-active) exact clones of the pseudo
   rules. Slots (all default to transparent/inherit so partial recipes work):
     --_fg   text            --_bg   background      --_bd   border colour
     --_fgH/_bgH  :hover      --_fgA/_bgA  :active
     --_fgD/_bgD/_bdD  disabled
   A compound sets whichever slots its style.ts recipe defines; unset states
   simply inherit the resting value (matches "no hover/active" recipes).
   ============================================================================ */
.pm-btn {
  /* resting */
  color: var(--_fg, inherit);
  background-color: var(--_bg, transparent);
  border-color: var(--_bd, transparent);
}

/* hover — live + frozen */
.pm-btn:hover,
.pm-btn.is-hover {
  color: var(--_fgH, var(--_fg, inherit));
  background-color: var(--_bgH, var(--_bg, transparent));
  border-color: var(--_bdH, var(--_bd, transparent));
}

/* active — live + frozen (wins over hover via source order) */
.pm-btn:active,
.pm-btn.is-active {
  color: var(--_fgA, var(--_fgH, var(--_fg, inherit)));
  background-color: var(--_bgA, var(--_bgH, var(--_bg, transparent)));
  border-color: var(--_bdA, var(--_bd, transparent));
}

/* disabled — live + frozen; wins over all above */
.pm-btn:disabled,
.pm-btn[disabled],
.pm-btn[aria-disabled="true"],
.pm-btn.is-disabled {
  color: var(--_fgD, var(--_fg, inherit));
  background-color: var(--_bgD, var(--_bg, transparent));
  border-color: var(--_bdD, var(--_bd, transparent));
}

/* ============================================================================
   VARIANT = FILLED   (style.ts:53-85,122-139,268-293,362-389 + plan #4)
   recipe: text-white bg-X-500/600 · hover X-600/700 · active X-700/800
   disabled: original product recipes kept per G1-1 verdict (Amir, 2026-07-03) — WCAG exempts disabled controls
   ============================================================================ */
.pm-btn--filled.pm-btn--primary {
  --_fg: var(--white);      --_bg: var(--primary-600);   /* G2-1(A) 2026-07-04: rest 500→600 → white label 4.85:1 AA (was 3.62:1) */
  --_bgH: var(--primary-700); --_bgA: var(--primary-700);   /* hover 600→700 (6.49:1); pressed keeps 700 */
  --_bgD: var(--primary-200);   /* style.ts:61 disabled:bg-primary-200; white text (style.ts:57) kept */
}
.pm-btn--filled.pm-btn--error {
  --_fg: var(--white);      --_bg: var(--error-600);
  --_bgH: var(--error-700);   --_bgA: var(--error-800);
  --_bgD: var(--error-300);     /* style.ts:276 disabled:bg-error-300; white text (style.ts:272) kept */
}
.pm-btn--filled.pm-btn--info {
  --_fg: var(--white);      --_bg: var(--info-600);
  --_bgH: var(--info-700);    --_bgA: var(--info-800);
  --_bgD: var(--info-300);      /* style.ts:289 disabled:bg-info-300; white text (style.ts:285) kept */
}
.pm-btn--filled.pm-btn--success {
  --_fg: var(--white);      --_bg: var(--success-700);   /* G2-1(B) R1 component loop 2026-07-07: rest 600→700 → white label 4.74:1 AA (was 3.19:1) */
  --_bgH: var(--success-800); --_bgA: var(--success-800);  /* hover 700→800 (6.78:1); pressed keeps 800 */
  --_bgD: var(--success-100); --_fgD: var(--success-500);   /* source recipe */
}
.pm-btn--filled.pm-btn--warning {
  --_fg: var(--white);      --_bg: var(--warning-700);   /* G2-1(B) R1 component loop 2026-07-07: rest 600→700 → white label 5.10:1 AA (was 3.26:1) */
  --_bgH: var(--warning-800); --_bgA: var(--warning-800);  /* hover 700→800 (7.13:1); pressed keeps 800 */
  --_bgD: var(--warning-100); --_fgD: var(--warning-500);   /* source recipe */
}
/* filled/secondary + filled/neutral share the neutral recipe (style.ts:122-139) */
.pm-btn--filled.pm-btn--secondary,
.pm-btn--filled.pm-btn--neutral {              /* plan #4: colourless combo filled/neutral */
  --_fg: var(--white);      --_bg: var(--neutral-600);   /* G2-1(B) R1 component loop 2026-07-07: rest 500→600 → white label 7.38:1 AA (was 4.35:1) */
  --_bgH: var(--neutral-700); --_bgA: var(--neutral-700);  /* hover 600→700 (11.02:1); pressed keeps 700 */
  --_bgD: var(--neutral-50);  --_fgD: var(--neutral-300);
}
/* filled/white (style.ts:66-85) — light chip: dark text on white, subtle border */
.pm-btn--filled.pm-btn--white {
  --_fg: var(--ink); --_bg: var(--white);   /* M1 pixel-gate fix 2026-07-08: the white chip is a FIXED light chip for dark surfaces — --_bg:--white never flips, so a theme-flipping label (--text-primary→#fff under [data-theme=dark]) rendered white-on-white (empty pill). Pin the label to --ink #16131c (theme-stable, = --text-primary's own light value) so it stays dark ink on the white chip in BOTH themes. Same class of role-that-must-stay-primitive as --text-on-color. */
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role */
  --_bd: var(--border);   /* M1: white-variant hairline → border role */
  border-width: 1px;          /* R1 component loop 2026-07-07: base sets border:0, so --_bd fed a 0-width
                                 border and the white-on-white chip rendered with no visible container;
                                 style.ts:66-85 source includes the Tailwind border class */
  --_bgD: transparent;        --_fgD: var(--neutral-300);
}

/* ============================================================================
   VARIANT = TONAL   (style.ts:86-157,294-329 + plan #4)
   recipe: text-X-700 bg-X-50 · hover 100 · active 200 · disabled bg-X-50 text-X-300
   (primary uses text-primary-500 / bg-primary-050 per source)
   ============================================================================ */
/* TONAL / PRIMARY — the label is INK, not magenta (founder ruling A, 2026-08-03;
   applied here 2026-08-04 after the visual pass caught the two live consumers,
   "Crop & Save" in image-crop-modal and "Search Shipping Rate" in stat-tile).
   The ruling: magenta is banned as small text in BOTH themes; controls and
   selections take ink and let the FILL keep carrying the state. That is exactly this
   recipe's shape — the magenta tint fill, the magenta hover/active deepening and the
   magenta focus ring all stay, and only the 12.5px word stops being magenta.
   WAS --primary-700 #9e1bb0 on the #fdf4ff tint (6.04:1 — an AA-legal magenta, which
   is why the earlier contrast-driven passes left it; A is a ban on accent-as-text,
   not a contrast patch). NOW --text-primary: 17.10:1 on the resting tint and 13.34:1
   on the --primary-200 active fill, both far clear of AA, and the ink flips with the
   theme so the dark repoint below needs no second stop.
   --_fgD (disabled) is NOT re-inked: disabled colours are veto-frozen (G1-1) in both
   themes and WCAG exempts them. It is the one magenta word left in this compound. */
.pm-btn--tonal.pm-btn--primary {
  --_fg: var(--text-primary); --_bg: var(--primary-050);   /* label → ink (ruling A). WAS --primary-700, set by G2-1(B) R2 2026-07-07 for 6.04:1 rest / 5.56 hover / 4.71 active on 050/100/200; source text-primary-500 kept only in docs */
  --_bgH: var(--primary-100); --_bgA: var(--primary-200);
  --_bgD: var(--primary-050); --_fgD: var(--primary-300);
}
.pm-btn--tonal.pm-btn--error {
  --_fg: var(--error-700);   --_bg: var(--error-50);
  --_bgH: var(--error-100);   --_bgA: var(--error-200);
  --_bgD: var(--error-50);    --_fgD: var(--error-300);
  --_fgA: var(--error-800); /* active AA (G2-1(B) R2) */
}
.pm-btn--tonal.pm-btn--info {
  --_fg: var(--info-700);    --_bg: var(--info-50);
  --_bgH: var(--info-100);    --_bgA: var(--info-200);
  --_bgD: var(--info-50);     --_fgD: var(--info-300);
}
.pm-btn--tonal.pm-btn--warning {
  --_fg: var(--warning-700); --_bg: var(--warning-50);
  --_bgH: var(--warning-100); --_bgA: var(--warning-200);
  --_bgD: var(--warning-50);  --_fgD: var(--warning-300);
  --_fgA: var(--warning-800); /* active AA (G2-1(B) R2) */
}
.pm-btn--tonal.pm-btn--success {                 /* plan #4: colourless combo tonal/success */
  --_fg: var(--success-700); --_bg: var(--success-50);
  --_bgH: var(--success-100); --_bgA: var(--success-200);
  --_bgD: var(--success-50);  --_fgD: var(--success-300);
  --_fgH: var(--success-800); --_fgA: var(--success-800); /* hover/active AA (G2-1(B) R2 2026-07-07) */
}
/* tonal/secondary + tonal/neutral share the neutral recipe (style.ts:104-121) */
.pm-btn--tonal.pm-btn--secondary,
.pm-btn--tonal.pm-btn--neutral {                 /* plan #4: colourless combo tonal/neutral */
  --_fg: var(--text-primary); --_bg: var(--surface-2);   /* M1: tonal/neutral label→primary text, resting fill→surface role */
  --_bgH: var(--surface-2); --_bgA: var(--surface-2);   /* M1: tonal/neutral hover/active wash → surface role */
  --_bgD: var(--neutral-50);  --_fgD: var(--neutral-300);
}
.pm-btn--tonal.pm-btn--white {                   /* plan #4: colourless combo tonal/white */
  --_fg: var(--ink); --_bg: var(--white);   /* M1 pixel-gate fix 2026-07-08: fixed light chip (--_bg:--white never flips); pin label to --ink so it stays dark on white in dark theme too (was --text-primary → white-on-white). See filled/white note above. */
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role (dark-flips) */
  --_bgD: var(--white);       --_fgD: var(--neutral-300);
}

/* --- TONAL colored variants: dark-theme repoint (M1 pixel-gate fix 2026-07-08) ---
   Root cause: the enabled tonal fill --_bg:var(--{color}-50) is a raw primitive
   from tokens.css with NO [data-theme="dark"] override, so on the dark canvas
   the 5 colored families rendered near-white pastel chips (verifier-proven:
   tonal-primary=(253,244,255), tonal-success=(234,255,246), tonal-info=(213,216,248)).
   Fix (same pattern as list.css / dashboard-tabs §2b, no invented hex): under dark,
   the resting fill flips to the surface role (--surface-2) and the label lightens
   one recipe step 700→400 — the Carbon "hover LIGHTENS on dark" rule that
   --accent-hover already uses (--primary-400); the light -700 label FAILED
   (2.03–3.82:1).

   RE-MEASURED 2026-08-01 (the two numbers above were both stale). The canvas is
   --deep #0f0715, not the retired #0b0410; and --surface-2 in dark resolves to
   --deep-6 #322639, not --deep-3 — that repoint landed in tokens-semantic.css:214
   on 2026-07-29, which moved this fill one plane lighter. The -400 ratios written
   here were measured against #1e1029 and survive only as the tray column:
       stop           tray #1e1029    THIS FILL #322639
       primary-400        6.70            5.29   AA
       error-400          6.64            5.24   AA
       warning-400       10.92            8.61   AA
       success-400       10.79            8.51   AA
       info-400           5.46            4.31   MISSES AA (4.5:1)
   info is the one row the lighter plane broke, the same way it broke --text-info
   (tokens-semantic.css:251-258 repointed that role to info-300 #96B0FA, 6.72 on
   #322639). This file still reads the raw --info-400 primitive, so it did not
   inherit that ruled fix. FLAGGED, NOT PATCHED — changing a shipped button ink is
   a founder-visible colour decision, not comment drift.
   tonal/secondary/neutral/white already
   flip correctly (routed to --surface-2/--ink in the earlier M1 sweep) and are untouched.
   --_bgD (disabled fill) stays veto-frozen per §5 (disabled colours frozen in both themes). */
[data-theme="dark"] .pm-btn--tonal.pm-btn--primary {
  /* label → ink here too (founder ruling A: the ban is on accent-as-text in BOTH
     themes, not a dark contrast patch). WAS --primary-400 #e26bff, which passed at
     5.29:1 on this fill and was still a magenta word. --text-primary is #ffffff in
     dark = 14.33:1 on --surface-2 #322639. The fill, the ring and the family keep
     carrying "primary"; only the 12.5px word changed. */
  --_fg: var(--text-primary); --_bg: var(--surface-2);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);
}
[data-theme="dark"] .pm-btn--tonal.pm-btn--error {
  --_fg: var(--error-400);   --_bg: var(--surface-2);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);
  --_fgA: var(--error-400);  /* override the light-theme active fg (--error-800) which fails on dark */
}
[data-theme="dark"] .pm-btn--tonal.pm-btn--info {
  --_fg: var(--info-400);    --_bg: var(--surface-2);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);
}
[data-theme="dark"] .pm-btn--tonal.pm-btn--warning {
  --_fg: var(--warning-400); --_bg: var(--surface-2);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);
  --_fgA: var(--warning-400);/* override the light-theme active fg (--warning-800) which fails on dark */
}
[data-theme="dark"] .pm-btn--tonal.pm-btn--success {
  --_fg: var(--success-400); --_bg: var(--surface-2);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);
  --_fgH: var(--success-400);/* override light-theme hover/active fg (--success-800) which fails on dark */
  --_fgA: var(--success-400);
}

/* ============================================================================
   VARIANT = REGULAR  (style.ts:158-189,252-267,330-361 + plan #4)
   recipe: text-X (transparent bg) · hover:bg-neutral-50 · active:bg-neutral-100
           disabled:bg-transparent disabled:text-X-300
   ============================================================================ */
.pm-btn--regular.pm-btn--primary {
  --_fg: var(--primary-600);  /* G2-1(B) R1 component loop 2026-07-07: fg 500→600 → 4.85:1 white / 4.60:1 paper AA (was 3.62/3.43) */
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role (dark-flips) */
  --_bgD: transparent;        --_fgD: var(--primary-300);
}
.pm-btn--regular.pm-btn--info {
  --_fg: var(--info-500);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role (dark-flips) */
  --_bgD: transparent;        --_fgD: var(--info-300);
}
.pm-btn--regular.pm-btn--error {
  --_fg: var(--error-700);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role (dark-flips) */
  --_bgD: transparent;        --_fgD: var(--error-300);
}
.pm-btn--regular.pm-btn--success {
  --_fg: var(--success-700);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role (dark-flips) */
  --_bgD: transparent;        --_fgD: var(--success-300);
  --_fgH: var(--success-800); /* hover AA (G2-1(B) R2) */
}
.pm-btn--regular.pm-btn--warning {               /* plan #4: colourless combo regular/warning */
  --_fg: var(--warning-700);
  --_bgH: var(--surface-2);  --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role (dark-flips) */
  --_bgD: transparent;        --_fgD: var(--warning-300);
}
/* regular/secondary — special recipe (style.ts:252-267): text hover, no bg hover */
.pm-btn--regular.pm-btn--secondary {
  --_fg: var(--text-secondary);   /* M1: secondary-button label → secondary text role */
  --_fgH: var(--text-primary); --_bgA: var(--surface-2);   /* M1: hover ink→primary text, active wash→surface */
  --_bgD: transparent;        --_fgD: var(--neutral-300);
}
/* regular/neutral — clone the secondary neutral recipe */
.pm-btn--regular.pm-btn--neutral {               /* plan #4: colourless combo regular/neutral */
  --_fg: var(--text-secondary);   /* M1: secondary-button label → secondary text role */
  --_fgH: var(--text-primary); --_bgA: var(--surface-2);   /* M1: hover ink→primary text, active wash→surface */
  --_bgD: transparent;        --_fgD: var(--neutral-300);
}
.pm-btn--regular.pm-btn--white {                 /* plan #4: colourless combo regular/white */
  --_fg: var(--white);
  --_bgH: var(--surface-2); --_bgA: var(--surface-2);   /* M1: regular/white hover/active wash → surface role */
  --_bgD: transparent;        --_fgD: var(--neutral-500);
}

/* ============================================================================
   VARIANT = OUTLINED  (style.ts:190-251 + plan #3 completes primary, #4 fills 4)
   border-width becomes 1px (CVA adds "border"); recipe: 1px border-X-200,
   hover:bg-X-50/100, active:bg-X-200, disabled bg-transparent text-X-300
   border-X-200. (primary keeps its resting bg-primary-050 + border-primary-500.)
   ============================================================================ */
.pm-btn--outlined { border-width: 1px; }

/* plan #3: complete outlined/primary using the established outlined recipe */
.pm-btn--outlined.pm-btn--primary {
  --_fg: var(--text-primary); --_bg: var(--primary-050); --_bd: var(--primary-500);   /* M1: outlined/primary label→primary text (brand fill+border KEEP) */
  --_bgH: var(--primary-100); --_bgA: var(--primary-200);
  --_bgD: transparent;        --_fgD: var(--primary-300); --_bdD: var(--primary-200);
}
/* QA-fix 2026-07-09: the opaque light --primary-050 fill stayed light in dark while
   --_fg (--text-primary) flipped to white → white-on-near-white, illegible. In dark
   the outlined/primary becomes a true outlined control: transparent fill, magenta
   border, subtle magenta wash on hover/active; the white label then reads. */
[data-theme="dark"] .pm-btn--outlined.pm-btn--primary {
  --_bg: transparent;
  --_bgH: rgba(var(--primary-500-rgb,211,66,240),.14);
  --_bgA: rgba(var(--primary-500-rgb,211,66,240),.22);
}
.pm-btn--outlined.pm-btn--error {
  --_fg: var(--error-700);   --_bd: var(--error-200);
  --_bgH: var(--error-50);    --_bgA: var(--error-200);
  --_bgD: transparent;        --_fgD: var(--error-300);   --_bdD: var(--error-200);
}
.pm-btn--outlined.pm-btn--success {
  --_fg: var(--success-700); --_bd: var(--success-200);
  --_bgH: var(--success-50);   --_bgA: var(--success-200);
  --_bgD: transparent;         --_fgD: var(--success-300); --_bdD: var(--success-200);
}
.pm-btn--outlined.pm-btn--warning {              /* plan #4: colourless combo outlined/warning */
  --_fg: var(--warning-700); --_bd: var(--warning-200);
  --_bgH: var(--warning-50);   --_bgA: var(--warning-200);
  --_bgD: transparent;         --_fgD: var(--warning-300); --_bdD: var(--warning-200);
}
.pm-btn--outlined.pm-btn--info {                 /* plan #4: colourless combo outlined/info */
  --_fg: var(--info-700);    --_bd: var(--info-200);
  --_bgH: var(--info-50);      --_bgA: var(--info-200);
  --_bgD: transparent;         --_fgD: var(--info-300);    --_bdD: var(--info-200);
}
/* outlined/secondary + outlined/neutral share the neutral recipe (style.ts:195-213) */
.pm-btn--outlined.pm-btn--secondary,
.pm-btn--outlined.pm-btn--neutral {              /* plan #4: colourless combo outlined/neutral */
  --_fg: var(--text-primary); --_bd: var(--border);   /* M1: neutral-variant label→primary text, hairline→border */
  --_bgH: var(--surface-2);   --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role */
  --_bgD: transparent;         --_fgD: var(--neutral-300); --_bdD: var(--neutral-100);
}
.pm-btn--outlined.pm-btn--white {                /* plan #4: colourless combo outlined/white */
  --_fg: var(--ink); --_bd: var(--border); --_bg: var(--white);   /* M1 pixel-gate fix 2026-07-08: opaque white chip (--_bg:--white never flips) → pin label to --ink so it stays dark on white in dark theme too (was --text-primary → white-on-white). See filled/white note above. Hairline stays --border. */
  --_bgH: var(--surface-2);   --_bgA: var(--surface-2);   /* M1: ghost hover/active wash → surface role */
  --_bgD: transparent;         --_fgD: var(--neutral-300); --_bdD: var(--neutral-100);
}

/* ----------------------------------------------------------------------------
   COLOR = ON-DARK  (added 2026-07-11, PromoBanner refine) — the TRUE transparent
   ghost for photo/dark surfaces. Distinct from --white (an OPAQUE white chip):
   the capture's "APPLY NOW"/"TRACK PACKAGE" affordance is a see-through hairline
   button that lets the image read through it — unreachable with --white. Only the
   `outlined` variant is defined (a ghost has no filled/tonal sibling; use
   filled/white for the solid on-dark chip). Label + hairline are white-family so
   they hold on any dark media; hover/active lay a low white-alpha wash (never an
   opaque fill) so the ghost stays a ghost. Token-alpha via color-mix, no raw rgba.
   The two-layer focus ring's offset colour is white here (the tile is dark), so the
   ring reads against the dark ground; see .pm-btn--on-dark:focus-visible below.
   Theme-stable by construction: white-on-dark holds in BOTH themes (a promo tile is
   dark in both), so no [data-theme="dark"] override is needed. --_fg white on a
   worst-case mid-grey photo still clears AA once the component scrim is present. */
.pm-btn--outlined.pm-btn--on-dark {
  --_fg: var(--white);
  --_bg: transparent;                                              /* TRUE ghost — image reads through */
  --_bd: color-mix(in srgb, var(--white) 55%, transparent);       /* legible hairline on any dark media */
  --_bgH: color-mix(in srgb, var(--white) 14%, transparent);      /* hover wash — stays translucent */
  --_bgA: color-mix(in srgb, var(--white) 22%, transparent);      /* active wash — a touch stronger */
  --_bdH: color-mix(in srgb, var(--white) 70%, transparent);      /* hairline firms up on hover */
  --_bdA: color-mix(in srgb, var(--white) 70%, transparent);
  --_bgD: transparent;         --_fgD: color-mix(in srgb, var(--white) 40%, transparent);
  --_bdD: color-mix(in srgb, var(--white) 22%, transparent);
}
/* On-dark ghost focus ring: swap the offset colour from --surface-1 (light) to a
   dark ground so the two-layer ring reads on the tile, not against white. */
.pm-btn--on-dark:focus-visible,
.pm-btn--on-dark.is-focus {
  box-shadow:
    0 0 0 2px var(--deep),
    0 0 0 4px var(--focus-ring);
}

/* ============================================================================
   RTL — icons use logical margins in the real component (me-2/ms-2). Nothing
   size/colour here is physical; gap + logical padding already flip correctly.
   ============================================================================ */

