/**
 * PulsarCards — Primitives
 * =========================
 * The shared component layer: reset, typography, buttons, fields, cards,
 * badges and the accessibility contract. Built entirely on the surface-role
 * tokens, so a primitive renders correctly on light, dark or branded
 * backgrounds without a single per-theme override.
 *
 * Load order is always: pulsar-tokens.css -> pulsar-primitives.css -> surface sheet.
 *
 * Applied UX principles, and where each one lives:
 *   Fitts        every control clears --tap-min
 *   Hick         .btn-primary is the one high-emphasis action per view
 *   von Restorff gold is reserved for the single most valuable action
 *   Miller       .stack / .cluster keep groups small and evenly spaced
 *   Doherty      feedback inside --dur-fast, so input never feels queued
 *   Jakob        controls look and behave the way users already expect
 */

/* ── Reset ─────────────────────────────────────────────────────────
   Scoped to what we actually need; not a full normalize. */
*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  /* Anchor targets clear the sticky header instead of hiding beneath it. */
  scroll-padding-top: 88px;
}

/* Smooth scrolling is a preference, not a default. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

body {
  margin: 0;
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--ui);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

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

img { height: auto; }

/* Product lockup. Ink variant is hidden until the admin dark theme. */
.brand-logo { width: auto; }
.brand-logo--ink { display: none; }
html[data-theme="dark"] .brand-logo--paper { display: none; }
html[data-theme="dark"] .brand-logo--ink { display: block; }

/* Inherit type into form controls rather than accepting UA defaults. */
button,
input,
select,
textarea { font: inherit; color: inherit; }

/* ── Typography ────────────────────────────────────────────────────*/
h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-family: var(--display);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
  /* Avoid a single trailing word on a line of its own. */
  text-wrap: balance;
}

h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }

/* Interface headings stay Inter: display serif is for page titles, not labels. */
h5, h6,
.heading-ui {
  font-family: var(--ui);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-normal);
}

p { margin: 0; }

.prose > * + * { margin-top: var(--space-4); }
.prose { max-width: var(--measure); }

.lead {
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
  max-width: var(--measure);
  text-wrap: pretty;
}

.eyebrow {
  display: block;
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--text-secondary);
}

.text-muted { color: var(--text-secondary); }
.text-sm    { font-size: var(--text-sm); }
.text-xs    { font-size: var(--text-xs); }

/* Tabular figures stop metric columns from shifting as values change. */
.numeric {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

a {
  color: var(--brand);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  transition: color var(--dur-fast) var(--ease-out);
}
a:hover { color: var(--brand-strong); }

/* ── Accessibility contract ───────────────────────────────────────*/

/* One visible focus style, everywhere, on every focusable thing. */
:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-offset);
  border-radius: var(--radius-xs);
}

/* Suppress the ring for pointer users only — never remove it outright. */
:focus:not(:focus-visible) { outline: none; }

/* Visually hidden, but still announced.
 *
 * Stay at the element's static position. Pinning left/top to 0 against a
 * distant positioned ancestor (portal-shell, a tall card) makes the
 * browser scroll that origin into view when the control is focused —
 * every radio tile, checkbox, or option then jumps the page to the top.
 * Wide tables already create a containing block via overflow-x: auto. */
.sr-only {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Keyboard users get the same escape hatch as a screen-reader user. */
.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: var(--z-toast);
  padding: var(--space-3) var(--space-4);
  background: var(--gold);
  color: var(--gold-ink);
  font-weight: var(--weight-semibold);
  border-radius: var(--radius-sm);
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease-out);
}
.skip-link:focus { transform: translateY(0); }

/* ── Layout helpers ───────────────────────────────────────────────*/
.container {
  width: 100%;
  max-width: var(--content-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container-wide { max-width: var(--content-wide); }

.section { padding-block: var(--section-y); }
.section-lg { padding-block: var(--section-y-lg); }

/* Owl selector: spacing belongs between siblings, not on them, which is
   what keeps first/last children from adding stray outer margin. */
.stack > * + * { margin-top: var(--stack-gap, var(--space-4)); }
.stack-sm { --stack-gap: var(--space-2); }
.stack-lg { --stack-gap: var(--space-6); }

.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cluster-gap, var(--space-3));
}
.cluster-sm { --cluster-gap: var(--space-2); }
.cluster-center { justify-content: center; }

