/* ==========================================================================
   PKOUG Design System — Components
   --------------------------------------------------------------------------
   Requires pkoug-tokens.css to be loaded first.

   Every class is namespaced `pk-` so this file cannot collide with the
   the legacy style.css / home.css / pages.css rules while both were in play.
   Those three are gone now; nothing here needs to defer to them any more
   during the phased rollout.

   Contents
     1.  Base & reset
     2.  Layout
     3.  Typography
     4.  Links & focus
     5.  Buttons
     6.  Forms
     7.  Cards
     8.  Alerts
     9.  Badges & status pills
     10. Tables
     11. Modal dialog
     12. Site header & navigation
     13. Breadcrumbs
     14. Pagination
     15. Empty states
     16. Loading states
     17. Avatars
     18. Member / profile cards
     19. Event cards
     20. Sponsor cards
     21. Footer
     22. Utilities
   ========================================================================== */

/* ==========================================================================
   1. BASE & RESET
   ========================================================================== */

*,
*::before,
*::after { box-sizing: border-box; }

.pk-scope,
body.pk-body {
  margin: 0;
  background: var(--pk-bg);
  color: var(--pk-ink);
  font-family: var(--pk-font-sans);
  font-size: var(--pk-text-base);
  line-height: var(--pk-leading-normal);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Give in-page anchors room to clear the sticky header. */
html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}
:target { scroll-margin-top: 6rem; }

img,
svg,
video { max-width: 100%; height: auto; }

/* Skip link — offscreen until focused.
   Positioned with `top` rather than a transform: a transformed element can
   still be scrolled to and, more importantly, the offset is animated, which
   left it stranded offscreen on focus in testing. Changing `top` is the
   long-established pattern and behaves identically in every browser. */
.pk-skip-link {
  position: fixed;
  top: -100px;
  left: var(--pk-space-4);
  z-index: var(--pk-z-toast);
  background: var(--pk-surface);
  color: var(--pk-link);
  border: 2px solid var(--pk-focus-ring);
  border-radius: var(--pk-radius-md);
  padding: var(--pk-space-3) var(--pk-space-4);
  font-weight: var(--pk-weight-semibold);
  text-decoration: none;
  box-shadow: var(--pk-shadow-lg);
}
.pk-skip-link:focus,
.pk-skip-link:focus-visible { top: var(--pk-space-4); }

/* Screen-reader-only text that is still focusable when needed. */
.pk-visually-hidden {
  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;
}

/* ==========================================================================
   2. LAYOUT
   ========================================================================== */

.pk-container {
  width: 100%;
  max-width: var(--pk-container);
  margin-inline: auto;
  padding-inline: var(--pk-container-pad);
}
.pk-container--narrow { max-width: 780px; }
.pk-container--wide   { max-width: 1400px; }

.pk-section { padding-block: var(--pk-section-y); }
.pk-section--tight { padding-block: var(--pk-space-10); }
.pk-section--sunk  { background: var(--pk-surface-sunk); }

.pk-stack > * + * { margin-top: var(--pk-space-4); }
.pk-stack--sm > * + * { margin-top: var(--pk-space-2); }
.pk-stack--lg > * + * { margin-top: var(--pk-space-8); }

.pk-cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pk-space-3);
  align-items: center;
}

/* Auto-fitting grid: the min column width is the only thing to tune. */
.pk-grid {
  display: grid;
  gap: var(--pk-space-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
}
.pk-grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); }
/* --3 was used by events.php, index.php and admin/event_edit.php without ever
   being defined, so all three silently fell back to the base 280px auto-fit.
   That happened to look right at desktop width and wrong between 840 and
   1120px, where the base rule fits four tracks and the markup expects three. */
.pk-grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.pk-grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); }

/* ==========================================================================
   3. TYPOGRAPHY
   ========================================================================== */

.pk-h1, .pk-h2, .pk-h3, .pk-h4 {
  margin: 0;
  color: var(--pk-ink);
  font-weight: var(--pk-weight-heavy);
  line-height: var(--pk-leading-tight);
  letter-spacing: var(--pk-tracking-tight);
  text-wrap: balance;
}
.pk-h1 { font-size: var(--pk-text-4xl); }
.pk-h2 { font-size: var(--pk-text-3xl); }
.pk-h3 { font-size: var(--pk-text-2xl); font-weight: var(--pk-weight-bold); }
.pk-h4 { font-size: var(--pk-text-xl);  font-weight: var(--pk-weight-bold); }

.pk-display {
  margin: 0;
  font-size: var(--pk-text-5xl);
  font-weight: var(--pk-weight-heavy);
  line-height: var(--pk-leading-tight);
  letter-spacing: var(--pk-tracking-tight);
  text-wrap: balance;
}

.pk-eyebrow {
  display: inline-block;
  margin: 0 0 var(--pk-space-3);
  color: var(--pk-link);
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-bold);
  letter-spacing: var(--pk-tracking-wide);
  text-transform: uppercase;
}

/* The place name in an eyebrow is a brand mark, and takes the same green as
   the highlighted `Pakistan` in the hero heading below it. Recolouring one
   word inside an otherwise uniform red line reads as a mistake, so the
   separators are muted as well: that turns the line into three distinct
   tokens, and the green one is plainly the first token rather than a word
   that lost its colour. `--pk-ink-muted`, not `--pk-ink-faint` — these are
   real characters in the text, not decoration. */
.pk-eyebrow__mark { color: var(--pk-brand-green-ink); }
.pk-eyebrow__sep  { color: var(--pk-ink-muted); font-weight: var(--pk-weight-semibold); }

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

.pk-prose { max-width: var(--pk-measure); color: var(--pk-ink-soft); }
.pk-prose > * + * { margin-top: var(--pk-space-4); }
.pk-prose p { margin: 0; text-wrap: pretty; }

.pk-muted { color: var(--pk-ink-muted); }
.pk-faint { color: var(--pk-ink-faint); font-size: var(--pk-text-sm); }

/* Brand wordmark: "Pk" green, "OUG" red. */
.pk-wordmark { font-weight: var(--pk-weight-heavy); letter-spacing: var(--pk-tracking-tight); }
.pk-wordmark .pk-wordmark__pk  { color: var(--pk-brand-green); }
.pk-wordmark .pk-wordmark__oug { color: var(--pk-brand-red-ink); }

/* ==========================================================================
   4. LINKS & FOCUS
   ========================================================================== */

/* The blanket link colour must not capture anchors that are styled as
   buttons, chips or nav items — those carry their own colour, and a bare
   `a` selector here outranks the single-class `.pk-btn` rule. */
.pk-scope a:not([class*="pk-btn"]):not([class*="pk-chip"]):not([class*="pk-nav__"]):not([class*="pk-pagination__"]),
.pk-body a:not([class*="pk-btn"]):not([class*="pk-chip"]):not([class*="pk-nav__"]):not([class*="pk-pagination__"]) {
  color: var(--pk-link);
  text-underline-offset: 0.2em;
}
.pk-scope a:not([class*="pk-btn"]):not([class*="pk-chip"]):not([class*="pk-nav__"]):not([class*="pk-pagination__"]):hover,
.pk-body a:not([class*="pk-btn"]):not([class*="pk-chip"]):not([class*="pk-nav__"]):not([class*="pk-pagination__"]):hover {
  color: var(--pk-link-hover);
}

/* One focus treatment for everything interactive. Uses :focus-visible so a
   mouse click does not leave a ring behind, but keyboard focus always does.
   Never paired with `outline: none` and no replacement. */
.pk-scope :focus-visible,
.pk-body :focus-visible,
.pk-btn:focus-visible,
.pk-input:focus-visible,
.pk-select:focus-visible,
.pk-textarea:focus-visible {
  outline: var(--pk-focus-width) solid var(--pk-focus-ring);
  outline-offset: var(--pk-focus-offset);
  border-radius: var(--pk-radius-sm);
}

/* ==========================================================================
   5. BUTTONS
   ========================================================================== */

.pk-btn {
  --_bg: transparent;
  --_fg: var(--pk-ink);
  --_bd: var(--pk-border-strong);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--pk-space-2);
  /* min-height 44px keeps every button above the WCAG 2.2 target size. */
  min-height: 44px;
  padding: var(--pk-space-3) var(--pk-space-5);
  border: 1px solid var(--_bd);
  border-radius: var(--pk-radius-md);
  background: var(--_bg);
  color: var(--_fg);
  font-family: inherit;
  font-size: var(--pk-text-base);
  font-weight: var(--pk-weight-semibold);
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--pk-dur) var(--pk-ease),
              border-color var(--pk-dur) var(--pk-ease),
              color var(--pk-dur) var(--pk-ease),
              transform var(--pk-dur-fast) var(--pk-ease);
}
.pk-btn:hover { transform: translateY(-1px); }
.pk-btn:active { transform: translateY(0); }
.pk-btn[disabled],
.pk-btn[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
  transform: none;
}

/* Primary — brand-red fill, white label. Verified 4.54:1 in both themes. */
.pk-btn--primary {
  --_bg: var(--pk-brand-red);
  --_fg: var(--pk-on-brand);
  --_bd: var(--pk-brand-red);
}
.pk-btn--primary:hover:not([disabled]) {
  --_bg: var(--pk-brand-red-hover);
  --_bd: var(--pk-brand-red-hover);
  color: var(--pk-on-brand);
}

.pk-btn--secondary {
  --_bg: var(--pk-surface);
  --_fg: var(--pk-ink);
  --_bd: var(--pk-border-strong);
}
.pk-btn--secondary:hover:not([disabled]) {
  --_bd: var(--pk-brand-red-ink);
  --_fg: var(--pk-link);
}

.pk-btn--ghost { --_bg: transparent; --_fg: var(--pk-link); --_bd: transparent; }
.pk-btn--ghost:hover:not([disabled]) { --_bg: var(--pk-brand-red-tint); }

