/* ==========================================================================
   SolutionTee TechHub — base, layout, components
   ========================================================================== */

/* ── Reset ──────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--nav-h) + 1.5rem);
}
/* Reduced motion means fewer and gentler animations, not none. Opacity and
   colour still carry the state change - they are what make a toast or a focus
   ring legible. What goes is movement: transforms, travel, position. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-property: opacity, background-color, background, border-color,
                         color, box-shadow, fill, stroke !important;
    transition-duration: 160ms !important;
  }
  /* Anything already in place stays in place rather than starting offset. */
  [data-reveal] { opacity: 1 !important; transform: none !important; }
  .drawer__link { opacity: 1 !important; transform: none !important; }
  /* Progress indicators are exempt: a frozen spinner stops reporting that
     anything is happening, which is worse than the motion it removes. */
  .mark__pulse, .btn.is-loading::before {
    animation-duration: revert !important;
    animation-iteration-count: infinite !important;
  }
  /* Keep its 6s delay - collapsing it would hide the overlay immediately. */
  #site-loader { animation-delay: 6s !important; animation-duration: 1ms !important; }
}

body {
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img, svg, video, canvas { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
ul, ol { list-style: none; padding: 0; }
a { color: inherit; text-decoration: none; }
table { border-collapse: collapse; width: 100%; }
hr { border: 0; height: 1px; background: var(--grad-line); }
::selection { background: var(--accent-500); color: var(--brand-900); }

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}

/* Custom scrollbar (progressive enhancement) */
* { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-thumb {
  background: var(--line-strong); border-radius: var(--r-pill);
  border: 3px solid transparent; background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover { background: var(--brand-400); background-clip: content-box; }

/* ── Typography ─────────────────────────────────────────────────────── */
h1, h2, h3, h4, h5 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: var(--lh-snug);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}
h1 { font-size: var(--fs-4xl); line-height: var(--lh-tight); }
h2 { font-size: var(--fs-3xl); }
h3 { font-size: var(--fs-xl); }
h4 { font-size: var(--fs-lg); }
p { text-wrap: pretty; }

.eyebrow {
  display: inline-flex; align-items: center; gap: 0.6em;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--accent-ink);
}
.eyebrow::before {
  content: ""; width: 26px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent-500));
}
.eyebrow--plain::before { display: none; }

.lede { font-size: var(--fs-lg); color: var(--text-muted); max-width: 62ch; }
.muted { color: var(--text-muted); }
.dim { color: var(--text-dim); }
.mono { font-family: var(--font-mono); }
.tiny { font-size: var(--fs-xs); }
.center { text-align: center; }
.nowrap { white-space: nowrap; }

.gradient-text {
  background: var(--grad-accent);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}
.brand-text {
  background: var(--grad-brand);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

.underline-sweep { position: relative; display: inline-block; }
.underline-sweep::after {
  content: ""; position: absolute; inset-inline-start: 0; bottom: -0.12em;
  width: 100%; height: 0.14em; border-radius: 2px;
  background: var(--grad-accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform var(--dur-slow) var(--ease-out);
}
.is-visible .underline-sweep::after, .underline-sweep.is-on::after { transform: scaleX(1); }

/* ── Layout ─────────────────────────────────────────────────────────── */
/* Layout margins live on padding, not width, so a full-bleed background can
   sit on the same element while its content stays inset from the safe area. */
.container,
.container--narrow,
.container--wide {
  width: 100%;
  margin-inline: auto;
  padding-inline: var(--gutter-safe);
}
.container { max-width: calc(var(--container) + var(--gutter) * 2); }
.container--narrow { max-width: calc(var(--container-narrow) + var(--gutter) * 2); }
.container--wide { max-width: calc(var(--container-wide) + var(--gutter) * 2); }

.section { padding-block: var(--section-y); position: relative; }
.section--tight { padding-block: clamp(2.5rem, 5vw, 4.5rem); }
.section--alt { background: var(--bg-alt); }

/* Section headers. The gap below a header is roughly half the gap between
   sections, so a header always reads as belonging to the block beneath it. */
.section-head { max-width: 74ch; margin-bottom: clamp(2rem, 4vw, 3.5rem); }
.section-head--center { margin-inline: auto; text-align: center; }
.section-head p { margin-top: var(--sp-4); }
.section-head h2 { margin-top: var(--sp-3); }

/* Header with a trailing action. One shared leading edge with every other
   header on the page; the action sits on the trailing edge and drops below
   on narrow screens instead of squeezing the heading. */
.section-head--split {
  display: flex; flex-wrap: wrap; gap: var(--sp-4) var(--sp-5);
  align-items: flex-end; justify-content: space-between;
  max-width: none;
}
.section-head--split > :first-child { flex: 1 1 min(100%, 34ch); max-width: 74ch; }
.section-head--split .section-head { margin-bottom: 0; }

.grid { display: grid; gap: clamp(1.25rem, 2.2vw, 2rem); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr)); }
.stack { display: flex; flex-direction: column; gap: var(--sp-4); }
.row { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; }
.row--between { justify-content: space-between; }
.row--end { justify-content: flex-end; }
.spacer { flex: 1; }

.skip-link {
  position: absolute; inset-inline-start: -9999px; top: 0; z-index: 10000;
  padding: 0.75rem 1.25rem; background: var(--accent-500); color: var(--brand-900);
  font-weight: 700; border-radius: 0 0 var(--r-sm) 0;
}
.skip-link:focus { inset-inline-start: 0; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* Decorative grid backdrop */
.bg-grid { position: absolute; inset: 0; pointer-events: none; opacity: 0.5; }
.bg-grid::before {
  content: ""; position: absolute; inset: 0;
  background-image:
    linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 64px 64px;
  mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 20%, transparent 75%);
}
/* Phones get no lattice. At 375px a 64px grid reads as a rendering artefact
   rather than as paper, and it is the tell that makes a header look
   generated. Every page header uses this layer, so one rule clears the
   lot — landing, academy, blog, business, about and contact alike. */
@media (max-width: 820px) { .bg-grid { display: none; } }
/* ── Buttons ────────────────────────────────────────────────────────── */
.btn {
  --btn-bg: var(--surface-strong);
  --btn-fg: var(--text);
  --btn-border: var(--line-strong);
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  gap: 0.6em; padding: 0.85em 1.6em;
  font-family: var(--font-display); font-size: var(--fs-sm); font-weight: 600;
  letter-spacing: 0.01em; line-height: 1;
  background: var(--btn-bg); color: var(--btn-fg);
  border: 1px solid var(--btn-border); border-radius: var(--r-pill);
  cursor: pointer; overflow: hidden; isolation: isolate;
  /* Press feedback is the fastest thing on the page - the user is watching
     their own finger. Colour and shadow can take their time behind it. */
  transition: transform var(--dur-press) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
  /* Labels must wrap rather than push the button off screen. A longer
     translation or 200% zoom makes a nowrap button the first thing to clip,
     and a two-line button beats an unreachable one. */
  white-space: normal;
  text-wrap: balance;
  max-width: 100%;
}
/* Hover motion is gated: a tap on a touch screen fires :hover and leaves the
   button stuck in its hovered state until something else is tapped. */
@media (hover: hover) and (pointer: fine) {
  .btn:hover { transform: translateY(-2px); }
}
/* Later in source order, so a press beats a hover on a pointer device. */
.btn:active { transform: scale(0.97); }
.btn[disabled], .btn.is-disabled { opacity: 0.5; pointer-events: none; }

/* Pending state. Between the click and the server's reply there is nothing
   telling the user the form was heard, and on a slow connection that is
   several seconds of silence - so this is feedback, not decoration.

   The label fades rather than being removed, and JS pins the button's width,
   so the button does not resize under the cursor. It stays focusable and
   keeps its name/value: `disabled` would drop the button from the submitted
   payload, which several of our forms depend on. */