/* Top spacing as an attribute rather than a `style="margin-top: …"`.
   This site's CSP allows styles only from 'self' plus a per-request nonce,
   and a nonce cannot apply to a style *attribute*, so every inline margin in
   the templates was being discarded by the browser — the spacing had never
   rendered. Values match the 8px scale; nothing new is invented. */
[data-space-top="4"] { margin-top: var(--space-4); }
[data-space-top="5"] { margin-top: var(--space-5); }
[data-space-top="6"] { margin-top: var(--space-6); }
[data-space-top="7"] { margin-top: var(--space-7); }
[data-space-block="8"] { margin-block: var(--space-8); }

.split {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* Auto-fit grid: the column count follows available width, so no
   breakpoint list is needed for card decks. */
.grid-auto {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(var(--col-min, 260px), 1fr));
}

/* ── Buttons ──────────────────────────────────────────────────────
   One base, five emphasis levels:

     .btn-primary    brand blue — the standard primary action
     .btn-gold       gold — the single most valuable action on a page
     .btn-secondary  outlined — everything else
     .btn-ghost      bare — low-emphasis, inline
     .btn-danger     outlined red, filling on hover

   Gold is rationed on purpose. The moment two gold buttons share a screen,
   neither one reads as the next step (von Restorff), so an ordinary Save
   must not wear the same colour as "Share my card". */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--text-primary);
  --btn-border: transparent;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-5);
  border: var(--border-hair) solid var(--btn-border);
  border-radius: var(--radius-full);
  background: var(--btn-bg);
  color: var(--btn-fg);
  font-size: var(--text-sm);
  font-weight: 720;
  line-height: 1;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-out),
    border-color     var(--dur-fast) var(--ease-out),
    color            var(--dur-fast) var(--ease-out),
    transform        var(--dur-fast) var(--ease-out),
    box-shadow       var(--dur-fast) var(--ease-out);
}

/* A 1px lift on hover and a settle on press: enough proprioceptive
   feedback to confirm the hit without animating layout. */
.btn:hover  { transform: translateY(-1px); }
.btn:active { transform: translateY(0); transition-duration: var(--dur-instant); }

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: var(--disabled-opacity);
  cursor: not-allowed;
  transform: none;
}

.btn-primary {
  --btn-bg: var(--brand);
  --btn-fg: #fff;
  --btn-border: var(--brand);
  box-shadow: var(--shadow-xs);
}
.btn-primary:hover {
  --btn-bg: var(--brand-strong);
  --btn-border: var(--brand-strong);
  box-shadow: var(--shadow-sm);
}

/* Reserved for the one action on a page that is worth the most. */
.btn-gold {
  --btn-bg: var(--gold);
  --btn-fg: var(--gold-ink);
  --btn-border: var(--gold);
  box-shadow: var(--shadow-sm);
}
.btn-gold:hover {
  --btn-bg: var(--gold-strong);
  --btn-border: var(--gold-strong);
  box-shadow: var(--shadow-gold);
}

.btn-secondary {
  --btn-bg: var(--surface-raised);
  --btn-fg: var(--text-primary);
  --btn-border: var(--surface-line);
}
.btn-secondary:hover { --btn-border: var(--gold); --btn-fg: var(--gold-strong); }

.btn-ghost { --btn-fg: var(--text-primary); }
.btn-ghost:hover { --btn-bg: var(--gold-wash); --btn-fg: var(--gold-strong); }

.btn-danger {
  --btn-bg: transparent;
  --btn-fg: var(--danger);
  --btn-border: var(--danger);
}
.btn-danger:hover { --btn-bg: var(--danger); --btn-fg: #fff; }

.btn-block { width: 100%; }
.btn-lg { min-height: 52px; padding-inline: var(--space-6); font-size: var(--text-base); }

/* Pointer-only compaction for dense admin tables. Touch keeps 44px. */
@media (hover: hover) and (pointer: fine) {
  .btn-sm { min-height: var(--tap-min-dense); padding: var(--space-2) var(--space-3); font-size: var(--text-xs); }
}
.btn-sm { min-height: var(--tap-min); padding: var(--space-2) var(--space-3); font-size: var(--text-xs); }

/* Icon-only button. Always needs an aria-label or an .sr-only child. */
.btn-icon {
  min-width: var(--tap-min);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
}

/* ── Fields ───────────────────────────────────────────────────────*/
.field { display: grid; gap: var(--space-2); }

.label {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}

.label-hint {
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  color: var(--text-secondary);
}

.input,
.textarea,
.select {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-3);
  border: var(--border-hair) solid var(--surface-line);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--text-primary);
  font-size: var(--text-sm);
  transition:
    border-color var(--dur-fast) var(--ease-out),
    box-shadow   var(--dur-fast) var(--ease-out);
}