.pk-btn--danger {
  --_bg: var(--pk-danger);
  --_fg: #FFFFFF;
  --_bd: var(--pk-danger);
}

.pk-btn--sm { min-height: 36px; padding: var(--pk-space-2) var(--pk-space-3); font-size: var(--pk-text-sm); }
.pk-btn--lg { min-height: 52px; padding: var(--pk-space-4) var(--pk-space-8); font-size: var(--pk-text-lg); }
.pk-btn--block { display: flex; width: 100%; }

/* Icon-only button — keeps a 44px hit area regardless of glyph size. */
.pk-btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid var(--pk-border-strong);
  border-radius: var(--pk-radius-md);
  background: var(--pk-surface);
  color: var(--pk-ink-soft);
  cursor: pointer;
  transition: background-color var(--pk-dur) var(--pk-ease),
              color var(--pk-dur) var(--pk-ease);
}
.pk-btn-icon:hover { background: var(--pk-surface-sunk); color: var(--pk-link); }

/* Busy state, toggled by pkoug.js */
.pk-btn[data-busy="true"] { pointer-events: none; opacity: 0.7; }
.pk-btn[data-busy="true"]::before {
  content: "";
  width: 1em; height: 1em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: pk-spin 0.6s linear infinite;
}

/* ==========================================================================
   6. FORMS
   ========================================================================== */

.pk-field { display: block; margin-bottom: var(--pk-space-5); }

.pk-label {
  display: block;
  margin-bottom: var(--pk-space-2);
  color: var(--pk-ink);
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-semibold);
}

/* Required marker carries text for screen readers, not just an asterisk. */
.pk-required { color: var(--pk-danger); font-weight: var(--pk-weight-bold); }

.pk-hint {
  margin: var(--pk-space-2) 0 0;
  color: var(--pk-ink-muted);
  font-size: var(--pk-text-sm);
}

/*
 * F3. WebKit sizes a <select>'s scrollable content from its longest OPTION,
 * not from its rendered box. On admin/admins.php at 360px the Role select
 * rendered at 278px — comfortably inside its container — while still pushing
 * document scrollWidth out by 63px, so the PAGE scrolled sideways in WebKit
 * and Safari while Chromium was clean.
 *
 * width:100% and min-width:0 do not help, because the box was never the
 * problem. Clipping the select's own overflow is what stops it contributing;
 * the rendered width is identical either way, so nothing moves visually and
 * the control still opens its full-width popup natively.
 *
 * THIS RULE IS FOR .pk-select ALONE. `.pk-input` was once part of its selector
 * list — a stray `.pk-input,` sat above this comment — which silently took
 * every text input on the site out of the base rule below and left it with
 * nothing but `overflow: hidden`. See the note on that rule.
 */
.pk-select {
  overflow: hidden;
  text-overflow: ellipsis;
}

/*
 * Every text control on the site. `.pk-input` MUST stay in this list.
 *
 * It was missing from it between f93336c and this commit, and the damage was
 * total rather than cosmetic: inputs fell back to the user-agent stylesheet at
 * 21px tall with a 2px inset border and a 13.3px font. That silently undid two
 * deliberate decisions recorded right here — the 44px minimum target size, and
 * the 16px font floor that stops iOS Safari zooming the page on focus. The
 * admin sign-in, the ACE guidance form and every search box were affected.
 *
 * Nothing caught it: the suites assert markup, and Q8 asserted labels,
 * contrast and focus rings — none of them measured a control's geometry. Q8
 * now does.
 */
.pk-input,
.pk-select,
.pk-textarea {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: var(--pk-space-3) var(--pk-space-4);
  border: 1px solid var(--pk-border-strong);
  border-radius: var(--pk-radius-md);
  background: var(--pk-surface);
  color: var(--pk-ink);
  font-family: inherit;
  /* 16px minimum prevents iOS Safari from zooming on focus. */
  font-size: var(--pk-text-base);
  line-height: 1.4;
  transition: border-color var(--pk-dur) var(--pk-ease),
              box-shadow var(--pk-dur) var(--pk-ease);
}

/* Reset the UA appearance so `type="search"` and `type="date"` cannot bring
   their own box back. The search cancel button goes with it — it is
   inconsistent between engines and the field already sits beside a Search
   button. */
.pk-input {
  -webkit-appearance: none;
  appearance: none;
}
.pk-input::-webkit-search-cancel-button,
.pk-input::-webkit-search-decoration { -webkit-appearance: none; }
.pk-input::placeholder,
.pk-textarea::placeholder { color: var(--pk-ink-faint); }

.pk-input:hover,
.pk-select:hover,
.pk-textarea:hover { border-color: var(--pk-ink-faint); }

.pk-input:focus,
.pk-select:focus,
.pk-textarea:focus {
  border-color: var(--pk-focus-ring);
  box-shadow: 0 0 0 3px var(--pk-focus-halo);
}

.pk-textarea { min-height: 120px; resize: vertical; }

.pk-select {
  appearance: none;
  padding-right: var(--pk-space-10);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23666F7C'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--pk-space-3) center;
  background-size: 20px 20px;
}

/* Invalid state — driven by aria-invalid so the styling and the accessible
   state can never drift apart. */
.pk-input[aria-invalid="true"],
.pk-select[aria-invalid="true"],
.pk-textarea[aria-invalid="true"] {
  border-color: var(--pk-danger);
  background: var(--pk-danger-tint);
}
.pk-input[aria-invalid="true"]:focus,
.pk-textarea[aria-invalid="true"]:focus { box-shadow: 0 0 0 3px rgba(185, 28, 28, 0.22); }

.pk-error {
  display: flex;
  align-items: flex-start;
  gap: var(--pk-space-2);
  margin: var(--pk-space-2) 0 0;
  color: var(--pk-danger);
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-medium);
}
.pk-error:empty { display: none; }

/* Checkbox / radio — 24px control with a comfortable clickable label. */
.pk-check {
  display: flex;
  align-items: flex-start;
  gap: var(--pk-space-3);
  min-height: 44px;
  padding: var(--pk-space-2) 0;
  cursor: pointer;
}
.pk-check input[type="checkbox"],
.pk-check input[type="radio"] {
  flex: none;
  width: 24px; height: 24px;
  margin: 0;
  accent-color: var(--pk-brand-red);
  cursor: pointer;
}
.pk-check__text { font-size: var(--pk-text-base); line-height: 1.4; }

/* Password field with visibility toggle. The toggle is inert without JS,
   so pkoug.js injects it rather than it being hard-coded in markup. */
.pk-password { position: relative; }
.pk-password .pk-input { padding-right: 3rem; }
.pk-password__toggle {
  position: absolute;
  top: 50%;
  right: var(--pk-space-2);
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px; height: 40px;
  border: 0;
  border-radius: var(--pk-radius-sm);
  background: transparent;
  color: var(--pk-ink-muted);
  cursor: pointer;
}
.pk-password__toggle:hover { color: var(--pk-link); background: var(--pk-surface-sunk); }

.pk-fieldset {
  margin: 0 0 var(--pk-space-6);
  padding: var(--pk-space-5);
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-lg);
}
.pk-fieldset > legend {
  padding-inline: var(--pk-space-2);
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-bold);
  letter-spacing: var(--pk-tracking-wide);
  text-transform: uppercase;
  color: var(--pk-ink-muted);
}

.pk-form-row {
  display: grid;
  gap: var(--pk-space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
}

/* ==========================================================================
   7. CARDS
   ========================================================================== */

.pk-card {
  display: flex;
  flex-direction: column;
  padding: var(--pk-space-6);
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  box-shadow: var(--pk-shadow-sm);
}
.pk-card--flat  { box-shadow: none; }
.pk-card--sunk  { background: var(--pk-surface-sunk); box-shadow: none; }
.pk-card--pad-lg { padding: var(--pk-space-8); }

/* Interactive card: the whole card is clickable via the stretched-link
   pattern, but only the inner <a> is a real tab stop. */
.pk-card--link { position: relative; transition: box-shadow var(--pk-dur) var(--pk-ease), transform var(--pk-dur) var(--pk-ease); }
.pk-card--link:hover { box-shadow: var(--pk-shadow-lg); transform: translateY(-2px); }
.pk-card--link:focus-within { box-shadow: var(--pk-shadow-lg); }
.pk-card__link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }

.pk-card__title { margin: 0 0 var(--pk-space-2); font-size: var(--pk-text-xl); font-weight: var(--pk-weight-bold); line-height: var(--pk-leading-snug); }
.pk-card__body  { margin: 0; color: var(--pk-ink-soft); }
.pk-card__footer {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pk-space-3);
  align-items: center;
  margin-top: auto;
  padding-top: var(--pk-space-5);
}

/* ==========================================================================
   8. ALERTS
   ========================================================================== */

.pk-alert {
  --_tint: var(--pk-info-tint);
  --_edge: var(--pk-info-edge);
  --_ink: var(--pk-info);
  display: flex;
  gap: var(--pk-space-3);
  padding: var(--pk-space-4) var(--pk-space-5);
  border: 1px solid var(--_edge);
  border-left-width: 4px;
  border-radius: var(--pk-radius-md);
  background: var(--_tint);
  color: var(--pk-ink);
}
.pk-alert__icon  { flex: none; color: var(--_ink); line-height: 1; }
.pk-alert__title { margin: 0 0 var(--pk-space-1); font-weight: var(--pk-weight-bold); color: var(--_ink); }
.pk-alert__body  { margin: 0; color: var(--pk-ink-soft); font-size: var(--pk-text-sm); }

.pk-alert--success { --_tint: var(--pk-success-tint); --_edge: var(--pk-success-edge); --_ink: var(--pk-success); }
.pk-alert--warning { --_tint: var(--pk-warning-tint); --_edge: var(--pk-warning-edge); --_ink: var(--pk-warning); }
.pk-alert--error   { --_tint: var(--pk-danger-tint);  --_edge: var(--pk-danger-edge);  --_ink: var(--pk-danger); }