.btn.is-loading {
  color: transparent;
  pointer-events: none;
  cursor: progress;
  transition: color var(--dur-fast) var(--ease-out);
}
.btn.is-loading svg { opacity: 0; transition: opacity var(--dur-fast) var(--ease-out); }
.btn.is-loading::before {
  content: "";
  position: absolute; top: 50%; left: 50%;
  width: 1.15em; height: 1.15em; margin: -0.575em 0 0 -0.575em;
  border-radius: 50%;
  /* currentColor is transparent while loading, so the ring names its own. */
  border: 2px solid color-mix(in srgb, var(--btn-fg) 26%, transparent);
  border-top-color: var(--btn-fg);
  /* A CSS animation, deliberately: it runs off the main thread, so it keeps
     turning while the page is busy doing the very work it is reporting.
     Constant motion, so linear - an eased spinner reads as stuttering. */
  animation: btn-spin 620ms linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

.btn--primary {
  --btn-bg: var(--grad-accent); --btn-fg: var(--brand-900); --btn-border: transparent;
  box-shadow: 0 12px 30px -14px rgba(242, 178, 51, 0.85);
}
@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover { box-shadow: 0 18px 40px -14px rgba(242, 178, 51, 0.95); }
}

.btn--brand {
  --btn-bg: var(--grad-brand); --btn-fg: #fff; --btn-border: transparent;
  box-shadow: var(--shadow-brand);
}

.btn--ghost { --btn-bg: transparent; --btn-fg: var(--text); --btn-border: var(--line-strong); }
.btn--ghost:hover { --btn-border: var(--accent-500); --btn-fg: var(--accent-500); }

.btn--solid { --btn-bg: var(--text); --btn-fg: var(--bg); --btn-border: transparent; }

.btn--danger { --btn-bg: var(--danger-bg); --btn-fg: var(--danger); --btn-border: rgba(255,93,115,.35); }
.btn--danger:hover { --btn-bg: var(--danger); --btn-fg: #fff; }

.btn--sm { padding: 0.6em 1.1em; font-size: var(--fs-xs); }
.btn--lg { padding: 1.05em 2.1em; font-size: var(--fs-md); }
.btn--block { width: 100%; }
.btn--icon { padding: 0.7em; aspect-ratio: 1; border-radius: 50%; }

/* Sheen sweep on hover */
.btn--primary::after, .btn--brand::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(110deg, transparent 20%, rgba(255,255,255,.4) 50%, transparent 80%);
  transform: translateX(-120%);
  transition: transform var(--dur-slow) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .btn--primary:hover::after, .btn--brand:hover::after { transform: translateX(120%); }
  .btn:hover .btn-arrow { transform: translateX(4px); }
}

.btn-arrow { transition: transform var(--dur-fast) var(--ease-out); }

/* ── Cards ──────────────────────────────────────────────────────────── */
.card {
  position: relative; display: flex; flex-direction: column;
  background: var(--grad-glass);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: clamp(1.25rem, 2vw, 1.85rem);
  overflow: hidden; isolation: isolate;
  backdrop-filter: var(--blur-glass);
  transition: transform var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.card::before {
  content: ""; position: absolute; inset: -1px; z-index: -1; opacity: 0;
  background: radial-gradient(420px circle at var(--mx, 50%) var(--my, 0%),
              color-mix(in srgb, var(--card-accent, var(--brand-400)) 26%, transparent), transparent 62%);
  transition: opacity var(--dur-fast) var(--ease-out);
}
/* A card is hovered dozens of times in a browsing session, so the motion has
   to be near-imperceptible: a 4px lift that reads as depth, not as an event. */
@media (hover: hover) and (pointer: fine) {
  .card:hover { transform: translateY(-4px); border-color: var(--line-strong); box-shadow: var(--shadow-lg); }
  .card:hover::before { opacity: 1; }
}

.card--flat { background: var(--surface); backdrop-filter: none; }
.card--solid { background: var(--surface-solid); backdrop-filter: none; }
.card--pad-lg { padding: clamp(1.75rem, 3vw, 2.75rem); }

.card-icon {
  display: grid; place-items: center;
  width: 54px; height: 54px; border-radius: var(--r-md);
  background: color-mix(in srgb, var(--card-accent, var(--brand-400)) 18%, transparent);
  color: var(--card-accent, var(--brand-300));
  border: 1px solid color-mix(in srgb, var(--card-accent, var(--brand-400)) 30%, transparent);
  margin-bottom: var(--sp-4);
  /* No overshoot here. A spring curve is for the rare and the playful; on a
     hover it turns every pass of the cursor into a small event. */
  transition: transform var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .card:hover .card-icon { transform: scale(1.05); }
}
.card-icon svg { width: 26px; height: 26px; }

.card-title { font-size: var(--fs-lg); margin-bottom: var(--sp-2); }
.card-body { color: var(--text-muted); font-size: var(--fs-sm); flex: 1; }
.card-foot {
  margin-top: var(--sp-5); padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
}

.card-media {
  position: relative; margin: calc(-1 * clamp(1.25rem, 2vw, 1.85rem));
  margin-bottom: var(--sp-5); aspect-ratio: 16 / 9; overflow: hidden;
  background: linear-gradient(135deg, var(--brand-600), var(--brand-800));
}
.card-media img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--dur-slow) var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .card:hover .card-media img { transform: scale(1.06); }
}
.card-media__fallback {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--font-display); font-size: 2.6rem; font-weight: 700;
  /* Sits on --surface, so it has to follow the theme: white-on-white was
     invisible the moment light became the default. */
  color: var(--text-dim); opacity: 0.35;
}

/* ── Badges & pills ─────────────────────────────────────────────────── */
.badge {
  display: inline-flex; align-items: center; gap: 0.45em;
  padding: 0.36em 0.8em; border-radius: var(--r-pill);
  font-family: var(--font-mono); font-size: var(--fs-2xs); font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  background: var(--surface-strong); color: var(--text-muted);
  border: 1px solid var(--line);
}
.badge--accent { background: rgba(242,178,51,.14); color: var(--accent-ink); border-color: var(--line-accent); }
.badge--brand { background: rgba(37,99,179,.16); color: var(--brand-300); border-color: rgba(37,99,179,.32); }
.badge--success { background: var(--success-bg); color: var(--success); border-color: rgba(34,197,139,.32); }
.badge--warning { background: var(--warning-bg); color: var(--warning); border-color: rgba(242,178,51,.32); }
.badge--danger { background: var(--danger-bg); color: var(--danger); border-color: rgba(255,93,115,.32); }
.badge--info { background: var(--info-bg); color: var(--info); border-color: rgba(56,189,248,.32); }
.badge--dot::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

.chip {
  display: inline-flex; align-items: center; gap: 0.4em;
  padding: 0.45em 0.95em; border-radius: var(--r-pill);
  font-size: var(--fs-xs); font-weight: 500;
  background: var(--surface); border: 1px solid var(--line); color: var(--text-muted);
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
a.chip:hover, .chip.is-active { background: var(--accent-500); color: var(--brand-900); border-color: transparent; }

/* ── Navigation ─────────────────────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: var(--z-nav);
  height: var(--nav-h);
  display: flex; align-items: center;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: var(--blur-glass);
  border-bottom: 1px solid transparent;
  /* .is-stuck changes only the backdrop and the hairline. `height` was in
     this list without ever changing, costing a layout pass near the trigger
     point on every scroll. */
  transition: background var(--dur) var(--ease-out),
              border-color var(--dur) var(--ease-out);
}
.nav.is-stuck { border-bottom-color: var(--line); background: color-mix(in srgb, var(--bg) 92%, transparent); }
.nav__inner { display: flex; align-items: center; gap: var(--sp-5); width: 100%; }

.logo { display: inline-flex; align-items: center; gap: 0.65rem; flex-shrink: 0; }
/* The mark carries its own colours so it reads on either theme without a
   tinted tile behind it. On the dark theme the navy runs would disappear
   into the header, so they lift to the light ink.

   No width/height here: the caller's class sizes it. Setting 100% on the
   same element the sizing class is on meant whichever rule came last won,
   and the mark rendered at its intrinsic 400x336 in the header. */
.brand-mark { display: block; }
.logo__mark { width: 40px; height: 34px; flex-shrink: 0; }
.logo__mark--lg { width: 52px; height: 44px; }