.input::placeholder,
.textarea::placeholder { color: var(--text-secondary); opacity: 0.75; }

.input:hover,
.textarea:hover,
.select:hover { border-color: var(--text-secondary); }

/* Focus reads as a ring on the field itself; the global outline would
   double up on an already-bordered box. */
.input:focus-visible,
.textarea:focus-visible,
.select:focus-visible {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-wash);
}

.textarea { min-height: 120px; resize: vertical; line-height: var(--leading-normal); }

.select {
  /* Custom chevron; appearance:none also removes the OS focus ring, which
     is why the rule above restores one. */
  appearance: none;
  padding-right: var(--space-7);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5l5-5' stroke='%236B6458' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
}

/* Validity is announced by colour *and* copy in .field-error, never colour alone. */
.input[aria-invalid="true"],
.textarea[aria-invalid="true"] { border-color: var(--danger); }

.field-error {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--danger);
}

.field-help { font-size: var(--text-xs); color: var(--text-secondary); }

/* ── Surfaces ─────────────────────────────────────────────────────*/
.card {
  padding: var(--space-5);
  border: var(--border-hair) solid var(--surface-line);
  border-radius: var(--radius-lg);
  background: var(--surface-raised);
  box-shadow: var(--shadow-xs);
}

.card-flush { padding: 0; overflow: hidden; }

.card-interactive {
  transition:
    transform  var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out);
}
.card-interactive:hover {
  transform: translateY(-2px);
  border-color: var(--gold-soft);
  box-shadow: var(--shadow-md);
}

.card-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}
.card-title {
  font-family: var(--ui);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-normal);
}

/* ── Badges and pills ─────────────────────────────────────────────*/
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  border-radius: var(--radius-full);
  background: var(--surface-sunken);
  color: var(--text-secondary);
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

.badge-gold    { background: var(--gold-wash);    color: var(--gold-strong); }
.badge-brand   { background: var(--brand-wash);   color: var(--brand); }
.badge-success { background: var(--success-wash); color: var(--success); }
.badge-warning { background: var(--warning-wash); color: var(--warning); }
.badge-danger  { background: var(--danger-wash);  color: var(--danger); }

/* ── Icons ────────────────────────────────────────────────────────
   Inline SVG from sql/icons.php. currentColor means an icon always
   matches the text beside it, with no per-context colour rules. */
.icon {
  display: inline-block;
  width: 1em;
  height: 1em;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -0.125em;
}
.icon-filled { fill: currentColor; stroke: none; }
.icon-sm { width: var(--icon-sm); height: var(--icon-sm); }
.icon-lg { width: var(--icon-lg); height: var(--icon-lg); }
.icon-xl { width: var(--icon-xl); height: var(--icon-xl); }

/* ── Feedback ─────────────────────────────────────────────────────*/
.flash {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: var(--border-hair) solid transparent;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}
.flash-success { background: var(--success-wash); border-color: var(--success); color: var(--success); }
.flash-danger  { background: var(--danger-wash);  border-color: var(--danger);  color: var(--danger); }
.flash-warning { background: var(--warning-wash); border-color: var(--warning); color: var(--warning); }
.flash-info    { background: var(--brand-wash);   border-color: var(--brand);   color: var(--brand); }

/* Legacy aliases kept so existing markup keeps rendering. */
.flash-ok  { background: var(--success-wash); border-color: var(--success); color: var(--success); }
.flash-bad { background: var(--danger-wash);  border-color: var(--danger);  color: var(--danger); }