/* An outcome the reader did not have a moment ago — a success, a refusal, an
   empty result — arrives rather than being found already there. It is a CSS
   animation, not a JS-applied class, so the state is on the page for a reader
   with no JavaScript exactly as it is for everyone else; the animation is the
   only thing they miss.

   NOT in the admin area. Admin screens are tools, and a flash on every save
   confirmation becomes noise to someone working through a queue. `.pk-admin`
   is on <body> there, so this scopes itself off without each screen opting
   out. */
.pk-body:not(.pk-admin) .pk-alert,
.pk-body:not(.pk-admin) .pk-empty {
  animation: pk-state-in var(--pk-dur-slow) var(--pk-ease-out);
}

@keyframes pk-state-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

/* ==========================================================================
   9. BADGES & STATUS PILLS
   ========================================================================== */

.pk-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--pk-space-1);
  padding: 0.25em 0.7em;
  border: 1px solid var(--pk-border-strong);
  border-radius: var(--pk-radius-full);
  background: var(--pk-surface-sunk);
  color: var(--pk-ink-soft);
  font-size: var(--pk-text-xs);
  font-weight: var(--pk-weight-bold);
  letter-spacing: var(--pk-tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
}
.pk-badge--brand   { background: var(--pk-brand-red-tint);  border-color: var(--pk-brand-red-edge); color: var(--pk-brand-red-ink); }
.pk-badge--success { background: var(--pk-success-tint);    border-color: var(--pk-success-edge);   color: var(--pk-success); }
.pk-badge--warning { background: var(--pk-warning-tint);    border-color: var(--pk-warning-edge);   color: var(--pk-warning); }
.pk-badge--danger  { background: var(--pk-danger-tint);     border-color: var(--pk-danger-edge);    color: var(--pk-danger); }
.pk-badge--info    { background: var(--pk-info-tint);       border-color: var(--pk-info-edge);      color: var(--pk-info); }

/* Verified marker. The tick is decorative; the word carries the meaning so
   the status is never colour-or-icon-only. */
.pk-verified {
  display: inline-flex;
  align-items: center;
  gap: var(--pk-space-1);
  color: var(--pk-success);
  font-size: var(--pk-text-xs);
  font-weight: var(--pk-weight-bold);
}
.pk-verified svg { flex: none; }

/* Skill / tag chips */
.pk-chip {
  display: inline-flex;
  align-items: center;
  padding: 0.3em 0.75em;
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-full);
  background: var(--pk-surface-sunk);
  color: var(--pk-ink-soft);
  font-size: var(--pk-text-xs);
  font-weight: var(--pk-weight-medium);
  white-space: nowrap;
}
a.pk-chip:hover { border-color: var(--pk-brand-red-ink); color: var(--pk-link); }

/* ==========================================================================
   10. TABLES
   ========================================================================== */

/* Wrapper owns the horizontal scroll so the page body never does.
   tabindex="0" is set by pkoug.js when the table actually overflows, so
   keyboard users can scroll it. */
.pk-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  -webkit-overflow-scrolling: touch;
}
.pk-table-wrap:focus-visible { outline: var(--pk-focus-width) solid var(--pk-focus-ring); outline-offset: 2px; }

.pk-table { width: 100%; border-collapse: collapse; font-size: var(--pk-text-sm); }
.pk-table th,
.pk-table td { padding: var(--pk-space-3) var(--pk-space-4); text-align: left; vertical-align: middle; }
.pk-table thead th {
  position: sticky;
  top: 0;
  background: var(--pk-surface-sunk);
  color: var(--pk-ink-muted);
  font-size: var(--pk-text-xs);
  font-weight: var(--pk-weight-bold);
  letter-spacing: var(--pk-tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
  border-bottom: 1px solid var(--pk-border);
}
.pk-table tbody tr + tr td { border-top: 1px solid var(--pk-divider); }
.pk-table tbody tr:hover td { background: var(--pk-surface-sunk); }
.pk-table__num { text-align: right; font-variant-numeric: tabular-nums; }

/* Below 768px each row becomes a stacked block. The column name comes from
   data-label on the cell, so no duplicated markup is needed. */
@media (max-width: 767px) {
  /* The whole table drops out of table layout. Leaving `display: table` on
     the element while the rows are blocks makes the browser synthesise
     anonymous table boxes, which keeps the intrinsic column widths and
     forces the container to scroll sideways. */
  .pk-table-wrap:has(.pk-table--stack) { overflow-x: visible; }
  .pk-table--stack,
  .pk-table--stack tbody { display: block; width: 100%; }
  .pk-table--stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); }
  .pk-table--stack tbody tr {
    display: block;
    padding: var(--pk-space-3) var(--pk-space-4);
    border-bottom: 1px solid var(--pk-border);
  }
  .pk-table--stack tbody tr + tr td { border-top: 0; }
  .pk-table--stack td {
    display: grid;
    grid-template-columns: 40% 1fr;
    gap: var(--pk-space-3);
    padding: var(--pk-space-2) 0;
  }
  .pk-table--stack td::before {
    content: attr(data-label);
    color: var(--pk-ink-muted);
    font-size: var(--pk-text-xs);
    font-weight: var(--pk-weight-bold);
    letter-spacing: var(--pk-tracking-wide);
    text-transform: uppercase;
  }
  .pk-table--stack td:empty { display: none; }
}

/* ==========================================================================
   11. MODAL DIALOG
   Built on <dialog>, so Esc-to-close, focus trapping and inertness of the
   background come from the platform rather than from script.
   ========================================================================== */

.pk-modal {
  width: min(560px, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  color: var(--pk-ink);
  box-shadow: var(--pk-shadow-lg);
}
.pk-modal::backdrop { background: var(--pk-overlay); }

.pk-modal__header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--pk-space-4);
  padding: var(--pk-space-5) var(--pk-space-6);
  border-bottom: 1px solid var(--pk-border);
}
.pk-modal__title { margin: 0; font-size: var(--pk-text-xl); font-weight: var(--pk-weight-bold); }
.pk-modal__body  { padding: var(--pk-space-6); color: var(--pk-ink-soft); }
.pk-modal__footer {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--pk-space-3);
  padding: var(--pk-space-5) var(--pk-space-6);
  border-top: 1px solid var(--pk-border);
  background: var(--pk-surface-sunk);
  border-radius: 0 0 var(--pk-radius-lg) var(--pk-radius-lg);
}

/* ==========================================================================
   12. SITE HEADER & NAVIGATION
   ========================================================================== */

.pk-header {
  position: sticky;
  top: 0;
  z-index: var(--pk-z-header);
  background: color-mix(in srgb, var(--pk-surface) 92%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--pk-border);
}
@supports not (backdrop-filter: blur(10px)) {
  .pk-header { background: var(--pk-surface); }
}

.pk-header__inner {
  display: flex;
  align-items: center;
  gap: var(--pk-space-4);
  min-height: 72px;
}

.pk-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--pk-space-3);
  text-decoration: none;
  color: inherit;
  margin-right: auto;
}
.pk-brand__logo { width: 44px; height: 44px; object-fit: contain; flex: none; }
.pk-brand__name { display: block; font-size: var(--pk-text-lg); line-height: 1.1; }
.pk-brand__sub  { display: block; color: var(--pk-ink-muted); font-size: var(--pk-text-xs); font-weight: var(--pk-weight-medium); }

.pk-nav__toggle { display: inline-flex; }

/* At desktop the nav is not a disclosure at all. This override makes the
   layout correct even if the script's `hidden` bookkeeping is stale — the
   nav can never end up invisible on a wide screen. */
@media (min-width: 1024px) {
  .pk-nav__toggle { display: none; }
  .pk-nav,
  .pk-nav[hidden] { display: flex; align-items: center; gap: var(--pk-space-4); margin-left: auto; }
}

.pk-nav__list {
  display: flex;
  align-items: center;
  gap: var(--pk-space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.pk-nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--pk-space-1);
  min-height: 44px;
  padding: var(--pk-space-2) var(--pk-space-3);
  border: 0;
  border-radius: var(--pk-radius-md);
  background: transparent;
  color: var(--pk-ink-soft);
  font-family: inherit;
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-semibold);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  /* Colour only. A nav link must not move on hover: these sit in a row and a
     translate on one of them nudges the eye along the whole bar. */
  transition: background-color var(--pk-dur-fast) var(--pk-ease),
              color var(--pk-dur-fast) var(--pk-ease);
}
.pk-nav__link:hover { background: var(--pk-surface-sunk); color: var(--pk-link); }

/* Current page marked with a bar, not colour alone. */
.pk-nav__link[aria-current="page"] { color: var(--pk-link); background: var(--pk-brand-red-tint); }
.pk-nav__link[aria-current="page"]::before {
  content: "";
  position: absolute;
  bottom: 0; left: var(--pk-space-3); right: var(--pk-space-3);
  height: 2px;
  background: var(--pk-brand-red-ink);
  border-radius: var(--pk-radius-full);
}
.pk-nav__item { position: relative; }

/* Dropdown. Hidden with [hidden] so it is genuinely removed from the a11y
   tree when closed, rather than merely visually hidden. */
.pk-dropdown {
  position: absolute;
  top: calc(100% + var(--pk-space-2));
  left: 0;
  z-index: var(--pk-z-dropdown);
  min-width: 260px;
  padding: var(--pk-space-2);
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  box-shadow: var(--pk-shadow-lg);
}
.pk-dropdown[hidden] { display: none; }
.pk-dropdown__item {
  display: block;
  padding: var(--pk-space-3);
  border-radius: var(--pk-radius-sm);
  color: var(--pk-ink-soft);
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-medium);
  text-decoration: none;
}
.pk-dropdown__item:hover { background: var(--pk-surface-sunk); color: var(--pk-link); }
.pk-dropdown__note { display: block; color: var(--pk-ink-faint); font-size: var(--pk-text-xs); font-weight: var(--pk-weight-normal); }