/* ── The mark, routing ──────────────────────────────────────────────────
   The same sequence as the loader, shortened: the trace routes out of the
   amber source and the crossbar reveals as it passes through. 953 and 219
   are the measured path lengths; the crossbar's 0.36s delay is where it
   sits along the trace (344 of 953), not a number picked to look right.

   One trigger class, set by brand-mark.js on hover and on entering the
   viewport. It used to be three hard-coded ancestor selectors, which meant
   a mark in a new context silently did nothing.
   -------------------------------------------------------------------- */
.brand-mark--draw .bm-trace { stroke-dasharray: 953; }
.brand-mark--draw .bm-cross { stroke-dasharray: 219; }

.brand-mark--draw.is-drawing .bm-trace { animation: bm-route 900ms linear 60ms both; }
.brand-mark--draw.is-drawing .bm-cross { animation: bm-cross 210ms linear 385ms both; }
.brand-mark--draw.is-drawing .bm-amber {
  animation: bm-node 340ms var(--ease-out) both;
  transform-origin: 388px 154px;
}
.brand-mark--draw.is-drawing .bm-navy {
  animation: bm-node 320ms var(--ease-out) 920ms both;
  transform-origin: 109px 390px;
}
@keyframes bm-route { from { stroke-dashoffset: 953; } to { stroke-dashoffset: 0; } }
@keyframes bm-cross { from { stroke-dashoffset: 219; } to { stroke-dashoffset: 0; } }
@keyframes bm-node {
  0%   { opacity: 0; transform: scale(0.4); }
  64%  { opacity: 1; transform: scale(1.08); }
  100% { opacity: 1; transform: scale(1); }
}
/* Reduced motion: the mark still arrives, it just does not draw itself. */
@media (prefers-reduced-motion: reduce) {
  .brand-mark--draw .bm-trace,
  .brand-mark--draw .bm-cross { stroke-dasharray: none; }
  .brand-mark--draw.is-drawing * { animation: none !important; }
}

/* The lockup from the brand sheet: one line, two colours, tagline beneath.
   It used to stack the name over "TECHHUB", which is not what the sheet
   sets - the split is inside the name itself. */
.logo__text { display: flex; flex-direction: column; line-height: 1.08; }
.logo__name {
  font-family: var(--font-display); font-weight: 700;
  font-size: 1.06rem; letter-spacing: -0.02em; white-space: nowrap;
}
.logo__name-a { color: var(--mark-navy); }
.logo__name-b { color: var(--mark-blue); }
.logo__tag {
  font-family: var(--font-mono); font-size: 0.56rem;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--mark-blue); white-space: nowrap;
}
/* The footer lockup has room to breathe. */
.footer .logo__name { font-size: 1.24rem; }
.footer .logo__tag { font-size: 0.6rem; letter-spacing: 0.2em; }

.nav__links { display: flex; align-items: center; gap: 0.35rem; }
.nav__link {
  position: relative; padding: 0.55rem 0.9rem; border-radius: var(--r-pill);
  font-size: var(--fs-sm); font-weight: 500; color: var(--text-muted);
  transition: color var(--dur) var(--ease-out), background var(--dur) var(--ease-out);
}
.nav__link:hover, .nav__link.is-active { color: var(--text); background: var(--surface); }
.nav__link.is-active::after {
  content: ""; position: absolute; left: 50%; bottom: 3px; translate: -50% 0;
  width: 16px; height: 2px; border-radius: 2px; background: var(--accent-500);
}
.nav__actions { display: flex; align-items: center; gap: 0.5rem; margin-inline-start: auto; }

.nav-account {
  min-height: 40px; display: inline-flex; align-items: center; gap: 0.55rem;
  padding: 0; border: 0; border-radius: 0;
  background: transparent; color: var(--text); text-align: left;
}
.nav-account:hover, .nav-account[aria-expanded="true"] {
  background: transparent;
}
.nav-account__copy {
  display: grid; gap: 0.05rem; min-width: 0; line-height: 1.1;
}
.nav-account__copy strong {
  max-width: 9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--fs-xs); font-weight: 650;
}
.nav-account__chevron {
  width: 18px; height: 28px; display: grid; place-items: center;
  color: var(--text-dim);
  transition: transform var(--dur-fast) var(--ease-out);
}
.nav-account-wrap.is-open .nav-account__chevron { transform: rotate(180deg); }
.dropdown__panel--account { min-width: 290px; }