.offline-banner {
  position: sticky;
  top: 0;
  z-index: 40;
  padding: var(--space-2) var(--space-4);
  background: var(--warning-wash);
  border-bottom: var(--border-hair) solid var(--warning);
  color: var(--ink, #161314);
  font-size: var(--text-sm);
  text-align: center;
}

/* ── Empty state ──────────────────────────────────────────────────
   An empty screen is a teaching moment: name the thing, say why it is
   empty, then offer the one action that fills it. */
.empty {
  display: grid;
  justify-items: center;
  gap: var(--space-3);
  padding: var(--space-7) var(--space-5);
  text-align: center;
  color: var(--text-secondary);
}
.empty-icon {
  display: grid;
  place-items: center;
  width: 56px; height: 56px;
  border-radius: var(--radius-full);
  background: var(--gold-wash);
  color: var(--gold-strong);
}
.empty-title {
  font-family: var(--ui);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}
.empty p { max-width: var(--measure-tight); font-size: var(--text-sm); }

/* ── Progress ─────────────────────────────────────────────────────
   Zeigarnik / goal-gradient: an unfinished bar is a standing invitation,
   and it pulls harder the closer it gets to full. */
.progress {
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--surface-sunken);
  overflow: hidden;
}
.progress-bar {
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--gold-strong), var(--gold));
  transition: width var(--dur-slow) var(--ease-out);
}

/* ── Divider ──────────────────────────────────────────────────────*/
.divider {
  height: var(--border-hair);
  border: 0;
  margin: var(--space-5) 0;
  background: var(--surface-line);
}

/* ── Tables ───────────────────────────────────────────────────────*/
.table-wrap {
  overflow-x: auto;
  /* Momentum scrolling on iOS, and a keyboard-reachable scroll region. */
  -webkit-overflow-scrolling: touch;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}
.table th,
.table td {
  padding: var(--space-3);
  text-align: left;
  border-bottom: var(--border-hair) solid var(--surface-line);
}
.table th {
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-secondary);
  white-space: nowrap;
}
.table tbody tr { transition: background-color var(--dur-fast) var(--ease-out); }
.table tbody tr:hover { background: var(--gold-wash); }
.table td:last-child,
.table th:last-child { text-align: right; }

/* ── Entrance animation ───────────────────────────────────────────
   Opt-in via .reveal; JS adds .is-visible on intersection. Transform and
   opacity only, so the compositor does the work and layout never reflows. */
.reveal {
  opacity: 0;
  transform: translateY(14px) scale(0.99);
  transition:
    opacity   var(--dur-slower) var(--ease-out),
    transform var(--dur-slower) var(--ease-out);
  will-change: opacity, transform;
}
.reveal.is-visible {
  opacity: 1;
  transform: none;
  will-change: auto;
}

/* Small stagger so a group reads as one gesture rather than a stutter. */
.reveal-stagger > * { transition-delay: calc(var(--i, 0) * 70ms); }

/* Without JS the observer never runs, so hidden content would be lost.
   `no-js` is on <html> from markup and removed by an inline script in the
   head, which means the visible-by-default state only survives when
   scripting is genuinely unavailable. A blanket `body.marketing .reveal`
   override was doing this unconditionally and silently disabled every
   scroll reveal on the marketing site. */
html.no-js .reveal {
  opacity: 1;
  transform: none;
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
  .btn:hover, .card-interactive:hover { transform: none; }
}

/* Signed-in Ultra portal: workspace copy must never wait on a reveal observer. */
html.portal-ultra .reveal,
html.portal-ultra .reveal.is-visible {
  opacity: 1;
  transform: none;
  transition: none;
  will-change: auto;
}

/* ── Native captcha + honeypot (login, cards, marketing contact) ─*/
.captcha-box {
  margin: 0;
  padding: var(--space-3) 0 0;
  border: 0;
  min-width: 0;
}
.captcha-legend {
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.captcha-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.captcha-question {
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.captcha-input {
  width: 4.5rem;
  min-height: var(--tap-min);
  padding: var(--space-2);
  border: var(--border-hair) solid var(--surface-line);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--text-primary);
  font: inherit;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.captcha-input:focus-visible {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-wash);
}
.captcha-image {
  display: block;
  height: 40px;
  width: auto;
  border-radius: var(--radius-sm);
  border: var(--border-hair) solid var(--surface-line);
}
.hp-field {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Print ────────────────────────────────────────────────────────*/
@media print {
  .btn, .skip-link { display: none !important; }
  body { background: #fff; color: #000; }
  .card { border: 1px solid #ccc; box-shadow: none; }
}