.pk-nav__actions { display: flex; align-items: center; gap: var(--pk-space-2); }

/* --- Mobile navigation ------------------------------------------------- */
@media (max-width: 1023px) {
  .pk-nav {
    position: fixed;
    inset: 72px 0 0;
    z-index: var(--pk-z-overlay);
    padding: var(--pk-space-4) var(--pk-container-pad) var(--pk-space-12);
    background: var(--pk-surface);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .pk-nav[hidden] { display: none; }
  .pk-nav__list { flex-direction: column; align-items: stretch; gap: 0; }
  .pk-nav__link { width: 100%; justify-content: space-between; padding: var(--pk-space-4) var(--pk-space-2); font-size: var(--pk-text-base); border-bottom: 1px solid var(--pk-divider); border-radius: 0; }
  .pk-nav__link[aria-current="page"]::before { display: none; }
  .pk-nav__link[aria-current="page"] { border-left: 3px solid var(--pk-brand-red-ink); padding-left: var(--pk-space-4); }
  /* Dropdowns become inline accordions rather than floating panels. */
  .pk-dropdown {
    position: static;
    min-width: 0;
    margin: 0 0 var(--pk-space-2);
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: var(--pk-surface-sunk);
  }
  .pk-dropdown__item { padding: var(--pk-space-3) var(--pk-space-5); }
  .pk-nav__actions { margin-top: var(--pk-space-6); }
  .pk-nav__actions .pk-btn { flex: 1; }
}

/* Locks background scroll while the mobile nav is open. */
body.pk-nav-open { overflow: hidden; }

/* ==========================================================================
   13. BREADCRUMBS
   ========================================================================== */

.pk-breadcrumb { margin-block: var(--pk-space-5); font-size: var(--pk-text-sm); }
.pk-breadcrumb__list { display: flex; flex-wrap: wrap; align-items: center; gap: var(--pk-space-2); margin: 0; padding: 0; list-style: none; }
.pk-breadcrumb__item { display: inline-flex; align-items: center; gap: var(--pk-space-2); color: var(--pk-ink-muted); }
.pk-breadcrumb__item:not(:last-child)::after { content: "/"; color: var(--pk-ink-faint); }
/* min-height 24px meets WCAG 2.2 SC 2.5.8 without visually enlarging the
   crumb, which reads as body text. */
.pk-breadcrumb__item a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--pk-ink-muted);
  text-decoration: none;
}
.pk-breadcrumb__item a:hover { color: var(--pk-link); text-decoration: underline; }
.pk-breadcrumb__item[aria-current="page"] { color: var(--pk-ink); font-weight: var(--pk-weight-semibold); }

/* ==========================================================================
   14. PAGINATION
   ========================================================================== */

.pk-pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: var(--pk-space-2); margin-block: var(--pk-space-8); }
.pk-pagination__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0 var(--pk-space-3);
  border: 1px solid var(--pk-border-strong);
  border-radius: var(--pk-radius-md);
  background: var(--pk-surface);
  color: var(--pk-ink-soft);
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-semibold);
  text-decoration: none;
}
.pk-pagination__link:hover { border-color: var(--pk-brand-red-ink); color: var(--pk-link); }
.pk-pagination__link[aria-current="page"] {
  background: var(--pk-brand-red);
  border-color: var(--pk-brand-red);
  color: var(--pk-on-brand);
}
.pk-pagination__link[aria-disabled="true"] { opacity: 0.45; pointer-events: none; }
.pk-pagination__gap { padding-inline: var(--pk-space-1); color: var(--pk-ink-faint); }

/* ==========================================================================
   15. EMPTY STATES
   ========================================================================== */

.pk-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--pk-space-4);
  padding: var(--pk-space-16) var(--pk-space-6);
  border: 1px dashed var(--pk-border-strong);
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  text-align: center;
}
.pk-empty__icon  { color: var(--pk-ink-faint); }
.pk-empty__title { margin: 0; font-size: var(--pk-text-xl); font-weight: var(--pk-weight-bold); }
.pk-empty__body  { margin: 0; max-width: 46ch; color: var(--pk-ink-muted); }

/* ==========================================================================
   16. LOADING STATES
   ========================================================================== */

@keyframes pk-spin { to { transform: rotate(360deg); } }
@keyframes pk-pulse { 50% { opacity: 0.45; } }

.pk-spinner {
  display: inline-block;
  width: 1.25rem; height: 1.25rem;
  border: 2px solid var(--pk-border-strong);
  border-top-color: var(--pk-brand-red-ink);
  border-radius: 50%;
  animation: pk-spin 0.7s linear infinite;
}

.pk-skeleton {
  border-radius: var(--pk-radius-sm);
  background: var(--pk-surface-sunk);
  animation: pk-pulse 1.4s var(--pk-ease) infinite;
}
.pk-skeleton--text   { height: 0.85em; margin-block: 0.3em; }
.pk-skeleton--title  { height: 1.4em; width: 60%; }
.pk-skeleton--avatar { width: 64px; height: 64px; border-radius: 50%; }

/* With reduced motion the spinner would freeze mid-rotation and read as
   broken, so it is replaced by a gentle opacity pulse instead. */
@media (prefers-reduced-motion: reduce) {
  .pk-spinner,
  .pk-btn[data-busy="true"]::before {
    animation: pk-pulse 1.4s ease-in-out infinite;
    border-top-color: var(--pk-border-strong);
  }
  .pk-skeleton { animation: none; opacity: 0.7; }
}

/* ==========================================================================
   17. AVATARS
   ========================================================================== */

.pk-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 64px; height: 64px;
  border: 1px solid var(--pk-border);
  border-radius: 50%;
  background: var(--pk-surface-sunk);
  overflow: hidden;
  color: var(--pk-ink-muted);
  font-size: var(--pk-text-lg);
  font-weight: var(--pk-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0;
}
.pk-avatar img { width: 100%; height: 100%; object-fit: cover; }
.pk-avatar--sm { width: 40px; height: 40px; font-size: var(--pk-text-sm); }
.pk-avatar--lg { width: 96px; height: 96px; font-size: var(--pk-text-2xl); }
.pk-avatar--xl { width: 128px; height: 128px; font-size: var(--pk-text-3xl); }

/* ==========================================================================
   18. MEMBER / PROFILE CARDS
   ========================================================================== */

.pk-member-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--pk-space-4);
  padding: var(--pk-space-6);
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  box-shadow: var(--pk-shadow-sm);
  transition: box-shadow var(--pk-dur) var(--pk-ease), transform var(--pk-dur) var(--pk-ease);
}
.pk-member-card:hover { box-shadow: var(--pk-shadow-lg); transform: translateY(-2px); }
.pk-member-card:focus-within { box-shadow: var(--pk-shadow-lg); }

.pk-member-card__head { display: flex; align-items: flex-start; gap: var(--pk-space-4); }
.pk-member-card__ident { min-width: 0; }
.pk-member-card__name {
  margin: 0 0 var(--pk-space-1);
  font-size: var(--pk-text-lg);
  font-weight: var(--pk-weight-bold);
  line-height: var(--pk-leading-snug);
}
.pk-member-card__name a { color: inherit; text-decoration: none; }
.pk-member-card__name a::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.pk-member-card__name a:hover { color: var(--pk-link); }

.pk-member-card__title { margin: 0; color: var(--pk-ink-soft); font-size: var(--pk-text-sm); font-weight: var(--pk-weight-medium); }
.pk-member-card__org   { margin: 0; color: var(--pk-ink-muted); font-size: var(--pk-text-sm); }

.pk-member-card__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--pk-space-2) var(--pk-space-4);
  color: var(--pk-ink-muted);
  font-size: var(--pk-text-sm);
}
.pk-member-card__meta span { display: inline-flex; align-items: center; gap: var(--pk-space-1); }

.pk-member-card__tags { display: flex; flex-wrap: wrap; gap: var(--pk-space-2); }
.pk-member-card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--pk-space-3);
  margin-top: auto;
  padding-top: var(--pk-space-4);
  border-top: 1px solid var(--pk-divider);
}

/* Profile header on the full member page */
.pk-profile-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--pk-space-6);
  padding: var(--pk-space-8);
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-xl);
  background: var(--pk-surface);
  box-shadow: var(--pk-shadow-sm);
}
.pk-profile-head__ident { min-width: 0; flex: 1 1 320px; }

/* ==========================================================================
   19. EVENT CARDS
   ========================================================================== */

.pk-event-card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  box-shadow: var(--pk-shadow-sm);
  overflow: hidden;
  transition: box-shadow var(--pk-dur) var(--pk-ease), transform var(--pk-dur) var(--pk-ease);
}
.pk-event-card:hover { box-shadow: var(--pk-shadow-lg); transform: translateY(-2px); }
.pk-event-card:focus-within { box-shadow: var(--pk-shadow-lg); }

/* aspect-ratio reserves the space before the image loads, so the card does
   not shift when it arrives. */
.pk-event-card__media { aspect-ratio: 16 / 9; background: var(--pk-surface-sunk); }
.pk-event-card__media img { width: 100%; height: 100%; object-fit: cover; }

.pk-event-card__body { display: flex; flex-direction: column; gap: var(--pk-space-3); padding: var(--pk-space-5); flex: 1; }