.icon-btn {
  display: grid; place-items: center; width: 40px; height: 40px;
  border-radius: 50%; color: var(--text-muted);
  border: 1px solid transparent;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.icon-btn:hover { background: var(--surface); color: var(--text); border-color: var(--line); }
.icon-btn svg { width: 19px; height: 19px; }
.icon-btn__badge {
  position: absolute; top: 4px; right: 4px; min-width: 16px; height: 16px;
  border-radius: var(--r-pill); background: var(--accent-500); color: var(--brand-900);
  font-size: 0.58rem; font-weight: 700; display: grid; place-items: center; padding: 0 4px;
}
.icon-btn-wrap { position: relative; }

.nav__toggle { display: none; }

/* Mobile drawer */
.drawer {
  position: fixed; inset: 0; z-index: var(--z-drawer);
  width: 100%; min-height: 100dvh; height: 100dvh;
  background: var(--bg);
  padding: max(var(--sp-5), env(safe-area-inset-top, 0px))
           var(--gutter-safe)
           max(var(--sp-5), var(--safe-block-end));
  display: flex; flex-direction: column; gap: var(--sp-4);
  transform: translateX(100%); visibility: hidden;
  transition: transform var(--dur-slow) var(--ease-drawer), visibility var(--dur-slow);
  overflow: hidden;
}
.drawer.is-open { transform: none; visibility: visible; }
.drawer__nav {
  display: flex; flex-direction: column; gap: 0;
  min-height: 0; overflow-y: auto; overscroll-behavior: contain;
}
.drawer__actions {
  display: flex; flex-direction: column; gap: .65rem;
  margin-top: auto; flex: 0 0 auto;
  padding-top: .35rem;
}
.drawer__link {
  display: flex; align-items: center; justify-content: space-between;
  padding: clamp(.62rem, 1.8dvh, .9rem) 0.25rem;
  font-family: var(--font-display); font-size: clamp(1.05rem, 3.2dvh, 1.3rem); font-weight: 600;
  border-bottom: 1px solid var(--line);
  opacity: 0; transform: translateY(14px);
}
.drawer.is-open .drawer__link {
  /* `both`, not `backwards`. The resting style below is opacity: 0, so
     `backwards` alone holds the from-state through the stagger delay and then
     drops straight back to that resting style the moment the animation ends -
     every link fades in and then vanishes. `both` keeps the from-state during
     the delay AND the to-state afterwards, which is what this needs. */
  animation: drawer-in var(--dur) var(--ease-out) both;
}
/* The stagger delay is set inline by the Nav module when the drawer opens -
   it has to be, because the link count varies with who is signed in. These
   used to be duplicated here as six nth-child rules that an inline style
   silently outranked, so the numbers in the stylesheet were never the ones
   that ran. One source only. */
@keyframes drawer-in { to { opacity: 1; transform: none; } }

@media (max-width: 1020px) {
  .nav__links { display: none; }
  .nav__toggle { display: grid; }
  .nav-account { padding: 0; border: 0; background: transparent; }
}

/* Dropdown */
.dropdown { position: relative; }
.dropdown__panel {
  position: absolute; right: 0; top: calc(100% + 10px); min-width: 260px;
  background: var(--bg-elevated); border: 1px solid var(--line);
  border-radius: var(--r-md); box-shadow: var(--shadow-lg);
  padding: 0.5rem; z-index: 10;
  opacity: 0; visibility: hidden; transform: translateY(-8px) scale(.98);
  /* Anchored under its trigger at the top-right, so it grows out of the
     trigger rather than out of its own middle. */
  transform-origin: top right;
  transition: opacity var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out),
              visibility var(--dur);
}
.dropdown.is-open .dropdown__panel { opacity: 1; visibility: visible; transform: none; }
.dropdown__item {
  display: flex; align-items: center; gap: 0.7rem; width: 100%;
  padding: 0.65rem 0.75rem; border-radius: var(--r-sm);
  font-size: var(--fs-sm); color: var(--text-muted); text-align: left;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.dropdown__item:hover { background: var(--surface); color: var(--text); }
.dropdown__item svg { width: 16px; height: 16px; flex-shrink: 0; }
.dropdown__divider { height: 1px; background: var(--line); margin: 0.4rem 0; }
.dropdown__head { padding: 0.7rem 0.75rem; border-bottom: 1px solid var(--line); margin-bottom: 0.4rem; }

/* ── Forms ──────────────────────────────────────────────────────────── */
.field { display: flex; flex-direction: column; gap: 0.45rem; margin-bottom: var(--sp-4); }
.field label, .label {
  font-size: var(--fs-sm); font-weight: 600; color: var(--text);
  display: flex; align-items: center; gap: 0.4rem;
}
.field .hint { font-size: var(--fs-xs); color: var(--text-dim); }
.required { color: var(--accent-ink); }

.input, .select, .textarea {
  width: 100%; padding: 0.85rem 1rem;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  font-size: var(--fs-sm); line-height: 1.45;
  transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.input::placeholder, .textarea::placeholder { color: var(--text-dim); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--line-strong); }
.input:focus, .select:focus, .textarea:focus {
  outline: none; border-color: var(--accent-500); box-shadow: var(--ring);
  background: var(--surface-strong);
}
.input.has-error, .select.has-error, .textarea.has-error { border-color: var(--danger); }
.textarea { resize: vertical; min-height: 120px; font-family: inherit; }
.select {
  appearance: none; padding-inline-end: 2.6rem; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a3b4d0' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0.9rem center; background-size: 17px;
}
.select option { background: var(--bg-elevated); color: var(--text); }
input[type="color"] { padding: 0.3rem; height: 44px; cursor: pointer; }
input[type="file"] {
  padding: 0.65rem; font-size: var(--fs-xs);
  background: var(--surface); border: 1px dashed var(--line-strong); border-radius: var(--r-sm);
  width: 100%; cursor: pointer;
}
input[type="file"]::file-selector-button {
  margin-inline-end: 0.85rem; padding: 0.45rem 0.9rem; border-radius: var(--r-pill);
  border: 0; background: var(--surface-strong); color: var(--text);
  font-size: var(--fs-xs); font-weight: 600; cursor: pointer;
}

.field-error { font-size: var(--fs-xs); color: var(--danger); display: flex; align-items: center; gap: 0.35rem; }
.field-error::before { content: "!"; display: grid; place-items: center; width: 14px; height: 14px; border-radius: 50%; background: var(--danger); color: #fff; font-size: 0.6rem; font-weight: 700; }

.check {
  display: flex; align-items: flex-start; gap: 0.7rem;
  font-size: var(--fs-sm); color: var(--text-muted); cursor: pointer;
  padding: 0.2rem 0;
}
.check input[type="checkbox"], .check input[type="radio"] {
  appearance: none; flex-shrink: 0; width: 20px; height: 20px; margin-top: 1px;
  border: 1.5px solid var(--line-strong); border-radius: 6px;
  background: var(--surface); cursor: pointer; position: relative;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.check input[type="radio"] { border-radius: 50%; }
.check input:checked { background: var(--accent-500); border-color: var(--accent-500); }
.check input[type="checkbox"]:checked::after {
  content: ""; position: absolute; left: 6px; top: 2px; width: 5px; height: 10px;
  border: solid var(--brand-900); border-width: 0 2.4px 2.4px 0; transform: rotate(43deg);
}
.check input[type="radio"]:checked::after {
  content: ""; position: absolute; inset: 4px; border-radius: 50%; background: var(--brand-900);
}
.check input:focus-visible { box-shadow: var(--ring); }

.check-grid { display: grid; gap: 0.5rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
.check-card {
  display: flex; align-items: center; gap: 0.75rem;
  padding: 0.95rem 1.1rem; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--surface); cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.check-card:hover { border-color: var(--line-strong); }
.check-card:has(input:checked) { border-color: var(--accent-500); background: rgba(242,178,51,.08); }

.form-note {
  font-size: var(--fs-xs); color: var(--text-dim);
  padding: 0.85rem 1rem; background: var(--surface); border-radius: var(--r-sm);
  border-inline-start: 2px solid var(--brand-400);
}

/* Password strength meter */
.pw-meter { display: flex; gap: 4px; margin-top: 0.35rem; }
.pw-meter span {
  flex: 1; height: 4px; border-radius: 2px; background: var(--line);
  transition: background var(--dur) var(--ease-out);
}
.pw-meter[data-score="1"] span:nth-child(-n+1) { background: var(--danger); }
.pw-meter[data-score="2"] span:nth-child(-n+2) { background: #f97316; }
.pw-meter[data-score="3"] span:nth-child(-n+3) { background: var(--warning); }
.pw-meter[data-score="4"] span { background: var(--success); }

/* OTP input group */
.otp-row { display: flex; gap: 0.6rem; justify-content: center; }
.otp-row input {
  width: 52px; height: 62px; text-align: center;
  font-family: var(--font-mono); font-size: 1.5rem; font-weight: 600;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--text);
}
.otp-row input:focus { outline: none; border-color: var(--accent-500); box-shadow: var(--ring); }
@media (max-width: 420px) { .otp-row input { width: 42px; height: 52px; font-size: 1.2rem; } }

/* ── Flash / alerts ─────────────────────────────────────────────────── */
/* ── Toasts ─────────────────────────────────────────────────────────────
   The coloured left stripe this used to have is the default every framework
   ships, and it spends a whole edge of the card saying one bit of
   information. The state lives in a filled icon chip instead: same bit, read
   faster, and it leaves the card's own geometry alone.

   The stack is empty until something arrives, so it must not intercept
   clicks — `pointer-events: none` here, restored on each toast. */
.toast-stack {
  position: fixed; top: calc(var(--nav-h) + 1rem);
  inset-inline-end: max(1.25rem, var(--gutter-safe));
  z-index: var(--z-toast); display: flex; flex-direction: column; gap: 0.6rem;
  width: min(384px, calc(100vw - 2.5rem)); pointer-events: none;
}

.toast {
  --toast-accent: var(--info);
  --toast-tint: var(--info-bg);

  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  gap: 0.7rem;
  padding: 0.85rem 0.9rem;
  border-radius: var(--r-md);
  background: var(--bg-elevated);
  border: 1px solid var(--line);
  box-shadow: var(--shadow-lg);
  pointer-events: auto;
  overflow: hidden;              /* clips the timer to the rounded corner */
  touch-action: pan-y;           /* let the swipe handler claim horizontal */

  /* Transitions, not keyframes. Toasts stack up during form validation and a
     user can dismiss one mid-entrance; a transition retargets from wherever
     the element currently is, where a keyframe restarts from zero. */
  opacity: 0;
  transform: translateX(24px);
  transition: opacity var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out);
}
.toast.is-in { opacity: 1; transform: none; }
/* Leaves through the edge it entered from — what makes swipe-away read as
   the obvious gesture rather than a hidden one. */
.toast.is-leaving { opacity: 0; transform: translateX(24px); }
/* While a finger is on it, the toast follows exactly and must not lag. */
.toast.is-dragging { transition: none; cursor: grabbing; user-select: none; }

.toast--success { --toast-accent: var(--success); --toast-tint: var(--success-bg); }
.toast--error   { --toast-accent: var(--danger);  --toast-tint: var(--danger-bg); }
.toast--warning { --toast-accent: var(--warning); --toast-tint: var(--warning-bg); }
.toast--info    { --toast-accent: var(--info);    --toast-tint: var(--info-bg); }

.toast__chip {
  display: grid; place-items: center;
  width: 28px; height: 28px; border-radius: 9px;
  background: var(--toast-tint); color: var(--toast-accent);
  flex-shrink: 0;
}
.toast__chip svg { width: 17px; height: 17px; }

.toast__body { min-width: 0; padding-block-start: 0.18rem; }
.toast__title {
  margin: 0; font-size: var(--fs-sm); font-weight: 550;
  line-height: 1.45; color: var(--text);
  /* Messages are sentences, not labels: let long ones wrap and break rather
     than push the close button off the card. */
  overflow-wrap: break-word;
}
.toast__detail {
  margin: 0.2rem 0 0; font-size: var(--fs-xs); line-height: 1.5;
  color: var(--text-muted); overflow-wrap: break-word;
}

/* An action turns a report into something you can act on — "Undo", "View".
   Ranged with the text, not stranded in the corner. */
.toast__action {
  margin-top: 0.55rem; display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: var(--fs-xs); font-weight: 600; color: var(--toast-accent);
  background: none; border: 0; padding: 0.15rem 0; cursor: pointer;
  border-bottom: 1px solid currentColor;
}
.toast__action:hover { opacity: 0.75; }

.toast__close {
  display: grid; place-items: center;
  width: 26px; height: 26px; margin: -0.1rem -0.15rem 0 0;
  border-radius: 8px; color: var(--text-dim); flex-shrink: 0;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.toast__close:hover { background: var(--surface); color: var(--text); }

/* Time remaining, as a hairline along the bottom edge. Scale rather than
   width so it animates on the compositor and never triggers layout. The JS
   sets the duration and pauses it on hover. */
.toast__timer {
  position: absolute; inset-inline: 0; bottom: 0; height: 2px;
  background: var(--toast-accent); opacity: 0.55;
  transform: scaleX(1); transform-origin: left center;
}
[dir="rtl"] .toast__timer { transform-origin: right center; }
.toast[data-sticky] .toast__timer { display: none; }

/* When several arrive at once, the count goes on the newest rather than
   repeating the same sentence down the stack. */
.toast__count {
  display: inline-block; margin-inline-start: 0.4rem;
  padding: 0.05rem 0.35rem; border-radius: var(--r-pill);
  background: var(--toast-tint); color: var(--toast-accent);
  font-size: 0.68rem; font-weight: 700; font-variant-numeric: tabular-nums;
  vertical-align: 1px;
}

/* Phones: bottom of the screen, where the thumb is, and clear of the nav.
   Swipe-down dismisses there, so the exit travels that way too. */
@media (max-width: 640px) {
  .toast-stack {
    top: auto; bottom: max(1rem, var(--gutter-safe));
    inset-inline: max(0.75rem, var(--gutter-safe));
    width: auto;
    flex-direction: column-reverse;   /* newest nearest the thumb */
  }
  .toast { transform: translateY(24px); }
  .toast.is-leaving { transform: translateY(24px); }
  .toast__close { width: 32px; height: 32px; }
}

/* Movement goes, the message stays. A toast that never appears is a toast
   that failed, so opacity is kept even here. */
@media (prefers-reduced-motion: reduce) {
  .toast, .toast.is-leaving { transform: none; }
  .toast__timer { display: none; }
}

.alert {
  display: flex; gap: 0.85rem; align-items: flex-start;
  padding: 1rem 1.15rem; border-radius: var(--r-md);
  background: var(--surface); border: 1px solid var(--line);
  font-size: var(--fs-sm);
}
.alert--success { background: var(--success-bg); border-color: rgba(34,197,139,.3); }
.alert--warning { background: var(--warning-bg); border-color: rgba(242,178,51,.3); }
.alert--danger { background: var(--danger-bg); border-color: rgba(255,93,115,.3); }
.alert--info { background: var(--info-bg); border-color: rgba(56,189,248,.3); }
.alert svg { flex-shrink: 0; width: 20px; height: 20px; margin-top: 1px; }

/* ── Tables ─────────────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-md); }
.table { min-width: 640px; font-size: var(--fs-sm); }
.table th {
  text-align: left; padding: 0.9rem 1rem; font-family: var(--font-mono);
  font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: 0.08em;
  color: var(--text-dim); background: var(--surface); font-weight: 600;
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
.table td { padding: 0.9rem 1rem; border-bottom: 1px solid var(--line); vertical-align: middle; }
.table tbody tr { transition: background var(--dur-fast) var(--ease-out); }
.table tbody tr:hover { background: var(--surface); }
.table tbody tr:last-child td { border-bottom: 0; }
.table__num { font-family: var(--font-mono); text-align: right; }

/* ── Pagination ─────────────────────────────────────────────────────── */
.pagination { display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: center; margin-top: var(--sp-7); }
.pagination a, .pagination span {
  min-width: 40px; height: 40px; padding: 0 0.75rem;
  display: grid; place-items: center; border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-family: var(--font-mono);
  border: 1px solid var(--line); color: var(--text-muted);
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.pagination a:hover { border-color: var(--accent-500); color: var(--accent-ink); }
.pagination .is-current { background: var(--accent-500); color: var(--brand-900); border-color: transparent; font-weight: 700; }
.pagination .is-gap { border: 0; }

/* ── Avatar ─────────────────────────────────────────────────────────── */
.avatar {
  display: grid; place-items: center; flex-shrink: 0;
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--grad-brand); color: #fff;
  font-family: var(--font-display); font-weight: 700; font-size: 0.82rem;
  overflow: hidden; border: 1px solid var(--line);
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar--sm { width: 32px; height: 32px; font-size: 0.7rem; }
.avatar--lg { width: 64px; height: 64px; font-size: 1.15rem; }
.avatar--xl { width: 104px; height: 104px; font-size: 1.9rem; }

/* ── Accordion ──────────────────────────────────────────────────────── */
.accordion { border-top: 1px solid var(--line); }
.accordion__item { border-bottom: 1px solid var(--line); }
.accordion__trigger {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  width: 100%; padding: 1.35rem 0.25rem; text-align: left;
  font-family: var(--font-display); font-size: var(--fs-md); font-weight: 600;
  color: var(--text); transition: color var(--dur-fast) var(--ease-out);
}
.accordion__trigger:hover { color: var(--accent-ink); }
.accordion__icon {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center; border: 1px solid var(--line);
  transition: transform var(--dur) var(--ease-out), background var(--dur) var(--ease-out);
}
.accordion__item.is-open .accordion__icon { transform: rotate(45deg); background: var(--accent-500); color: var(--brand-900); border-color: transparent; }
.accordion__panel {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  opacity: 0;
  transition: grid-template-rows var(--dur) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out),
              visibility 0s linear var(--dur);
}
.accordion__item.is-open .accordion__panel {
  grid-template-rows: 1fr;
  visibility: visible;
  opacity: 1;
  transition-delay: 0s;
}
.accordion__panel > div { overflow: hidden; }
.accordion__panel p { padding-bottom: 1.4rem; color: var(--text-muted); font-size: var(--fs-sm); max-width: 72ch; }

/* ── Tabs ───────────────────────────────────────────────────────────── */
.tabs {
  display: flex; gap: 0.35rem; padding: 0.35rem;
  background: var(--surface); border-radius: var(--r-pill);
  border: 1px solid var(--line);
  width: fit-content; max-width: 100%; overflow-x: auto;
}
/* Once the labels stop fitting, wrap them onto a second row rather than
   hiding options behind a scroll a thumb has little reason to try.
   Translated labels grow, so this is the safe direction. */
@media (max-width: 620px) {
  .tabs {
    flex-wrap: wrap; justify-content: center;
    overflow-x: visible; width: 100%;
    border-radius: var(--r-lg);
  }
}
.tab {
  padding: 0.6rem 1.15rem; border-radius: var(--r-pill);
  font-size: var(--fs-sm); font-weight: 600; color: var(--text-muted);
  white-space: nowrap;
  transition: color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.tab:hover { color: var(--text); }
.tab.is-active { background: var(--accent-500); color: var(--brand-900); }
.tab-panel { display: none; }
.tab-panel.is-active { display: block; animation: fade-up var(--dur) var(--ease-out); }

/* ── Page-hero entrance ─────────────────────────────────────────────── */
/* Every inner page opens with a .page-hero, and it is above the fold on
   arrival - so it wants a mount animation, not a scroll reveal. One rule
   covers every page rather than annotating forty templates.

   A CSS animation, not a transition: there is no state to toggle, and it runs
   off the main thread, so it stays smooth while the page is still fetching
   its images. `backwards` holds the from-state during the stagger delay,
   which is what stops a flash of the final position before each one starts. */
@media (prefers-reduced-motion: no-preference) {
  html.js .page-hero > .container > * {
    animation: page-hero-in 320ms var(--ease-out) backwards;
  }
  html.js .page-hero > .container > :nth-child(2) { animation-delay: 50ms; }
  html.js .page-hero > .container > :nth-child(3) { animation-delay: 100ms; }
  html.js .page-hero > .container > :nth-child(4) { animation-delay: 150ms; }
  html.js .page-hero > .container > :nth-child(n+5) { animation-delay: 200ms; }
}
@keyframes page-hero-in { from { opacity: 0; transform: translateY(10px); } }

/* ── Scroll reveal ──────────────────────────────────────────────────── */
/* The travel used to be 26px over 720ms, which left an element visibly
   settling well after the eye had already reached it. 14px over 460ms lands
   as you arrive.

   `html.js` gates the hidden start state, so if the script never runs the
   page is simply readable rather than blank. Every rule below carries the
   same `html.js` prefix so the resting and revealed states stay on one
   specificity ladder - without it the direction variants outrank
   `.is-visible` and the element never comes back. */
[data-reveal] {
  transition: opacity var(--dur-reveal) var(--ease-out),
              transform var(--dur-reveal) var(--ease-out);
}
html.js [data-reveal] { opacity: 0; transform: translateY(14px); }
html.js [data-reveal="left"] { transform: translateX(-20px); }
html.js [data-reveal="right"] { transform: translateX(20px); }
html.js [data-reveal="scale"] { transform: scale(.96); }
/* Below the point where these columns stack, a sideways entrance pushes past
   the inline edge and creates real overflow. Fall back to the vertical
   reveal rather than leaning on body { overflow-x: hidden } to hide it. */
@media (max-width: 900px) {
  html.js [data-reveal="left"], html.js [data-reveal="right"] { transform: translateY(14px); }
}
html.js [data-reveal].is-visible { opacity: 1; transform: none; }
@keyframes fade-up { from { opacity: 0; transform: translateY(8px); } }

/* ── Footer ─────────────────────────────────────────────────────────── */
.footer { position: relative; background: var(--bg-alt); border-top: 1px solid var(--line); padding-top: var(--sp-9); overflow: hidden; }
.footer__grid { display: grid; gap: var(--sp-6); grid-template-columns: 1.6fr repeat(3, 1fr); }
@media (max-width: 900px) { .footer__grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .footer__grid { grid-template-columns: 1fr; } }
.footer h4 { font-size: var(--fs-sm); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); margin-bottom: var(--sp-4); }
.footer__link { display: block; padding: 0.32rem 0; font-size: var(--fs-sm); color: var(--text-muted); transition: color var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out); }
.footer__link:hover { color: var(--accent-ink); }
@media (hover: hover) and (pointer: fine) {
  .footer__link:hover { transform: translateX(3px); }
}
.footer__bottom {
  margin-top: var(--sp-8);
  padding-block: var(--sp-5) max(var(--sp-5), var(--safe-block-end));
  border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: var(--sp-4); justify-content: space-between; align-items: center;
  font-size: var(--fs-xs); color: var(--text-dim);
}
.social-row { display: flex; gap: 0.5rem; }
.social {
  display: grid; place-items: center; width: 38px; height: 38px; border-radius: var(--r-sm);
  border: 1px solid var(--line); color: var(--text-muted);
  transition: background 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);
}
.social:hover { background: var(--accent-500); color: var(--brand-900); border-color: transparent; }
@media (hover: hover) and (pointer: fine) {
  .social:hover { transform: translateY(-3px); }
}
.social svg { width: 17px; height: 17px; }

.footer__wordmark {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(3rem, 15vw, 11rem); line-height: 0.8;
  letter-spacing: -0.05em; text-align: center;
  color: transparent; -webkit-text-stroke: 1px var(--line-strong);
  user-select: none; margin-bottom: -0.18em; padding-top: var(--sp-6);
  opacity: .55;
}

/* ── Notice modal ───────────────────────────────────────────────────── */
/* A native <dialog>, so the focus trap, the Escape key, the inertness of the
   page behind it and the top-layer stacking all come from the browser rather
   than from code we would have to keep correct.

   Two shapes, one panel: with an image it is a card split down the middle on
   a wide screen and stacked on a phone; without one it is a narrow column. */
.notice {
  /* The UA centres a modal <dialog> with `margin: auto`, and the reset at the
     top of this file (`* { margin: 0 }`) takes that away - which pins the
     panel to the top-left corner. Put it back explicitly. */
  margin: auto;
  /* `%` not `vw` here: 100vw counts the scrollbar, so the panel came out
     narrower than asked for and sat slightly off centre. Inside a modal
     dialog, `%` resolves against the space actually available. */
  width: min(520px, calc(100% - 2rem));
  max-height: calc(100dvh - 2rem);
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xl);
  background: var(--bg-elevated);
  color: var(--text);
  box-shadow: var(--shadow-lg);
  overflow: hidden;

  /* Entrance and exit. Never scale(0) - it arrives from 0.97, which reads as
     coming forward rather than as being conjured. A modal is not anchored to
     a trigger, so it keeps the default centred origin.
     `allow-discrete` is what lets `display` and the top layer participate, so
     the exit actually plays instead of the dialog vanishing on close. Older
     browsers simply skip the transition and open instantly. */
  opacity: 0;
  transform: translateY(10px) scale(0.97);
  transition: opacity var(--dur) var(--ease-out),
              transform var(--dur) var(--ease-out),
              overlay var(--dur) allow-discrete,
              display var(--dur) allow-discrete;
}
.notice[open] { opacity: 1; transform: none; }
@starting-style {
  .notice[open] { opacity: 0; transform: translateY(10px) scale(0.97); }
}
.notice--framed { width: min(780px, calc(100% - 2rem)); }

.notice::backdrop {
  background: rgba(3, 8, 18, 0.66);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity var(--dur) var(--ease-out),
              overlay var(--dur) allow-discrete,
              display var(--dur) allow-discrete;
}
.notice[open]::backdrop { opacity: 1; }
@starting-style { .notice[open]::backdrop { opacity: 0; } }

.notice__inner {
  position: relative; display: flex; flex-direction: column;
  /* The panel clips to its own rounded corners, so the scroll has to live on
     the inside - otherwise a tall notice on a landscape phone loses its
     buttons off the bottom edge. */
  max-height: calc(100dvh - 2rem); overflow-y: auto;
}
.notice__close {
  position: absolute; top: 0.75rem; right: 0.75rem; z-index: 2;
  display: grid; place-items: center; width: 34px; height: 34px;
  border: 1px solid var(--line); border-radius: 50%;
  background: color-mix(in srgb, var(--bg-elevated) 82%, transparent);
  backdrop-filter: blur(4px);
  color: var(--text-dim); cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
.notice__close:hover {
  background: var(--surface-strong); color: var(--text); border-color: var(--line-strong);
}

.notice__media { aspect-ratio: 16 / 9; overflow: hidden; background: var(--surface); }
.notice__media img { width: 100%; height: 100%; object-fit: cover; display: block; }

.notice__body { padding: clamp(1.5rem, 4vw, 2.25rem); }
.notice__eyebrow {
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-ink);
}
.notice__pulse {
  width: 7px; height: 7px; border-radius: 50%; background: var(--accent-500);
  box-shadow: 0 0 0 0 rgba(242, 178, 51, 0.55);
  animation: notice-pulse 2s var(--ease-out) infinite;
}
@keyframes notice-pulse {
  70% { box-shadow: 0 0 0 9px rgba(242, 178, 51, 0); }
  100% { box-shadow: 0 0 0 0 rgba(242, 178, 51, 0); }
}
.notice__title {
  margin-top: 0.6rem; font-size: var(--fs-xl); line-height: var(--lh-snug);
  letter-spacing: var(--tracking-tight); text-wrap: balance;
}
.notice__lede {
  margin-top: 0.7rem; color: var(--text-muted); font-size: var(--fs-sm);
  line-height: 1.6; text-wrap: pretty;
}
.notice__cta { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 1.5rem; }
.notice__cta .btn { flex: 1 1 auto; }

/* Side by side once there is room for the image to be worth its space. */
@media (min-width: 720px) {
  .notice--framed .notice__inner {
    display: grid; grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr);
    align-items: stretch;
  }
  .notice--framed .notice__media { aspect-ratio: auto; height: 100%; }
  .notice--framed .notice__body { display: flex; flex-direction: column; justify-content: center; }
}
@media (max-width: 480px) {
  .notice { border-radius: var(--r-lg); }
  .notice__cta .btn { flex: 1 1 100%; }
}