/* Date block — the calendar-tile pattern */
.pk-event-date {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 62px;
  padding: var(--pk-space-2);
  border: 1px solid var(--pk-brand-red-edge);
  border-radius: var(--pk-radius-md);
  background: var(--pk-brand-red-tint);
  line-height: 1.1;
}
.pk-event-date__month { color: var(--pk-brand-red-ink); font-size: var(--pk-text-xs); font-weight: var(--pk-weight-heavy); letter-spacing: var(--pk-tracking-wide); text-transform: uppercase; }
.pk-event-date__day   { color: var(--pk-ink); font-size: var(--pk-text-xl); font-weight: var(--pk-weight-heavy); }
.pk-event-date__year  { color: var(--pk-ink-muted); font-size: var(--pk-text-xs); }

.pk-event-card__title { margin: 0; font-size: var(--pk-text-lg); font-weight: var(--pk-weight-bold); line-height: var(--pk-leading-snug); }
.pk-event-card__title a { color: inherit; text-decoration: none; }
.pk-event-card__title a::after { content: ""; position: absolute; inset: 0; }
.pk-event-card__title a:hover { color: var(--pk-link); }

.pk-event-card__meta { display: flex; flex-wrap: wrap; gap: var(--pk-space-2) var(--pk-space-4); color: var(--pk-ink-muted); font-size: var(--pk-text-sm); }
.pk-event-card__meta span { display: inline-flex; align-items: center; gap: var(--pk-space-1); }
.pk-event-card__foot { display: flex; flex-wrap: wrap; gap: var(--pk-space-2); margin-top: auto; padding-top: var(--pk-space-3); }

.pk-event-card--past { opacity: 0.82; }
.pk-event-card--past .pk-event-date { background: var(--pk-surface-sunk); border-color: var(--pk-border); }
.pk-event-card--past .pk-event-date__month { color: var(--pk-ink-muted); }

/* ==========================================================================
   20. SPONSOR CARDS
   ========================================================================== */

.pk-sponsor-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--pk-space-4);
  padding: var(--pk-space-6);
  border: 1px solid var(--pk-border);
  border-top: 3px solid var(--_tier, var(--pk-border-strong));
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  box-shadow: var(--pk-shadow-sm);
  text-align: center;
  transition: box-shadow var(--pk-dur) var(--pk-ease), transform var(--pk-dur) var(--pk-ease);
}
.pk-sponsor-card:hover { box-shadow: var(--pk-shadow-lg); transform: translateY(-2px); }

/* Tier is signalled by the top rule AND the badge text, never colour alone. */
.pk-sponsor-card--platinum { --_tier: #6E7B8B; }
.pk-sponsor-card--gold     { --_tier: #B7791F; }
.pk-sponsor-card--silver   { --_tier: #8A94A6; }

.pk-sponsor-card__logo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 96px;
  padding: var(--pk-space-3);
  background: var(--pk-surface-sunk);
  border-radius: var(--pk-radius-md);
}
.pk-sponsor-card__logo img { max-height: 100%; width: auto; object-fit: contain; }
.pk-sponsor-card__name { margin: 0; font-size: var(--pk-text-lg); font-weight: var(--pk-weight-bold); }
.pk-sponsor-card__name a { color: inherit; text-decoration: none; }
.pk-sponsor-card__name a::after { content: ""; position: absolute; inset: 0; }

/* ==========================================================================
   21. FOOTER
   ========================================================================== */

.pk-footer {
  margin-top: var(--pk-space-20);
  padding-top: var(--pk-space-16);
  border-top: 1px solid var(--pk-border);
  background: var(--pk-surface-sunk);
  color: var(--pk-ink-soft);
}
.pk-footer__grid {
  display: grid;
  gap: var(--pk-space-8);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  padding-bottom: var(--pk-space-12);
}
.pk-footer__brandcol { grid-column: span 1; max-width: 42ch; }
.pk-footer__head {
  margin: 0 0 var(--pk-space-4);
  color: var(--pk-ink);
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-bold);
  letter-spacing: var(--pk-tracking-wide);
  text-transform: uppercase;
}
.pk-footer__list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--pk-space-1); }
.pk-footer__list a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;   /* discrete nav target, not inline body text */
  color: var(--pk-ink-soft);
  font-size: var(--pk-text-sm);
  text-decoration: none;
}
.pk-footer__list a:hover { color: var(--pk-link); text-decoration: underline; }

.pk-footer__social { display: flex; gap: var(--pk-space-2); margin-top: var(--pk-space-5); }
.pk-footer__social a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px; height: 44px;
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-md);
  background: var(--pk-surface);
  color: var(--pk-ink-muted);
}
.pk-footer__social a:hover { color: var(--pk-link); border-color: var(--pk-brand-red-ink); }

.pk-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--pk-space-4);
  padding-block: var(--pk-space-5);
  border-top: 1px solid var(--pk-border);
  color: var(--pk-ink-muted);
  font-size: var(--pk-text-sm);
}

/* ==========================================================================
   20b. TABS
   The tablist ships with [hidden] and is revealed by script; with JS off
   every panel stays open, so the content reads as a plain sequence of
   sections rather than becoming unreachable.
   ========================================================================== */

.pk-tabs {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--pk-space-2);
  overflow-x: auto;
  padding-bottom: var(--pk-space-2);
  border-bottom: 1px solid var(--pk-border);
  -webkit-overflow-scrolling: touch;
  /* The strip scrolls horizontally on narrow screens; scroll-snap keeps a
     tab from being left half-visible. */
  scroll-snap-type: x proximity;
}
.pk-tabs[hidden] { display: none; }

.pk-tab {
  flex: none;
  scroll-snap-align: start;
  min-height: 44px;
  padding: 0 var(--pk-space-4);
  border: 1px solid transparent;
  border-radius: var(--pk-radius-md) var(--pk-radius-md) 0 0;
  background: transparent;
  color: var(--pk-ink-muted);
  font-family: inherit;
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-semibold);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--pk-dur) var(--pk-ease),
              background-color var(--pk-dur) var(--pk-ease);
}
.pk-tab:hover { color: var(--pk-link); background: var(--pk-surface-sunk); }

/* Selected tab carries a weight change and an underline bar, not colour
   alone, and is exposed through aria-selected. */
.pk-tab[aria-selected="true"] {
  position: relative;
  color: var(--pk-link);
  background: var(--pk-surface);
  border-color: var(--pk-border);
  border-bottom-color: transparent;
}
.pk-tab[aria-selected="true"]::after {
  content: "";
  position: absolute;
  left: var(--pk-space-3);
  right: var(--pk-space-3);
  bottom: -1px;
  height: 2px;
  background: var(--pk-brand-red);
  border-radius: var(--pk-radius-full);
}

/* Grows from the centre so the indicator reads as arriving on this tab rather
   than appearing. scaleX only — the bar reserves its full width throughout,
   so the tab strip never reflows.

   Public only, like the state entrance below. `admin/pricing_history.php`
   renders no tabs while its audit table is empty, so this leaks into the
   admin area only once real history exists — which is exactly the kind of
   motion that appears months later and nobody connects to this commit. */
.pk-body:not(.pk-admin) .pk-tab[aria-selected="true"]::after {
  transform-origin: 50% 50%;
  animation: pk-tab-underline var(--pk-dur) var(--pk-ease-out);
}

@keyframes pk-tab-underline {
  from { transform: scaleX(0); opacity: 0; }
  to   { transform: scaleX(1); opacity: 1; }
}

.pk-tabpanel[hidden] { display: none; }

/* The newly shown panel fades up. Applied to the panel that is NOT hidden,
   so with JS off — where every panel is visible and stacked — it plays once
   on load and then never again. Public only, for the reason above. */
.pk-body:not(.pk-admin) .pk-tabpanel:not([hidden]) {
  animation: pk-panel-in var(--pk-dur) var(--pk-ease-out);
}

@keyframes pk-panel-in {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}
/* Without JS the panels all show, so they need separating from each other. */
.pk-tabpanel + .pk-tabpanel {
  margin-top: var(--pk-space-8);
  padding-top: var(--pk-space-8);
  border-top: 1px solid var(--pk-divider);
}

/* ==========================================================================
   20c. PROGRESS BAR
   Decorative reporting of a value shown in adjacent text, so it is marked
   aria-hidden at the usage site rather than given a progressbar role.
   ========================================================================== */

.pk-progress__track {
  height: 8px;
  margin-top: var(--pk-space-3);
  border-radius: var(--pk-radius-full);
  background: var(--pk-surface-sunk);
  border: 1px solid var(--pk-border);
  overflow: hidden;
}
.pk-progress__bar {
  display: block;
  width: 0;
  height: 100%;
  border-radius: var(--pk-radius-full);
  background: var(--pk-brand-red);
  transition: width var(--pk-dur-slow) var(--pk-ease-out);
}

/* ==========================================================================
   20d. HONEYPOT
   Moved off-screen rather than display:none. Bots commonly skip fields that
   are display:none or type=hidden, but will fill a visible-to-the-DOM text
   input. Real users never reach it: it is aria-hidden and tabindex="-1".
   ========================================================================== */

.pk-honeypot {
  position: absolute !important;
  left: -9999px !important;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ==========================================================================
   21a. FILTER BUTTONS
   Toggle chips for filtering a list. Real <button> elements with
   aria-pressed, so keyboard operation comes from the platform.
   ========================================================================== */

.pk-filter-bar { gap: var(--pk-space-2); }

.pk-filter {
  display: inline-flex;
  align-items: center;
  /* 36px keeps these above the WCAG 2.2 24px minimum while staying visually
     light enough to read as chips rather than buttons. */
  min-height: 36px;
  padding: 0 var(--pk-space-4);
  border: 1px solid var(--pk-border-strong);
  border-radius: var(--pk-radius-full);
  background: var(--pk-surface);
  color: var(--pk-ink-soft);
  font-family: inherit;
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-semibold);
  cursor: pointer;
  transition: background-color var(--pk-dur) var(--pk-ease),
              border-color var(--pk-dur) var(--pk-ease),
              color var(--pk-dur) var(--pk-ease);
}
.pk-filter:hover { border-color: var(--pk-brand-red-ink); color: var(--pk-link); }