@media (prefers-reduced-motion: reduce) {
  /* Keep the fade, drop the movement - and stop the dot pulsing. */
  .notice, .notice[open] { transform: none; }
  .notice__pulse { animation: none; }
}

/* ── Loading overlay ────────────────────────────────────────────────── */
#site-loader {
  position: fixed; inset: 0; z-index: var(--z-loader);
  display: grid; place-items: center;
  /* Painted in the viewer's theme. It used to be brand-900 always, which
     meant a light-mode visitor got a full-screen dark flash before the
     site they asked for. */
  background: var(--loader-bg);
  transition: opacity var(--dur-slow) var(--ease-out), visibility var(--dur-slow);
}
#site-loader.is-done { opacity: 0; visibility: hidden; pointer-events: none; }
/* Repeat views inside a session skip it outright - see the head script. */
html.no-loader #site-loader { display: none; }
/* The JS failsafe cannot fire if app.js is the thing that failed to load, and
   a full-screen overlay with no way out is the worst outcome on this page.
   This one runs on the CSS timeline, so it clears regardless. */
#site-loader { animation: loader-failsafe 1ms linear 6s forwards; }
@keyframes loader-failsafe { to { opacity: 0; visibility: hidden; pointer-events: none; } }
.loader { display: flex; flex-direction: column; align-items: center; gap: 1.5rem; }