/* Selected state carries a fill, a border and a leading dot — not colour
   alone — and is exposed through aria-pressed. */
.pk-filter[aria-pressed="true"] {
  background: var(--pk-brand-red);
  border-color: var(--pk-brand-red);
  color: var(--pk-on-brand);
}
.pk-filter[aria-pressed="true"]::before {
  content: "";
  width: 6px;
  height: 6px;
  margin-right: var(--pk-space-2);
  border-radius: 50%;
  background: currentColor;
}

/* Monospace field, used for the copyable SIG proposal template. */
.pk-mono {
  font-family: var(--pk-font-mono);
  font-size: var(--pk-text-sm);
  line-height: var(--pk-leading-relaxed);
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
}

/* ==========================================================================
   21b. LISTS, CHECKLISTS & FAQ DISCLOSURES
   ========================================================================== */

.pk-list {
  margin: 0;
  padding-left: var(--pk-space-5);
  color: var(--pk-ink-soft);
}
.pk-list li + li { margin-top: var(--pk-space-2); }

/* Ticked list. The marker is decorative — the text carries the meaning, and
   list semantics still announce the item count. */
.pk-checklist { margin: 0; padding: 0; list-style: none; }
.pk-checklist li {
  position: relative;
  padding-left: var(--pk-space-8);
  color: var(--pk-ink-soft);
  font-size: var(--pk-text-sm);
}
.pk-checklist li + li { margin-top: var(--pk-space-3); }
.pk-checklist li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.15em;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--pk-success-tint);
  border: 1px solid var(--pk-success-edge);
  /* Tick drawn with a border, so it needs no icon font or extra request. */
  background-image: linear-gradient(45deg, transparent 45%, var(--pk-success) 45%, var(--pk-success) 55%, transparent 55%),
                    linear-gradient(-45deg, transparent 60%, var(--pk-success) 60%, var(--pk-success) 70%, transparent 70%);
  background-size: 10px 2px, 6px 2px;
  background-position: 6px 10px, 4px 8px;
  background-repeat: no-repeat;
}

/* Native <details> so the disclosure works with JavaScript disabled. */
.pk-faq {
  border: 1px solid var(--pk-border);
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface);
  overflow: hidden;
}
.pk-faq + .pk-faq { margin-top: var(--pk-space-3); }

.pk-faq__q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--pk-space-4);
  min-height: 56px;
  padding: var(--pk-space-4) var(--pk-space-5);
  color: var(--pk-ink);
  font-weight: var(--pk-weight-semibold);
  cursor: pointer;
  list-style: none;
}
.pk-faq__q::-webkit-details-marker { display: none; }
.pk-faq__q::after {
  content: "";
  flex: none;
  width: 10px;
  height: 10px;
  border-right: 2px solid var(--pk-ink-muted);
  border-bottom: 2px solid var(--pk-ink-muted);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--pk-dur) var(--pk-ease);
}
.pk-faq[open] .pk-faq__q::after { transform: rotate(-135deg) translate(-2px, -2px); }
.pk-faq__q {
  transition: background-color var(--pk-dur-fast) var(--pk-ease),
              color var(--pk-dur-fast) var(--pk-ease);
}
.pk-faq__q:hover { background: var(--pk-surface-sunk); color: var(--pk-link); }
.pk-faq__q:focus-visible {
  outline: var(--pk-focus-width) solid var(--pk-focus-ring);
  outline-offset: -2px;
}
.pk-faq__a {
  padding: 0 var(--pk-space-5) var(--pk-space-5);
  color: var(--pk-ink-soft);
  font-size: var(--pk-text-sm);
  line-height: var(--pk-leading-relaxed);
}

/* The answer fades up as the disclosure opens.
   <details> reflows when it opens — that is the control working, not a shift
   this introduces — so the animation deliberately does not touch height. It
   animates opacity and a 4px translate only, and is over well before the
   reader has finished the first line. */
.pk-faq[open] .pk-faq__a {
  animation: pk-faq-open var(--pk-dur) var(--pk-ease-out);
}

@keyframes pk-faq-open {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* ==========================================================================
   22. SCROLL REVEAL
   Content is styled visible here. pkoug.js sets `data-pk-reveal="pending"`
   only after confirming IntersectionObserver exists, then flips it to
   "shown" as each element scrolls in. So with JavaScript off, blocked or
   unsupported, nothing is ever hidden.

   Only opacity and transform animate — neither triggers layout, so this
   cannot cause a shift. The element reserves its full space throughout.
   ========================================================================== */

[data-pk-reveal="pending"] {
  opacity: 0;
  transform: translateY(12px);
}

[data-pk-reveal="shown"] {
  opacity: 1;
  transform: none;
  transition: opacity var(--pk-dur-slow) var(--pk-ease-out),
              transform var(--pk-dur-slow) var(--pk-ease-out);
  transition-delay: var(--pk-reveal-delay, 0ms);
}

/* Reduced motion: never hide anything, never move anything. The attribute
   still gets set, but it has no visual effect. */
@media (prefers-reduced-motion: reduce) {
  [data-pk-reveal="pending"],
  [data-pk-reveal="shown"] {
    opacity: 1;
    transform: none;
    transition: none;
    transition-delay: 0ms;
  }
}

/* ==========================================================================
   23. UTILITIES
   ========================================================================== */

.pk-hide { display: none !important; }
@media (max-width: 767px)  { .pk-hide-mobile  { display: none !important; } }
@media (min-width: 768px)  { .pk-hide-desktop { display: none !important; } }

.pk-text-center { text-align: center; }
.pk-mt-0 { margin-top: 0 !important; }
.pk-mb-0 { margin-bottom: 0 !important; }

/* Honour the OS setting for anything that slipped through the token layer. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  .pk-spinner,
  .pk-skeleton,
  .pk-btn[data-busy="true"]::before {
    animation-duration: 1.4s !important;
    animation-iteration-count: infinite !important;
  }
}

/* ==========================================================================
   24. REFINEMENT PASS
   Phase 2. Every rule here earns its place by fixing something measured in a
   browser, not by being current. Opacity and transform only; nothing here
   changes layout, and everything degrades to the previous appearance when a
   feature is unsupported.
   ========================================================================== */

/* --------------------------------------------------------------- RHYTHM ---
   Two adjacent <section class="pk-section"> each carry padding-block, so the
   space between them was the SUM of both — 192px at desktop on blog.php,
   between the search field and its own results. The preceding section's
   padding-bottom is already the separation; the following one's padding-top
   was pure dead space.

   Sunk sections keep theirs: their padding is the band, not the gap. */
.pk-section + .pk-section:not(.pk-section--sunk) { padding-top: 0; }

/* A page-head section sits closer to what it introduces than to an unrelated
   band below it. */
.pk-blog-head { padding-bottom: var(--pk-space-10); }

/* ---------------------------------------------------------- STICKY NAV ---
   The header is already sticky with a backdrop blur. What it lacked was any
   acknowledgement that the page had moved: over a long article it floated
   with the same flat edge it has at the top, which reads as an artefact
   rather than as a layer.

   Driven by scroll position, NOT by a scroll listener — `animation-timeline:
   scroll()` runs on the compositor, so this costs no JavaScript at all and
   honours the project's no-per-frame-JS rule by construction. Where it is
   unsupported the header keeps exactly its current appearance, which is why
   the whole thing sits behind @supports. */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .pk-header {
      animation: pk-header-lift linear both;
      animation-timeline: scroll(root block);
      /* Complete the change over the first 120px of travel. */
      animation-range: 0 120px;
    }
    @keyframes pk-header-lift {
      to {
        background: color-mix(in srgb, var(--pk-surface) 82%, transparent);
        box-shadow: var(--pk-shadow-md);
        border-bottom-color: transparent;
      }
    }
  }
}

/* ------------------------------------------------------------- SURFACES ---
   A hard 1px line between bands is the cheapest possible transition and looks
   it. A hairline that fades out at both ends reads as the surface ending
   rather than as a drawn rule. Decorative, so it is a pseudo-element and
   never announced. */
.pk-section--sunk { position: relative; }
.pk-section--sunk::before,
.pk-section--sunk::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--pk-border) 18%, var(--pk-border) 82%, transparent);
  pointer-events: none;
}
.pk-section--sunk::before { top: 0; }
.pk-section--sunk::after  { bottom: 0; }

/* ---------------------------------------------------------------- CARDS ---
   The lift existed; the edge did not move with it, so a hovered card grew a
   shadow while keeping the same flat border as its neighbours. Moving the
   border toward the brand tint is what makes it read as "this one", and it
   costs no layout because the border width never changes.

   `@media (hover: hover)` matters: on a touch screen :hover sticks after a
   tap, so a card stayed lifted until something else was tapped. */
@media (hover: hover) {
  .pk-card--link:hover { border-color: var(--pk-brand-red-edge); }
}

/* focus-within gets the SAME treatment as hover, deliberately. A keyboard
   user tabbing into a card should see what a pointer user sees, and this was
   previously a weaker state — shadow only, no border. */
.pk-card--link:focus-within {
  border-color: var(--pk-brand-red-edge);
  transform: translateY(-2px);
}

/* Blog and sponsor cards are the same object with different content, and were
   drifting apart: only .pk-card--link had any interaction feedback at all. */
.pk-blog-card,
.pk-sponsor-card {
  transition: box-shadow var(--pk-dur) var(--pk-ease),
              transform var(--pk-dur) var(--pk-ease),
              border-color var(--pk-dur) var(--pk-ease);
}
@media (hover: hover) {
  .pk-blog-card:hover,
  .pk-sponsor-card:hover {
    box-shadow: var(--pk-shadow-lg);
    transform: translateY(-2px);
    border-color: var(--pk-brand-red-edge);
  }
}
.pk-blog-card:focus-within,
.pk-sponsor-card:focus-within {
  box-shadow: var(--pk-shadow-lg);
  transform: translateY(-2px);
  border-color: var(--pk-brand-red-edge);
}