/* The Lottie sits where the inline mark does; whichever is live is the one
   shown. `has-lottie` lands only after the player has actually rendered, so
   a failed download leaves the inline SVG visible rather than an empty box. */
.loader__lottie { width: 148px; height: 148px; display: none; }
#site-loader.has-lottie .loader__lottie { display: block; }
#site-loader.has-lottie .loader__mark { display: none; }

/* ── The mark, routed ───────────────────────────────────────────────────
   953 is the measured length of the trace. The blue crossbar begins at 344
   along it and ends at 563, which is where the crossbar's own delay and
   duration below come from - they are not eyeballed.

   Everything routes at a constant speed: `linear`, not an ease. A signal
   travelling a wire does not accelerate, and an eased draw would put the
   navy trace and the blue crossbar out of step with each other.
   -------------------------------------------------------------------- */
.loader__mark { width: 148px; height: 148px; overflow: visible; }
.loader__mark .mark__trace { stroke: var(--mark-navy, #0b1f44); }
.loader__mark .mark__blue  { stroke: var(--mark-blue, #2563b3); }
.loader__mark .mark__navy  { fill:   var(--mark-navy, #0b1f44); }
.loader__mark .mark__amber { fill:   var(--mark-amber, #f2b233); }

.mark__trace, .mark__blue, .mark__pulse {
  fill: none;
  stroke-width: 58;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The routing. Its round cap is the moving point - nothing else needed. */
.mark__trace {
  stroke-dasharray: 953;
  stroke-dashoffset: 953;
  animation: route 1200ms linear 180ms forwards;
}
@keyframes route { to { stroke-dashoffset: 0; } }

/* 344/953 of the way along, for 219/953 of the duration. */
.mark__blue {
  stroke-dasharray: 219;
  stroke-dashoffset: 219;
  animation: route-blue 276ms linear 613ms forwards;
}
@keyframes route-blue { to { stroke-dashoffset: 0; } }

/* The source. It is there before the trace leaves it, because that is what
   the mark says: the amber node is where the signal starts. */
.mark__amber {
  transform-origin: 388px 154px;
  animation: node-land 460ms var(--ease-out) both;
}
/* The destination, landing as the routing arrives. */
.mark__navy {
  transform-origin: 109px 390px;
  animation: node-land 420ms var(--ease-out) 1290ms both;
}
@keyframes node-land {
  0%   { opacity: 0; transform: scale(0.4); }
  64%  { opacity: 1; transform: scale(1.08); }
  100% { opacity: 1; transform: scale(1); }
}

/* Pulses running the finished line. One trace-length per cycle, so the
   loop closes on itself with no seam. */
.mark__pulse {
  stroke: var(--mark-pulse, #d68f16);
  stroke-dasharray: 250 703;
  stroke-dashoffset: 953;
  opacity: 0;
  animation: pulse-run 1400ms linear 1560ms infinite,
             pulse-in 320ms var(--ease-out) 1460ms forwards;
}
@keyframes pulse-run { to { stroke-dashoffset: 0; } }
@keyframes pulse-in { to { opacity: 1; } }

/* The loader paints its own brand-900 canvas, so it keeps the bright amber. */
.loader__label {
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.32em;
  text-transform: uppercase; color: var(--accent-ink);
  animation: loader-label 600ms var(--ease-out) 900ms both;
}
@keyframes loader-label { from { opacity: 0; transform: translateY(6px); } }

/* Route-change progress bar */
#route-progress {
  position: fixed; top: 0; inset-inline-start: 0; height: 2px; width: 0;
  background: var(--grad-accent); z-index: 10000;
  transition: width 240ms var(--ease-out), opacity 300ms;
  box-shadow: 0 0 12px rgba(242,178,51,.8);
}

/* ── Utilities ──────────────────────────────────────────────────────── */
.mt-0 { margin-top: 0; } .mt-2 { margin-top: var(--sp-2); } .mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); } .mt-5 { margin-top: var(--sp-5); } .mt-6 { margin-top: var(--sp-6); }
.mt-7 { margin-top: var(--sp-7); } .mt-8 { margin-top: var(--sp-8); }
.mb-0 { margin-bottom: 0; } .mb-3 { margin-bottom: var(--sp-3); } .mb-4 { margin-bottom: var(--sp-4); }
.mb-5 { margin-bottom: var(--sp-5); } .mb-6 { margin-bottom: var(--sp-6); }
.hide { display: none !important; }
@media (max-width: 760px) { .hide-sm { display: none !important; } }
@media (min-width: 761px) { .show-sm { display: none !important; } }
.full { width: 100%; }
.rel { position: relative; }
.of-hidden { overflow: hidden; }
.no-scroll { overflow: hidden; }

/* ══════════════════════════════════════════════════════════════════════
   LOADING STATES
   Three patterns, each matched to the wait it covers - the split the
   research consistently lands on:

     skeleton     content arriving into a known layout (images, media)
     spinner      a short blocking action (form submit) - see .btn.is-loading
     progress bar a navigation, where the wait has a start and an end

   A spinner is the wrong tool for content: it says "something is happening"
   without saying what will be there, and it tests as slower than a skeleton
   even when the wait is identical.
   ══════════════════════════════════════════════════════════════════════ */
.skeleton,
.card-media:not(.is-loaded),
.media-frame:not(.is-loaded) {
  position: relative;
  overflow: hidden;
  background: var(--surface-strong);
}
/* The sweep runs left-to-right: a loading motion that travels the way the
   language is read tests as faster than one that pulses in place. */
.skeleton::after,
.card-media:not(.is-loaded)::after,
.media-frame:not(.is-loaded)::after {
  content: "";
  position: absolute; inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg,
              transparent, color-mix(in srgb, var(--text) 7%, transparent), transparent);
  animation: skeleton-sweep 1.4s linear infinite;
}
@keyframes skeleton-sweep { to { transform: translateX(100%); } }

/* Images fade in over their own skeleton rather than snapping in. */
.card-media img,
.media-frame img { opacity: 0; transition: opacity var(--dur) var(--ease-out); }
.card-media.is-loaded img,
.media-frame.is-loaded img { opacity: 1; }

.skeleton--text { height: 0.85em; border-radius: 4px; margin-block: 0.35em; }
.skeleton--line { height: 1px; }

@media (prefers-reduced-motion: reduce) {
  .skeleton::after,
  .card-media:not(.is-loaded)::after,
  .media-frame:not(.is-loaded)::after { animation: none; }
}

/* ── Notification rows ──────────────────────────────────────────────────
   Read and unread used to differ by `opacity:.6`, which reads as "disabled"
   rather than "you have seen this" and left no marker for the ones you have
   not. A dot carries the state instead, so the text stays at full contrast
   whichever it is. */
.note { align-items: flex-start; }
.note__dot {
  flex: 0 0 auto; width: 7px; height: 7px; margin-top: 0.42rem;
  border-radius: 50%; background: transparent;
}
.note--unread .note__dot { background: var(--accent-500); }
.note__text { flex: 1; min-width: 0; }
.note__title {
  display: block; font-size: var(--fs-sm); font-weight: 500;
  color: var(--text-muted);
}
.note--unread .note__title { font-weight: 650; color: var(--text); }
.note__time { display: block; }

/* ── Notification panel ─────────────────────────────────────────────────
   The panel had no ceiling, so an account with thirty unread notifications
   rendered a dropdown taller than the window with its oldest entries below
   the bottom edge and no way to scroll to them. The head stays put and the
   list scrolls beneath it. */
.dropdown__panel--notes {
  width: min(360px, calc(100vw - 2rem));
  max-height: min(70vh, 460px);
  display: flex; flex-direction: column;
  padding-block-end: 0.35rem;
}
.dropdown__panel--notes .dropdown__head {
  flex: 0 0 auto;
  margin-bottom: 0.25rem;
}
.notes__list {
  flex: 1 1 auto;
  overflow-y: auto;
  /* Reaching the end of this list must not start scrolling the page
     behind it — that is how a dropdown closes itself under the cursor. */
  overscroll-behavior: contain;
  scrollbar-width: thin;
}
.notes__badge {
  display: inline-block; margin-inline-start: 0.15rem;
  padding: 0.02rem 0.32rem; border-radius: var(--r-pill);
  background: var(--accent-500); color: var(--brand-900);
  font-size: 0.62rem; font-weight: 700; font-variant-numeric: tabular-nums;
  vertical-align: 1px;
}

/* On a phone the dropdown is most of the screen, so it gets the room. */
@media (max-width: 560px) {
  .dropdown__panel--notes {
    position: fixed;
    inset-inline: 0.75rem; top: calc(var(--nav-h) + 0.5rem);
    width: auto; max-height: min(72vh, 520px);
  }
}

/* ── Theme change ───────────────────────────────────────────────────────
   A theme flip repaints every surface at once. Unsoftened it reads as a
   glitch rather than as something the user did.

   Two paths, matching app.js. Where the View Transitions API exists the
   browser crossfades a snapshot of the whole page — colours, shadows,
   borders and images together, on the compositor, for the cost of one
   composite. Everywhere else, `.theme-shifting` puts a transition on the
   colour properties for the length of the change and is then removed, so
   hovers and focus rings keep their own much faster timing. */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: var(--dur-theme, 300ms);
  animation-timing-function: var(--ease-out);
  /* Both halves are painted at once and cross-faded; without this the new
     page wipes over the old one from a corner. */
  mix-blend-mode: normal;
}

.theme-shifting,
.theme-shifting *,
.theme-shifting *::before,
.theme-shifting *::after {
  /* Named properties, never `all`: transitioning `all` here would also
     animate every transform and size on the page for a third of a second,
     and the page would appear to breathe on a colour change. */
  transition-property: background-color, background-image, border-color,
                       color, fill, stroke, box-shadow, outline-color !important;
  transition-duration: var(--dur-theme, 300ms) !important;
  transition-timing-function: var(--ease-out) !important;
  transition-delay: 0s !important;
}

/* The switch is a colour change, and colour changes survive reduced motion.
   What goes is the crossfade — the new theme simply arrives. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation: none; }
}

/* A delete control sits in a row of ordinary actions, so it stays quiet
   until it is reached for. Loud on the row, it draws the eye to the one
   button nobody should click by accident. */
.btn--quiet-danger { color: var(--text-dim); }
.btn--quiet-danger:hover {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
  background: var(--danger-bg);
}

/* The reader's own zone, shown beside a time the server rendered in Lagos.
   Secondary by design: the Lagos time is the fact, this is the courtesy. */
.local-time {
  display: inline-block;
  margin-inline-start: 0.4rem;
  font-size: 0.82em;
  color: var(--text-dim);
  white-space: nowrap;
}

/* ── Academy menu (desktop) ─────────────────────────────────────────────
   A menu rather than a page: the two products under it are different
   decisions, and naming both saves a visit to an overview to find out
   which is which. Anchored under its trigger at the left edge — it grows
   out of the word "Academy", not out of the far corner. */
.nav__menu { position: relative; }
.nav__link--menu {
  display: inline-flex; align-items: center; gap: 0.3rem;
  background: none; border: 0; cursor: pointer; font: inherit;
}
.nav__chev { transition: transform var(--dur-fast) var(--ease-out); }
.nav__menu.is-open .nav__chev { transform: rotate(180deg); }

.nav__panel {
  left: -0.5rem; right: auto;
  width: 19.5rem; padding: 0.4rem;
  transform-origin: top left;
}
.nav__menu.is-open .nav__panel { opacity: 1; visibility: visible; transform: none; }
/* Hover opens it too on a real pointer; focus-within keeps it open while
   keyboard users tab through the two entries. */
@media (hover: hover) and (pointer: fine) {
  .nav__menu:hover .nav__panel,
  .nav__menu:focus-within .nav__panel { opacity: 1; visibility: visible; transform: none; }
  .nav__menu:hover .nav__chev { transform: rotate(180deg); }
}

.nav__item {
  display: flex; gap: 0.75rem; align-items: flex-start;
  padding: 0.7rem 0.75rem; border-radius: var(--r-sm);
  color: var(--text); text-decoration: none;
  transition: background var(--dur-fast) var(--ease-out);
}
.nav__item:hover, .nav__item:focus-visible, .nav__item.is-current { background: var(--surface); }
.nav__item-icon {
  display: grid; place-items: center; flex-shrink: 0;
  width: 34px; height: 34px; border-radius: 9px;
  background: var(--surface-strong); color: var(--accent-ink);
}
.nav__item-text { display: grid; gap: 0.1rem; }
.nav__item-text strong { font-size: var(--fs-sm); font-weight: 600; }
.nav__item-text small { font-size: var(--fs-xs); color: var(--text-muted); line-height: 1.4; }
.nav__panel-foot {
  display: flex; align-items: center; gap: 0.35rem;
  margin: 0.3rem 0.35rem 0.1rem; padding: 0.6rem 0.4rem 0.35rem;
  border-top: 1px solid var(--line);
  font-size: var(--fs-xs); color: var(--text-muted); text-decoration: none;
}
.nav__panel-foot:hover { color: var(--accent-ink); }

/* ── Academy group (mobile drawer) ────────────────────────────────────── */
.drawer__group { border-bottom: 1px solid var(--line); }
.drawer__link--group { cursor: pointer; list-style: none; border-bottom: 0; }
.drawer__link--group::-webkit-details-marker { display: none; }
.drawer__chev { transition: transform var(--dur-fast) var(--ease-out); }
.drawer__group[open] .drawer__chev { transform: rotate(180deg); }
.drawer__sublink {
  display: grid; gap: 0.1rem;
  padding: 0.65rem 0.25rem 0.65rem 1rem;
  margin-inline-start: 0.25rem;
  border-inline-start: 2px solid var(--line);
  color: var(--text); text-decoration: none;
}
.drawer__sublink:last-child { margin-bottom: 0.7rem; }
.drawer__sublink strong { font-size: 1rem; font-weight: 600; }
.drawer__sublink small { font-size: var(--fs-xs); color: var(--text-muted); }
.drawer__sublink:hover { border-inline-start-color: var(--accent-500); }
.drawer__sublink--quiet { font-size: var(--fs-sm); color: var(--text-muted); }