/* The image inside a blog card scales very slightly on hover. The media box
   already clips, so this is contained — and it is on the IMAGE, not the card,
   so the card's own geometry is untouched. */
.pk-blog-card__media { overflow: hidden; }
.pk-blog-card__media img { transition: transform var(--pk-dur-slow) var(--pk-ease-out); }
@media (hover: hover) {
  .pk-blog-card:hover .pk-blog-card__media img { transform: scale(1.03); }
}

/* --------------------------------------------------------------- STATES ---
   An empty state was a flat panel with an icon. Giving it a faint tinted wash
   makes it read as a deliberate answer — "there is nothing here yet" — rather
   than as a region that failed to load. */
.pk-empty {
  background:
    radial-gradient(120% 100% at 50% 0%, var(--pk-brand-red-tint), transparent 70%),
    var(--pk-surface);
}

/* A disabled control should look unavailable, not broken. */
.pk-btn[disabled],
.pk-btn[aria-disabled="true"] { filter: saturate(0.65); }

/* ------------------------------------------------- BUTTON PRESS FEEDBACK ---
   Buttons lifted on hover and returned on :active, which is right, but the
   press had no weight to it. A very slight inset scale reads as the control
   accepting the press. Transform only. */
.pk-btn:active { transform: translateY(0) scale(0.985); }

/* ------------------------------------------------------ VIEW TRANSITIONS ---
   Cross-document view transitions: a same-origin navigation cross-fades
   instead of blanking to white. This is progressive in the strictest sense —
   an engine without support navigates exactly as it does today, and there is
   no script, no polyfill and no fallback path to maintain.

   Deliberately NOT enabled under reduced motion: a cross-fade is still
   motion, and the rule is that it is removed rather than shortened. The
   @media wrapper is what does that, because `navigation: auto` cannot be
   overridden per-element. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--pk-dur);
  animation-timing-function: var(--pk-ease-out);
}

/* The header is the one element that must NOT cross-fade: it is in the same
   place on both pages, so fading it out and in makes a fixed element flicker
   on every navigation. Naming it gives it its own transition group, and
   assigning no animation to that group leaves it alone. */
.pk-header { view-transition-name: pk-header; }
::view-transition-group(pk-header) { animation: none; }

/* ==========================================================================
   25. MOTION HIERARCHY
   Five tiers, strongest first. The point of a hierarchy is that things are
   NOT animated the same way: if every element arrives with equal weight the
   page reads as busy and nothing is emphasised.

     1. hero content ......... one orchestrated entrance, the largest travel
     2. major sections ....... restrained reveal, larger travel than a card
     3. cards ................ short staggered entrance, small travel
     4. buttons and controls . interaction feedback only, no entrance
     5. admin ................ essential transitions only, nothing decorative

   Everything is opacity and transform. Nothing here changes layout.
   ========================================================================== */

/* --- TIER 2. Sections ----------------------------------------------------
   A section header carries more travel than a card, so arriving at a new
   section registers as a section rather than as more cards. */
[data-pk-reveal="pending"]:not([data-pk-reveal-group] > *) {
  transform: translateY(20px);
}
[data-pk-reveal="shown"]:not([data-pk-reveal-group] > *) {
  transition-duration: 520ms;
}

/* --- TIER 3. Cards -------------------------------------------------------
   Deliberately SHORTER and smaller than a section. A card is one item in a
   set; it should feel like part of a sequence, not an announcement. The
   stagger does the work, so each individual card needs very little travel. */
[data-pk-reveal-group] > [data-pk-reveal="pending"] {
  transform: translateY(9px);
}
[data-pk-reveal-group] > [data-pk-reveal="shown"] {
  transition-duration: 380ms;
}

/* --- TIER 4. Controls ----------------------------------------------------
   Feedback only. No control has an entrance animation: a button that arrives
   is a button you cannot press yet. */
.pk-btn--primary { box-shadow: 0 1px 2px rgba(11,15,20,.10); }
@media (hover: hover) {
  .pk-btn--primary:hover:not([disabled]) {
    box-shadow: 0 6px 16px color-mix(in srgb, var(--pk-brand-red) 28%, transparent);
  }
}
.pk-btn { transition-property: background-color, border-color, color, transform, box-shadow; }

/* The focus ring arrives instantly — never transitioned. A ring that fades in
   is a ring that is not there yet for someone tabbing quickly. */
.pk-btn:focus-visible,
.pk-input:focus-visible,
.pk-select:focus-visible,
.pk-textarea:focus-visible { transition: none; }

/* Chips and tags: the smallest interactive thing on the page, so the
   smallest possible response. Colour only. */
.pk-chip {
  transition: background-color var(--pk-dur-fast) var(--pk-ease),
              border-color var(--pk-dur-fast) var(--pk-ease),
              color var(--pk-dur-fast) var(--pk-ease);
}

/* Tab and FAQ hit areas already transition colour; give the FAQ marker the
   same easing as the panel it opens so the two read as one gesture. */
.pk-faq__q::after { transition-timing-function: var(--pk-ease-out); }

/* --- DEPTH ---------------------------------------------------------------
   A flat tint reads as a block of colour; a very shallow vertical gradient
   reads as a surface. Two stops, a few percent apart — enough to catch the
   eye as depth, not enough to be seen as a gradient. */
.pk-section--sunk {
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--pk-surface-sunk) 60%, var(--pk-bg)) 0%,
      var(--pk-surface-sunk) 18%,
      var(--pk-surface-sunk) 82%,
      color-mix(in srgb, var(--pk-surface-sunk) 60%, var(--pk-bg)) 100%);
}

/* Cards sat flat against the page. A resting shadow this shallow is not
   visible as a shadow — it is what stops the card looking painted on. */
.pk-card,
.pk-blog-card,
.pk-sponsor-card { box-shadow: 0 1px 2px rgba(11,15,20,.045); }
:root[data-theme="dark"] .pk-card,
:root[data-theme="dark"] .pk-blog-card,
:root[data-theme="dark"] .pk-sponsor-card { box-shadow: 0 1px 2px rgba(0,0,0,.4); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .pk-card,
  :root:not([data-theme="light"]) .pk-blog-card,
  :root:not([data-theme="light"]) .pk-sponsor-card { box-shadow: 0 1px 2px rgba(0,0,0,.4); }
}

/* --- TYPOGRAPHY ----------------------------------------------------------
   Large display type needs negative tracking or it reads loose; body text at
   the same tracking would read cramped. This is why it is set per size
   rather than globally. */
.pk-display { letter-spacing: -0.03em; }
.pk-h1 { letter-spacing: -0.022em; }
.pk-h2 { letter-spacing: -0.016em; }

/* A lead is the bridge between a heading and body text and should read as
   neither. Slightly longer line-height, slightly softer ink. */
.pk-lead { line-height: 1.62; color: var(--pk-ink-soft); }

/* An eyebrow is a label, not a sentence: wide tracking, small, and it should
   sit tight to the heading it introduces rather than floating. */
.pk-eyebrow { margin-bottom: var(--pk-space-2); }

/* --- MOBILE --------------------------------------------------------------
   Designed for the width, not compressed from desktop. Below 560px the hero
   is the whole first screen, so it gets its own proportions: less vertical
   padding (the viewport is short), tighter display tracking (long words wrap
   badly at large sizes), and full-width buttons that are genuinely thumb
   targets rather than desktop buttons that happen to fit. */
@media (max-width: 560px) {
  .pk-hero { padding-block: var(--pk-space-10) var(--pk-space-12); }
  .pk-display { letter-spacing: -0.035em; line-height: 1.06; }
  .pk-hero__cta { gap: var(--pk-space-3); }
  .pk-hero__cta .pk-btn { flex: 1 1 100%; }
  .pk-lead { line-height: 1.58; }

  /* Cards carry their own separation on a narrow screen, where they stack
     and the eye has no column to follow. */
  .pk-grid { gap: var(--pk-space-5); }

  /* A section boundary is the only cue that content has changed subject once
     everything is one column. */
  .pk-section { padding-block: var(--pk-space-10); }
}

/* ==========================================================================
   26. EVENT BANNERS
   Uploads are accepted as supplied — no conversion, no thumbnails — so the
   ratio is enforced in CSS instead. Every <img> carries intrinsic width and
   height in the markup, which is what reserves the box before the bytes
   arrive; without those a banner is the largest layout shift on the page.
   ========================================================================== */

.pk-event-banner {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--pk-radius-lg);
  background: var(--pk-surface-sunk);
}
.pk-event-banner img {
  display: block;
  width: 100%;
  height: 100%;
  /* An image that is not 16:9 is cropped rather than distorted or letterboxed:
     the recommendation is 1600x900, and anything else still has to sit in the
     same slot without changing the layout around it. */
  object-fit: cover;
  object-position: center;
}

/* The designed fallback. An event with no banner must not leave a hole, and it
   must not look like an image that failed to load — so this is deliberately a
   panel with the brand wash rather than a grey box or a broken-image icon. */
.pk-event-banner--none {
  display: grid;
  place-items: center;
  background:
    radial-gradient(120% 140% at 20% 0%, var(--pk-brand-red-tint), transparent 60%),
    radial-gradient(120% 140% at 85% 100%, var(--pk-brand-navy-tint), transparent 62%),
    var(--pk-surface-sunk);
  border: 1px solid var(--pk-border);
}
.pk-event-banner--none span {
  color: var(--pk-ink-faint);
  font-size: var(--pk-text-sm);
  font-weight: var(--pk-weight-semibold);
  letter-spacing: var(--pk-tracking-wide);
  text-transform: uppercase;
}

/* On a card the banner is the top edge, so it loses its lower corners. */
.pk-card .pk-event-banner { border-radius: var(--pk-radius-md); margin-bottom: var(--pk-space-4); }

/* ==========================================================================
   27. TESTIMONIALS / COMMUNITY VOICES
   A quotation with an attribution, so the markup is figure > blockquote +
   figcaption rather than a paragraph wearing quotation marks. The styles below
   only dress that structure; none of them are load-bearing for meaning.

   Photos are accepted as supplied — no conversion, no thumbnails, same
   decision as the event banners — so the circle and the crop are enforced
   here, and every <img> carries intrinsic width and height in the markup to
   reserve its box before the bytes arrive.
   ========================================================================== */

.pk-quote {
  display: flex;
  flex-direction: column;
  gap: var(--pk-space-5);
}

.pk-quote__body {
  margin: 0;
  /* The decorative mark is a pseudo-element, not a character in the text: a
     screen reader must not announce it, and it must not end up in a copied
     selection of what the person actually said. */
  position: relative;
  padding-top: var(--pk-space-6);
}
.pk-quote__body::before {
  content: "\201C";
  position: absolute;
  top: calc(var(--pk-space-2) * -1);
  left: 0;
  font-size: 3.5rem;
  line-height: 1;
  color: var(--pk-brand-red-edge);
  pointer-events: none;
}
.pk-quote__body p {
  margin: 0;
  font-size: var(--pk-text-lg);
  line-height: var(--pk-leading-relaxed);
  color: var(--pk-ink);
}

.pk-quote__by {
  display: flex;
  align-items: center;
  gap: var(--pk-space-3);
  /* Pushed to the bottom so attributions line up across a row of cards whose
     quotes are different lengths. */
  margin-top: auto;
}

.pk-quote__photo {
  flex: 0 0 auto;
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  object-fit: cover;
  object-position: center;
  border: 1px solid var(--pk-border);
  background: var(--pk-surface-sunk);
}

.pk-quote__name {
  display: block;
  font-weight: var(--pk-weight-semibold);
  color: var(--pk-ink);
}
.pk-quote__role {
  display: block;
  font-size: var(--pk-text-sm);
  line-height: var(--pk-leading-snug);
}

/* ==========================================================================
   28. STATUS VOCABULARY — shared between the admin area and the member portal
   ==========================================================================

   These rules lived in pkoug-admin.css, which no member page loads. The member
   dashboard and security centre were already emitting `.pk-status`,
   `.pk-billing--*` and `.pk-cred--*`, so every one of those pills has been
   rendering as plain inherited text — the same silent failure §25 recorded,
   one level up: the class HAD a rule, just not in a stylesheet the page asked
   for. tools/test_admin_styles.php could not see it, because it checked that a
   rule existed SOMEWHERE rather than that it existed in a sheet the page links.
   It now checks per page, so this cannot recur.

   They are here rather than duplicated because there is one vocabulary. An
   account that is `locked` means the same thing to the administrator holding
   the record and to the member reading their own security centre, and two
   copies of a colour scale is how the two come to disagree.
   ========================================================================== */

/* --------------------------------------------------------------- status --
   Semantic colour, separate from the brand accent: these say good, waiting
   and refused, and must not be confused with "this is a PKOUG thing". */

.pk-status {
  display: inline-block;
  font-size: var(--pk-text-xs, .75rem);
  font-weight: var(--pk-weight-heavy);
  padding: .18em .6em;
  border-radius: 999px;
  border: 1px solid currentColor;
  white-space: nowrap;
}

/* Through the semantic tokens rather than literal hex. These were four
   hardcoded colours plus two duplicated dark-theme override blocks, which is
   how `--rejected` ended up pointing at an undefined property while the other
   three quietly kept working. The tokens are already defined for every theme
   state, so the overrides are no longer needed at all.

   Measured, light on surface / on sunk, then dark:
     approved  5.02 / 4.56 / 10.06     verified  5.02 / 4.56 / 10.56
     rejected  6.47 / 5.88 /  6.98     pending   7.56 / 6.87 /  7.80  */
.pk-status--approved             { color: var(--pk-success); }
.pk-status--verified             { color: var(--pk-warning); }
.pk-status--rejected             { color: var(--pk-danger); }
.pk-status--pending_verification { color: var(--pk-text-muted); }

/* Billing state pills — payments list, record view, and the member dashboard.
   Separate from .pk-status--* because billing state and submission status
   are different axes: a record can be `verified` and `expired` at once. */
.pk-billing--active  { color: var(--pk-success); }
.pk-billing--expired { color: var(--pk-danger); }
.pk-billing--pending { color: var(--pk-warning); }
.pk-billing--other   { color: var(--pk-text-muted); }

/* --------------------------------------------------- publication state --
   admin/events.php and admin/testimonials.php both interpolate a row's status
   into the class name, so EVERY value either table can hold needs a rule here
   or the badge renders unstyled. Events was doing exactly that: `published`,
   `draft` and `cancelled` were emitted and none of the three existed.
   tools/test_admin_styles.php now fails when that recurs.

   Publication state is its own axis, distinct from billing and from
   submission status — an event can be `published` and in the past. */
.pk-billing--published { color: var(--pk-success); }
.pk-billing--draft     { color: var(--pk-text-muted); }
.pk-billing--cancelled { color: var(--pk-danger); }

/* Member submission states, added with migration 016. `pending` already exists
   above on the billing axis and carries the same meaning here — something is
   waiting on a person — so it is reused rather than duplicated.

   `rejected` is muted rather than red. A moderator declining to publish a
   testimonial is an ordinary editorial outcome, not a fault or an alarm, and
   the member can revise and resubmit; colouring it like an error would misread
   the state to whoever is triaging the queue, and would tell the member their
   own words were a mistake. `--pk-danger` stays for states that are genuinely
   irreversible or wrong. */
.pk-billing--rejected  { color: var(--pk-text-muted); }

/* Withdrawn consent, on admin/testimonials.php and on the member's own
   testimonial screen. Its own modifier rather than reusing `--cancelled`: an
   administrator cancelling an event and a person revoking permission to be
   quoted are different acts, and the second is not reversible. Heavier weight
   because it is the one state on either screen that can never be undone. */
.pk-billing--withdrawn {
  color: var(--pk-danger);
  font-weight: var(--pk-weight-heavy);
}

/* ==========================================================================
   28a. CREDENTIAL STATE — migration 017
   ==========================================================================

   Its own prefix rather than more `.pk-billing--*` values. Billing state,
   publication state and CREDENTIAL state are three different axes: a member
   can be billed `active` and locked out, or `expired` and perfectly able to
   sign in. Overloading one prefix is how `pk-billing--published` came to sit
   beside `pk-billing--expired` meaning nothing like the same thing.

   admin/view.php and member/security.php both interpolate the state into the
   class name, so every value pkoug_member_credential_state() can return needs
   a rule here or the badge renders unstyled and silent — the defect class §25
   recorded. The registry in tools/test_admin_styles.php reads
   PKOUG_CRED_STATES, so adding a state to the constant fails that suite until
   it is styled.                                                              */

.pk-cred--active    { color: var(--pk-success); }

/* Waiting on the member, and actionable by an admin — reissue the invitation.
   Warning rather than muted because it is a state somebody should chase. */
.pk-cred--invited   { color: var(--pk-warning); }

/* Nothing is wrong: they simply have not been invited yet. Muted, for the
   same reason `--rejected` is muted on the testimonial axis — an ordinary
   outcome must not be coloured like a fault. */
.pk-cred--unclaimed { color: var(--pk-text-muted); }
.pk-cred--none      { color: var(--pk-text-muted); }

/* A restriction an administrator applied. Red is right here: unlike a declined
   testimonial, this is somebody being kept out, and whoever is triaging the
   record — or the member reading their own security centre — needs to see it
   before they act on anything else. */
.pk-cred--locked    { color: var(--pk-danger); }

/* The heaviest state on this axis and the only one an ordinary admin cannot
   reverse, so it carries weight as well as colour — the same treatment
   `--withdrawn` gets, for the same reason. */
.pk-cred--disabled  {
  color: var(--pk-danger);
  font-weight: var(--pk-weight-heavy);
}

/* Not a state — a flag that rides alongside one. Warning, because it is a
   pending obligation on the member rather than a restriction on them. */
.pk-cred--change    { color: var(--pk-warning); }

/* ==========================================================================
   28b. THREE PLAIN CLASSES THAT WERE EMITTED WITH NO RULE
   ==========================================================================

   `.pk-h5` was used six times in admin/view.php and was never written, so every
   sub-heading on that screen rendered at the browser's default h3 size with the
   project's type scale ignored. `.pk-table__caption` belongs with it:
   admin/payments.php hides its caption visually, and a caption that is meant to
   be SEEN needs a rule that makes it look deliberate rather than like stray
   text above a table. The member security centre captions its activity table
   the same way.

   Both are plain classes rather than modifiers, which is why the modifier
   sweep in tools/test_admin_styles.php never caught the first one.

   `.pk-field--check` is the third instance, found by extending that sweep to
   member/: member/profile.php and member/testimonial.php have been using it
   around consent and privacy checkboxes since Phase 2, and it had no rule at
   all. It exists because `.pk-field` carries a bottom margin sized for a
   labelled input, which is too much between stacked checkboxes.              */
.pk-h5 {
  font-family: var(--pk-font-display, inherit);
  font-size: var(--pk-text-lg);
  font-weight: var(--pk-weight-bold);
  line-height: var(--pk-leading-tight);
  color: var(--pk-ink);
}

.pk-table__caption {
  caption-side: top;
  text-align: left;
  padding: 0 0 var(--pk-space-3);
  font-size: var(--pk-text-sm);
  color: var(--pk-text-muted);
}

.pk-field--check { margin-bottom: var(--pk-space-2); }
