/* Digital Discovery Group — component layer.
 *
 * Authored, not generated. Pairs with design-system.css (which IS generated,
 * from the bundle's tokens/). Load order matters: tokens first, this second.
 *
 * WHY THIS FILE EXISTS IN THIS SHAPE
 * ----------------------------------
 * The design system ships 48 React components whose styling lives entirely in
 * inline `style={{…}}` objects, with hover and press held in React.useState.
 * That shape cannot ship here — the site is Astro with no React renderer — and
 * it should not, even if it could:
 *
 *   · Inline styles put every declaration in the HTML of every page, so nothing
 *     caches and the same button costs bytes on all 54 pages.
 *   · Hover in useState means no hover before hydration, and no hover at all on
 *     a static page. As CSS `:hover` it works with JavaScript switched off.
 *   · Astro's scoped <style> would emit 48 near-identical per-page bundles.
 *
 * So each component's VARIANTS / SIZES / HOVER maps are transcribed here as
 * flat classes, and a thin .astro component maps props to class names. Where
 * this file departs from the prototype, the reason is in a comment next to it.
 *
 * CONVENTIONS
 *   .ddg-x            block
 *   .ddg-x--variant   variant, from the component's VARIANTS map
 *   .ddg-x--sm|md|lg  size, from its SIZES map
 *   .ddg-x__part      internal element
 *   [data-*]          state the server knows (active, selected, invalid)
 *
 * Nothing here uses !important, and nothing sets a colour that is not a token.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   ACTIONS
   ═══════════════════════════════════════════════════════════════════════════ */

/* Button ─────────────────────────────────────────────────────────────────── */

.ddg-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-family: var(--font-sans);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
  text-decoration: none;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  transition: var(--transition-control), transform var(--duration-instant) var(--ease-standard);
}

.ddg-btn:active:not(:disabled):not([aria-disabled='true']) {
  transform: translateY(1px);
}

.ddg-btn--full {
  width: 100%;
}

/* Sizes — padding / font-size / min-height / gap, verbatim from SIZES */
.ddg-btn--xs { padding: 5px 11px;  font-size: var(--text-micro);    min-height: 28px; gap: 6px; }
.ddg-btn--sm { padding: 7px 14px;  font-size: var(--text-caption);  min-height: 34px; gap: 7px; }
.ddg-btn--md { padding: 9px 18px;  font-size: var(--text-body-sm);  min-height: 40px; gap: 8px; }
.ddg-btn--lg { padding: 12px 24px; font-size: var(--text-body);     min-height: 46px; gap: 9px; }

/* Variants */
.ddg-btn--primary {
  background: var(--action-primary-bg);
  color: var(--action-primary-fg);
  border-color: var(--action-primary-bg);
  box-shadow: var(--highlight-top), var(--shadow-xs);
}
.ddg-btn--primary:hover:not(:disabled) {
  background: var(--action-primary-bg-hover);
  border-color: var(--action-primary-bg-hover);
  box-shadow: var(--highlight-top), var(--shadow-md);
}

.ddg-btn--accent {
  background: var(--action-accent-bg);
  color: var(--action-accent-fg);
  border-color: var(--action-accent-bg);
  box-shadow: var(--highlight-top), var(--glow-brand);
}
.ddg-btn--accent:hover:not(:disabled) {
  background: var(--action-accent-bg-hover);
  border-color: var(--action-accent-bg-hover);
}

.ddg-btn--secondary {
  background: var(--neutral-0);
  color: var(--action-secondary-fg);
  border-color: var(--action-secondary-border);
  box-shadow: var(--shadow-xs);
}
.ddg-btn--secondary:hover:not(:disabled) {
  background: var(--action-secondary-bg-hover);
  border-color: var(--slate-300);
  box-shadow: var(--shadow-sm);
}

.ddg-btn--ghost {
  background: transparent;
  color: var(--action-ghost-fg);
}
.ddg-btn--ghost:hover:not(:disabled) {
  background: var(--neutral-100);
  color: var(--ink-900);
}

/* `--text-inverse` and `--surface-ink`, not `--neutral-0` and `--ink-900`.
   Both of those re-point: in the dark theme `--neutral-0` becomes #0F2029 and
   `--ink-900` becomes #E9F1F6, so this white pill with ink text turned into a
   dark pill with pale text — invisible against the ink Panel it exists to sit
   on, while its own text/background ratio stayed high enough that no contrast
   check said a word.

   On 2026-08-12 a sweep found FIFTEEN more rules doing this, all of them
   `--neutral-0` standing in for white on a surface that never follows the
   theme: the inverse icon button, the inverse logo, the social hover, the
   inverted metric value and table body (both measured 1.01:1 — absent, not
   low), both inverted tab states, the toast dismiss, the chart legend value,
   Echo's initials, bubble, send button and inverted composer input, and the
   inverse upload zone. `--text-inverse` is #FFFFFF on both grounds and is what
   every one of them meant. */
.ddg-btn--inverse {
  background: var(--text-inverse);
  color: var(--surface-ink);
  border-color: var(--text-inverse);
  box-shadow: var(--shadow-sm);
}
.ddg-btn--inverse:hover:not(:disabled) {
  background: rgba(255,255,255,.88);
  border-color: rgba(255,255,255,.88);
}

/* --text-inverse, NOT --neutral-0. They are both white on paper, which is why
   this survived review, and they part company under [data-theme="dark"]:
   --neutral-0 re-points to #0F2029 and --text-inverse stays #FFFFFF.
   This button only ever sits on a dark ground — an ink Panel, a hero — and an
   ink Panel is dark in BOTH themes. So in dark mode the label was #0F2029 on
   #1B2F3F, measured at 1.21:1, which is not "low contrast": it is a button
   with no visible label, on the hero of Home and all five practice pages, for
   every visitor after about 6pm Sydney (the theme follows the business clock).

   Same trap as the glass cards on an ink panel. The rule generalises: on a
   surface that does not follow the theme, do not use a token that does. */
.ddg-btn--inverse-outline {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-inverse);
  border-color: rgba(255, 255, 255, 0.2);
  box-shadow: var(--highlight-top);
}
.ddg-btn--inverse-outline:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.13);
  border-color: rgba(255, 255, 255, 0.34);
}

/* Semantic actions. Green commits positive work; orange marks an important,
   non-destructive call to action; blue primary remains standard UX. */
.ddg-btn--cyan {
  background: var(--action-cyan-bg);
  color: var(--action-cyan-fg);
  border-color: var(--action-cyan-bg);
  box-shadow: var(--highlight-top), var(--glow-digital);
}
.ddg-btn--cyan:hover:not(:disabled) {
  background: var(--action-cyan-bg-hover);
  border-color: var(--action-cyan-bg-hover);
}

.ddg-btn--green {
  background: var(--action-green-bg);
  color: var(--action-green-fg);
  border-color: var(--action-green-bg);
  box-shadow: var(--highlight-top), var(--glow-green);
}
.ddg-btn--green:hover:not(:disabled) {
  background: var(--action-green-bg-hover);
  border-color: var(--action-green-bg-hover);
}

.ddg-btn--orange {
  background: var(--action-orange-bg);
  color: var(--action-orange-fg);
  border-color: var(--action-orange-bg);
  box-shadow: var(--highlight-top), var(--glow-retail);
}
.ddg-btn--orange:hover:not(:disabled) {
  background: var(--action-orange-bg-hover);
  border-color: var(--action-orange-bg-hover);
}

/* Glass. A lens, not a fill — it needs something behind it to refract, so it
   must never sit on flat paper. The specular arc is a ::before rather than the
   prototype's extra <span>, which keeps the markup to one element. */
.ddg-btn--glass,
.ddg-btn--glass-inverse {
  isolation: isolate;
}
.ddg-btn--glass::before,
.ddg-btn--glass-inverse::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.74;
  transition: opacity var(--duration-base) var(--ease-standard);
}
.ddg-btn--glass:hover::before,
.ddg-btn--glass-inverse:hover::before {
  opacity: 1;
}
.ddg-btn--glass > * ,
.ddg-btn--glass-inverse > * {
  position: relative;
  z-index: 1;
}

.ddg-btn--glass {
  background: var(--action-glass-bg);
  color: var(--action-glass-fg);
  border-color: var(--action-glass-border);
  box-shadow: var(--action-glass-shadow);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
}
.ddg-btn--glass::before { background: var(--glass-specular); }
.ddg-btn--glass:hover:not(:disabled) {
  background: var(--action-glass-bg-hover);
  border-color: #fff;
}

.ddg-btn--glass-inverse {
  background: var(--action-glass-bg-inverse);
  color: var(--action-glass-fg-inverse);
  border-color: var(--action-glass-border-inverse);
  box-shadow: var(--action-glass-shadow-inverse);
  backdrop-filter: var(--glass-blur-inverse);
  -webkit-backdrop-filter: var(--glass-blur-inverse);
}
.ddg-btn--glass-inverse::before { background: var(--glass-specular-inverse); }
.ddg-btn--glass-inverse:hover:not(:disabled) {
  background: var(--action-glass-bg-inverse-hover);
  border-color: rgba(255, 255, 255, 0.52);
}

/* Sheen on the filled variants. The prototype mounts an extra <span> on hover;
   an ::after that fades in does the same thing with no markup and no JS.
   Glass variants are excluded — they already own ::before. */
.ddg-btn--primary::after,
.ddg-btn--accent::after,
.ddg-btn--cyan::after,
.ddg-btn--green::after,
.ddg-btn--orange::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--sheen);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--duration-fast) var(--ease-standard);
}
.ddg-btn--primary:hover:not(:disabled)::after,
.ddg-btn--accent:hover:not(:disabled)::after,
.ddg-btn--cyan:hover:not(:disabled)::after,
.ddg-btn--green:hover:not(:disabled)::after,
.ddg-btn--orange:hover:not(:disabled)::after {
  opacity: 1;
}

/* Disabled and loading share one appearance, as they do in the prototype
   (`off = disabled || loading`). An <a> cannot be :disabled, so honour
   aria-disabled too. */
.ddg-btn:disabled,
.ddg-btn[aria-disabled='true'] {
  background: var(--action-disabled-bg);
  color: var(--action-disabled-fg);
  border-color: var(--action-disabled-bg);
  box-shadow: none;
  cursor: not-allowed;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.ddg-btn:disabled::after,
.ddg-btn[aria-disabled='true']::after,
.ddg-btn:disabled::before,
.ddg-btn[aria-disabled='true']::before {
  display: none;
}

.ddg-btn__spinner {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: var(--anim-spin);
}

.ddg-btn__icon {
  display: inline-flex;
  flex: 0 0 auto;
}

/* ArrowLink ──────────────────────────────────────────────────────────────── */

.ddg-arrowlink {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-sans);
  font-weight: var(--weight-semibold);
  font-size: var(--text-body-sm);
  letter-spacing: var(--tracking-wide);
  text-decoration: none;
  transition: var(--transition-control);
}
.ddg-arrowlink--sm { font-size: var(--text-caption); }

/* --text-link, not --ink-700. --ink-700 is the wordmark navy and it does NOT
   re-point in the dark theme, so the default arrow link stayed #184058 while
   the page around it went dark: 1.69:1, on Home's "More about us" and anywhere
   else this tone lands on a page a visitor reads after dark. --text-link is the
   token that exists for this — it re-points to #8CC6F0 — and an arrow link is a
   link, so it should have been reading the link colour regardless. */
.ddg-arrowlink--ink { color: var(--text-link); }
.ddg-arrowlink--ink:hover { color: var(--text-link-hover); }
/* --text-inverse for the same reason as .ddg-btn--inverse-outline above: this
   tone is only ever used on a ground that stays dark in both themes. */
.ddg-arrowlink--inverse { color: var(--text-inverse); }
.ddg-arrowlink--muted { color: var(--slate-500); }
.ddg-arrowlink--accent { color: var(--text-accent); }

/* The one glyph in the system that is not Lucide: a plain → (U+2192) set in the
   label's own type, so it tracks and colours with the text. */
.ddg-arrowlink__arrow {
  display: inline-flex;
  font-size: 1.05em;
  line-height: 1;
  transition: transform var(--duration-base) var(--ease-out);
}
.ddg-arrowlink:hover .ddg-arrowlink__arrow {
  transform: translateX(4px);
}

/* IconButton ─────────────────────────────────────────────────────────────── */

.ddg-iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  transition: var(--transition-control);
}
.ddg-iconbtn--pill { border-radius: var(--radius-pill); }

.ddg-iconbtn--xs { width: 26px; height: 26px; }
.ddg-iconbtn--sm { width: 30px; height: 30px; }
.ddg-iconbtn--md { width: 36px; height: 36px; }
.ddg-iconbtn--lg { width: 44px; height: 44px; }

.ddg-iconbtn--solid {
  background: var(--ink-700);
  color: var(--action-primary-fg);
  border-color: var(--ink-700);
  box-shadow: var(--highlight-top), var(--shadow-xs);
}
.ddg-iconbtn--solid:hover:not(:disabled) { background: var(--ink-900); }

/* `--action-accent-bg`, not the raw hue. It was already reading
   `--action-accent-bg-hover` on hover, so the resting and hover states came
   from two different places — and when the resting accent action moved to
   #2A66D6 on 2026-08-12 this control would have been the one accent button
   left on the old blue, a centimetre from one that had changed. */
.ddg-iconbtn--accent {
  background: var(--action-accent-bg);
  color: var(--action-accent-fg);
  border-color: var(--action-accent-bg);
  box-shadow: var(--highlight-top), var(--glow-brand);
}
.ddg-iconbtn--accent:hover:not(:disabled) { background: var(--action-accent-bg-hover); }

.ddg-iconbtn--outline {
  background: var(--neutral-0);
  color: var(--slate-600);
  border-color: var(--border-default);
  box-shadow: var(--shadow-xs);
}
.ddg-iconbtn--outline:hover:not(:disabled) {
  background: var(--neutral-100);
  border-color: var(--neutral-300);
  color: var(--ink-900);
}

.ddg-iconbtn--ghost {
  background: transparent;
  color: var(--slate-500);
}
.ddg-iconbtn--ghost:hover:not(:disabled) {
  background: var(--neutral-100);
  color: var(--ink-900);
}

.ddg-iconbtn--inverse {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-inverse);
  border-color: rgba(255, 255, 255, 0.16);
  box-shadow: var(--highlight-top);
}
.ddg-iconbtn--inverse:hover:not(:disabled) { background: rgba(255, 255, 255, 0.14); }

.ddg-iconbtn:disabled,
.ddg-iconbtn[aria-disabled='true'] {
  opacity: 0.45;
  cursor: not-allowed;
}

/* ═══════════════════════════════════════════════════════════════════════════
   SURFACES
   ═══════════════════════════════════════════════════════════════════════════ */

/* Card ───────────────────────────────────────────────────────────────────── */

.ddg-card {
  position: relative;
  display: flex;
  flex-direction: column;
  border-radius: var(--radius-lg);
  overflow: hidden;
  text-decoration: none;
  border: 1px solid transparent;
  transition: var(--transition-lift);
}

.ddg-card--paper {
  background: var(--surface-card);
  color: var(--text-primary);
  border-color: var(--border-default);
  box-shadow: var(--shadow-xs);
}
.ddg-card--sunken {
  background: var(--surface-sunken);
  color: var(--text-primary);
}
/* ── Always-dark containers pin the tone tokens ─────────────────────────────

   `--wash-*` and `--wash-*-fg` re-point under `[data-theme="dark"]`, which is
   right for a badge on a page. It is wrong for a badge inside `Panel tone="ink"`
   or `ConsoleShell`, which are dark on BOTH grounds: in the light theme that
   badge got an opaque pale wash and a paper-tuned foreground, dropped onto ink.

   Rather than give every tone component an `--invert` modifier and hope each
   call site remembers it, the CONTAINER pins the pair to its dark values. Any
   component placed inside one is then correct without knowing where it is —
   badge, practice chip, callout icon, delta, health pill, and anything added
   later.

   This is the only shape that scales. The alternative is one more `--invert`
   variant per component, and this repo has now paid for that lesson eight
   times: the same "theme-following token on a surface that does not follow the
   theme" bug has been found in `.ddg-btn--inverse-outline`, `.ddg-arrowlink--ink`,
   `.ddg-practicecard__more`, `ai.astro`'s numerals, the metric value, the
   inverted table, the health pill and the practice chip.

   ⚠ THE ONE THING THIS CANNOT DO is un-pin itself. A theme-following island
   nested INSIDE one of these — a paper Card inside an ink Panel — would inherit
   the ink cuts and put them on a light card. No page does that today (checked
   in the DOM across all 52 built pages, not by reading the markup), and if one
   ever does, `contrast:check` measures every page on both grounds and will say
   so. The fix at that point is a matching restore block on the light container,
   which needs `-paper` aliases the way this needs `-ink` ones. Do not add them
   speculatively; four extra tokens per hue for a case that does not exist is
   how a token layer becomes unreadable. */
.ddg-panel--ink,
.ddg-card--ink,
.ddg-card--slate,
.ddg-metric--invert,
.ddg-console {
  --wash-cyber: var(--wash-cyber-ink);
  --wash-it: var(--wash-it-ink);
  --wash-ai: var(--wash-ai-ink);
  --wash-search: var(--wash-search-ink);
  --wash-digital: var(--wash-digital-ink);
  --wash-retail: var(--wash-retail-ink);
  --wash-green: var(--wash-green-ink);
  --wash-amber: var(--wash-amber-ink);
  --wash-red: var(--wash-red-ink);

  --wash-cyber-fg: var(--wash-cyber-fg-ink);
  --wash-it-fg: var(--wash-it-fg-ink);
  --wash-ai-fg: var(--wash-ai-fg-ink);
  --wash-search-fg: var(--wash-search-fg-ink);
  --wash-digital-fg: var(--wash-digital-fg-ink);
  --wash-retail-fg: var(--wash-retail-fg-ink);
  --wash-green-fg: var(--wash-green-fg-ink);
  --wash-amber-fg: var(--wash-amber-fg-ink);
  --wash-red-fg: var(--wash-red-fg-ink);
}

/* An always-dark surface has to hand its foreground DOWN to headings, and
   `color: inherit` is not decoration.

   `design-system.css`'s base layer sets `h1,h2,h3,h4 { color: var(--text-primary) }`
   as an element rule. An element rule beats inheritance, so an `<h3>` dropped
   inside an ink Card took the theme's primary text colour and ignored the card
   entirely: **1.00:1 in the light theme, #0C2233 on #0C2233.** The card was
   doing its job — it sets `--text-inverse` — and the heading simply never
   asked. `SectionHeading` hides this whenever it is used, because it takes an
   `invert` prop; a plain heading has no such escape.

   The rule below is `inherit` rather than `--text-inverse` so it keeps working
   for any tone added later without needing a matching entry here. */
.ddg-card--ink :is(h1, h2, h3, h4, h5, h6),
.ddg-card--slate :is(h1, h2, h3, h4, h5, h6),
.ddg-panel--ink :is(h1, h2, h3, h4, h5, h6) {
  color: inherit;
}

.ddg-card--ink {
  background: var(--surface-ink);
  color: var(--text-inverse);
  border-color: rgba(255, 255, 255, 0.09);
  box-shadow: var(--highlight-top), var(--shadow-md);
}
.ddg-card--slate {
  background: var(--surface-slate);
  color: var(--text-inverse);
  border-color: rgba(255, 255, 255, 0.09);
  box-shadow: var(--highlight-top);
}
.ddg-card--outline {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--border-emphasis);
}
.ddg-card--glass {
  background: var(--surface-glass);
  color: var(--text-primary);
  border-color: var(--neutral-0);
  box-shadow: var(--shadow-md);
  /* --frost-blur, not a hand-written near-copy. The literal here was
     saturate(160%) where the token is 180% — two glass recipes that look the
     same in isolation and differ side by side. */
  backdrop-filter: var(--frost-blur);
  -webkit-backdrop-filter: var(--frost-blur);
}

/* Glow. Replaces the tone's own shadow, as in the prototype — dark tones keep
   their inner top highlight so they still read as lit. */
.ddg-card--glow-brand   { box-shadow: var(--glow-brand); }
.ddg-card--glow-ai      { box-shadow: var(--glow-ai); }
.ddg-card--glow-digital { box-shadow: var(--glow-digital); }
.ddg-card--glow-soft    { box-shadow: var(--glow-soft); }
.ddg-card--ink.ddg-card--glow-brand   { box-shadow: var(--highlight-top), var(--glow-brand); }
.ddg-card--ink.ddg-card--glow-ai      { box-shadow: var(--highlight-top), var(--glow-ai); }
.ddg-card--ink.ddg-card--glow-digital { box-shadow: var(--highlight-top), var(--glow-digital); }
.ddg-card--ink.ddg-card--glow-soft    { box-shadow: var(--highlight-top), var(--glow-soft); }
.ddg-card--slate.ddg-card--glow-brand   { box-shadow: var(--highlight-top), var(--glow-brand); }
.ddg-card--slate.ddg-card--glow-ai      { box-shadow: var(--highlight-top), var(--glow-ai); }
.ddg-card--slate.ddg-card--glow-digital { box-shadow: var(--highlight-top), var(--glow-digital); }
.ddg-card--slate.ddg-card--glow-soft    { box-shadow: var(--highlight-top), var(--glow-soft); }

.ddg-card--interactive { cursor: pointer; }
.ddg-card--interactive:hover,
a.ddg-card:hover {
  transform: translateY(-3px);
  border-color: var(--slate-300);
  box-shadow: var(--shadow-md);
}

/* The lift is motion, and someone who has asked for less of it should not get
   it. PracticeCard had this guard from the start and Card did not, so the two
   cards behaved differently under the same setting. */
@media (prefers-reduced-motion: reduce) {
  .ddg-card--interactive:hover,
  a.ddg-card:hover {
    transform: none;
  }
}
.ddg-card--ink.ddg-card--interactive:hover,
.ddg-card--slate.ddg-card--interactive:hover,
a.ddg-card--ink:hover,
a.ddg-card--slate:hover {
  border-color: rgba(255, 255, 255, 0.16);
  box-shadow: var(--highlight-top), var(--shadow-md);
}

/* Padding scale drives the body and header; the footer keeps its own 12px
   vertical, matching the prototype. */
.ddg-card--pad-none { --ddg-card-pad: 0; }
.ddg-card--pad-xs   { --ddg-card-pad: 12px; }
.ddg-card--pad-sm   { --ddg-card-pad: 16px; }
.ddg-card--pad-md   { --ddg-card-pad: 20px; }
.ddg-card--pad-lg   { --ddg-card-pad: 26px; }
.ddg-card--pad-xl   { --ddg-card-pad: 34px; }

.ddg-card__accent {
  height: 2px;
  width: 100%;
  flex: 0 0 auto;
}
.ddg-card__media {
  width: 100%;
  overflow: hidden;
  flex: 0 0 auto;
}
.ddg-card__media > img,
.ddg-card__media > picture > img {
  display: block;
  width: 100%;
  height: auto;
}
.ddg-card__header {
  padding: var(--ddg-card-pad, 20px) var(--ddg-card-pad, 20px) 0;
  flex: 0 0 auto;
}
.ddg-card__body {
  padding: var(--ddg-card-pad, 20px);
  display: flex;
  flex-direction: column;
  gap: 10px;
  flex: 1 1 auto;
  min-width: 0;
}
.ddg-card__footer {
  padding: 12px var(--ddg-card-pad, 20px);
  border-top: 1px solid var(--border-default);
  background: var(--neutral-50);
  flex: 0 0 auto;
}
.ddg-card--ink .ddg-card__footer,
.ddg-card--slate .ddg-card__footer {
  border-top-color: rgba(255, 255, 255, 0.09);
  background: rgba(255, 255, 255, 0.025);
}

/* Panel ──────────────────────────────────────────────────────────────────── */

.ddg-panel {
  position: relative;
  width: 100%;
  padding: var(--section-y) var(--gutter);
  overflow: hidden;
}

.ddg-panel--paper { background: var(--surface-page);      color: var(--text-primary); }
.ddg-panel--alt   { background: var(--surface-page-alt);  color: var(--text-primary); }
.ddg-panel--ink   { background: var(--surface-ink);       color: var(--text-inverse); }
.ddg-panel--deep  { background: var(--surface-ink-deep);  color: var(--text-inverse); }
.ddg-panel--wing  { background: var(--wing-gradient);     color: var(--text-inverse); }

.ddg-panel--rule { border-top: 1px solid var(--border-default); }
.ddg-panel--ink.ddg-panel--rule,
.ddg-panel--deep.ddg-panel--rule,
.ddg-panel--wing.ddg-panel--rule { border-top-color: rgba(255, 255, 255, 0.1); }

.ddg-panel__inner {
  position: relative;
  z-index: 1;
  margin: 0 auto;
  width: 100%;
  max-width: var(--container-max);
}
.ddg-panel--prose .ddg-panel__inner { max-width: var(--container-prose); }
.ddg-panel--wide  .ddg-panel__inner { max-width: var(--container-wide); }
.ddg-panel--full  .ddg-panel__inner { max-width: 100%; }

/* Decorative layers. Each is a <span aria-hidden> the component emits; the
   class picks the paint. Compose at most two fields plus one atmosphere. */
.ddg-panel__layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.ddg-panel__layer--grid       { background: var(--grid-lines);    background-size: var(--grid-size) var(--grid-size); }
.ddg-panel__layer--blueprint  { background: var(--bg-blueprint);  background-size: var(--bg-blueprint-size); }
.ddg-panel__layer--dots       { background: var(--bg-dots);       background-size: var(--bg-dots-size); }
.ddg-panel__layer--hatch      { background: var(--bg-hatch); }
.ddg-panel__layer--topo       { background: var(--bg-topo); }
/* `scan` has no light-tone value in the prototype's FIELDS map; on paper it is
   skipped rather than approximated. */
.ddg-panel--ink  .ddg-panel__layer--grid,
.ddg-panel--deep .ddg-panel__layer--grid,
.ddg-panel--wing .ddg-panel__layer--grid      { background-image: var(--grid-lines-inverse); }
.ddg-panel--ink  .ddg-panel__layer--blueprint,
.ddg-panel--deep .ddg-panel__layer--blueprint,
.ddg-panel--wing .ddg-panel__layer--blueprint { background-image: var(--bg-blueprint-inverse); }
.ddg-panel--ink  .ddg-panel__layer--dots,
.ddg-panel--deep .ddg-panel__layer--dots,
.ddg-panel--wing .ddg-panel__layer--dots      { background-image: var(--bg-dots-inverse); }
.ddg-panel--ink  .ddg-panel__layer--hatch,
.ddg-panel--deep .ddg-panel__layer--hatch,
.ddg-panel--wing .ddg-panel__layer--hatch     { background-image: var(--bg-hatch-inverse); }
.ddg-panel--ink  .ddg-panel__layer--topo,
.ddg-panel--deep .ddg-panel__layer--topo,
.ddg-panel--wing .ddg-panel__layer--topo      { background-image: var(--bg-topo-inverse); }
.ddg-panel--ink  .ddg-panel__layer--scan,
.ddg-panel--deep .ddg-panel__layer--scan,
.ddg-panel--wing .ddg-panel__layer--scan      { background-image: var(--bg-scanline-inverse); }

.ddg-panel__layer--mask-radial { mask-image: var(--mask-fade-radial); -webkit-mask-image: var(--mask-fade-radial); }
.ddg-panel__layer--mask-bottom { mask-image: var(--mask-fade-b);      -webkit-mask-image: var(--mask-fade-b); }
.ddg-panel__layer--mask-top    { mask-image: var(--mask-fade-t);      -webkit-mask-image: var(--mask-fade-t); }
.ddg-panel__layer--mask-sides  { mask-image: var(--mask-margins);     -webkit-mask-image: var(--mask-margins); }

/* Atmosphere sits outside the box so its edges never show. */
.ddg-panel__layer--air { inset: -12%; }
.ddg-panel__layer--mesh    { background: var(--mesh-paper);      animation: var(--anim-drift); }
.ddg-panel__layer--nebula  { background: var(--mesh-paper);      animation: var(--anim-drift); }
.ddg-panel__layer--aurora  { background: var(--bg-halo-paper);   animation: var(--anim-aurora); }
.ddg-panel__layer--halo    { background: var(--bg-halo-paper);   animation: var(--anim-bloom); }
.ddg-panel--ink  .ddg-panel__layer--mesh,
.ddg-panel--deep .ddg-panel__layer--mesh,
.ddg-panel--wing .ddg-panel__layer--mesh    { background: var(--mesh-ink); }
.ddg-panel--ink  .ddg-panel__layer--nebula,
.ddg-panel--deep .ddg-panel__layer--nebula,
.ddg-panel--wing .ddg-panel__layer--nebula  { background: var(--bg-nebula-ink); }
.ddg-panel--ink  .ddg-panel__layer--aurora,
.ddg-panel--deep .ddg-panel__layer--aurora,
.ddg-panel--wing .ddg-panel__layer--aurora  { background: var(--bg-aurora-ink); }
.ddg-panel--ink  .ddg-panel__layer--halo,
.ddg-panel--deep .ddg-panel__layer--halo,
.ddg-panel--wing .ddg-panel__layer--halo    { background: var(--bg-nebula-ink); }

.ddg-panel__layer--noise {
  background-image: var(--bg-noise);
  opacity: var(--bg-noise-opacity-paper);
  mix-blend-mode: multiply;
}
.ddg-panel--ink  .ddg-panel__layer--noise,
.ddg-panel--deep .ddg-panel__layer--noise,
.ddg-panel--wing .ddg-panel__layer--noise {
  opacity: var(--bg-noise-opacity-ink);
  mix-blend-mode: screen;
}

.ddg-panel__layer--vignette { background: var(--vignette-paper); }
.ddg-panel--ink  .ddg-panel__layer--vignette,
.ddg-panel--deep .ddg-panel__layer--vignette,
.ddg-panel--wing .ddg-panel__layer--vignette { background: var(--vignette-ink); }

/* Cursor-tracked light. --spot-x / --spot-y are set by script; both tokens
   carry a fallback so the layer is still valid before any pointer move. */
.ddg-panel__layer--spotlight {
  background: var(--bg-spotlight);
  transition: background var(--duration-slow) linear;
}
.ddg-panel--ink  .ddg-panel__layer--spotlight,
.ddg-panel--deep .ddg-panel__layer--spotlight,
.ddg-panel--wing .ddg-panel__layer--spotlight { background: var(--bg-spotlight-inverse); }

/* Beam is a dark-tone effect only, per the prototype's `beam && dark`. */
.ddg-panel--ink  .ddg-panel__layer--beam,
.ddg-panel--deep .ddg-panel__layer--beam,
.ddg-panel--wing .ddg-panel__layer--beam {
  bottom: auto;
  height: 34%;
  background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.05) 60%, transparent);
  animation: var(--anim-scan);
}

.ddg-panel__plume {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--plume-gradient);
  pointer-events: none;
}

/* Divider ────────────────────────────────────────────────────────────────── */

.ddg-divider {
  border: none;
  border-top: 1px solid var(--border-default);
  margin: 0;
  width: 100%;
}
.ddg-divider--strong  { border-top-color: var(--border-emphasis); }
.ddg-divider--inverse { border-top-color: rgba(255, 255, 255, 0.12); }

.ddg-divider-labelled {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
}
.ddg-divider-labelled__label {
  flex: 0 0 auto;
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.ddg-divider-labelled__rule {
  flex: 1 1 auto;
  height: 1px;
  background: var(--border-default);
}
.ddg-divider-labelled--strong .ddg-divider-labelled__rule { background: var(--border-emphasis); }
.ddg-divider-labelled--inverse .ddg-divider-labelled__label { color: var(--text-inverse-muted); }
.ddg-divider-labelled--inverse .ddg-divider-labelled__rule  { background: rgba(255, 255, 255, 0.12); }

.ddg-divider-v {
  display: inline-block;
  width: 1px;
  align-self: stretch;
  background: var(--border-default);
}
.ddg-divider-v--inverse { background: rgba(255, 255, 255, 0.12); }

/* ═══════════════════════════════════════════════════════════════════════════
   CONTENT
   ═══════════════════════════════════════════════════════════════════════════ */

/* Eyebrow ────────────────────────────────────────────────────────────────── */

/* On 2026-08-12 eleven declarations in this file took `--slate-400` as a TEXT
   colour. `--text-tertiary` pointed at the same rung, so they all rendered
   identically and looked like a style choice rather than a bypass — until the
   semantic token was corrected for the light ground and eleven places did not
   move with it. The eyebrow is the loudest: nineteen instances across sixteen
   pages, still at 4.42:1 after every other tertiary reference had been fixed.
   The raw ramps are for surfaces that must NOT follow the theme; foreground
   text takes a semantic token, so that a decision about text can be made in
   one place. All eleven now do. */
.ddg-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.ddg-eyebrow--inverse { color: var(--text-inverse-muted); }
.ddg-eyebrow--brand   { color: var(--accent-blue); }

.ddg-eyebrow__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--slate-400);
}
.ddg-eyebrow__dot--cyber   { background: var(--practice-cyber); }
.ddg-eyebrow__dot--it      { background: var(--practice-it); }
.ddg-eyebrow__dot--ai      { background: var(--practice-ai); }
.ddg-eyebrow__dot--search  { background: var(--practice-search); }
.ddg-eyebrow__dot--digital { background: var(--practice-digital); }
.ddg-eyebrow__dot--retail  { background: var(--practice-retail); }

/* SectionHeading ─────────────────────────────────────────────────────────── */

.ddg-heading {
  display: flex;
  gap: 28px;
  align-items: flex-end;
  justify-content: space-between;
  flex-wrap: wrap;
}
.ddg-heading__group {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-width: var(--measure-lead);
  align-items: flex-start;
}
.ddg-heading--center {
  text-align: center;
}
.ddg-heading--center .ddg-heading__group {
  align-items: center;
  margin: 0 auto;
}
.ddg-heading__title {
  font-family: var(--font-display);
  font-size: var(--text-display-3);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  font-weight: var(--weight-display);
  color: var(--text-primary);
  margin: 0;
}
.ddg-heading--sm .ddg-heading__title { font-size: var(--text-h1); }
.ddg-heading--lg .ddg-heading__title { font-size: var(--text-display-2); }
.ddg-heading__lead {
  font-size: var(--text-lead);
  line-height: var(--leading-snug);
  font-weight: var(--weight-regular);
  color: var(--text-secondary);
  max-width: var(--measure-lead);
  margin: 0;
}
.ddg-heading__action {
  flex: 0 0 auto;
  padding-bottom: 4px;
}
.ddg-heading--invert .ddg-heading__title { color: var(--text-inverse); }
.ddg-heading--invert .ddg-heading__lead  { color: var(--text-inverse-muted); }

/* The gap down to whatever the heading is introducing. 40px is the system's
   own figure — the prototype sets `marginTop:40px` on the block after every
   SectionHeading — but leaving it to the caller meant it was simply forgotten,
   and a measured pass over the built pages found every heading in a panel's
   content column sitting flush (0px) against the grid beneath it.

   A DEFAULT, not a ruling. Both rules are held at zero specificity by
   `:where()`, so any page that has already decided its own spacing keeps it —
   `/learn`'s `.ln__dashhead` at 24px and `/blog`'s `.essays__head` at 32px are
   deliberate and still win. Written bare these selectors weigh (0,3,0), which
   beats every page style on the site (`is:global` is (0,1,0), Astro-scoped at
   most (0,2,0)) and would have overridden both without a word.

   Scoped to the panel's content column because that column is block flow, so
   the margin also COLLAPSES with any margin-top the page sets on the block
   below — max(40px, that), never the sum.

   `:not(:last-child)` does more than trim a trailing margin off the last block
   in a panel: it is also what spares the specimen pages. `design-system.astro`'s
   `.spec` IS a direct child of this column and DOES hold a heading, so it would
   otherwise match — it is excluded because it is its panel's only child, and its
   own `gap: var(--space-8)` goes on governing.

   Two rule blocks rather than one selector list, and that is load-bearing: a
   selector list is non-forgiving, so a browser without `:has()` would throw away
   the plain rule along with the `:has()` one and take every heading on the site
   back to 0px. Split, only the wrapper case degrades. */
:where(.ddg-panel__inner > .ddg-heading:not(:last-child)) {
  margin-bottom: var(--space-10);
}
/* The wrappers — a reveal hook on the homepage, a heading paired with a note on
   /learn. Those are handles, not boxes, so the gap comes off the wrapper. */
:where(.ddg-panel__inner > *:not(:last-child):has(> .ddg-heading)) {
  margin-bottom: var(--space-10);
}

/* Stat ───────────────────────────────────────────────────────────────────── */

.ddg-stat {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ddg-stat--center { text-align: center; }

.ddg-stat__rule {
  width: 24px;
  height: 2px;
  margin-bottom: 4px;
  border-radius: 1px;
  align-self: flex-start;
  background: var(--slate-300);
}
.ddg-stat--center .ddg-stat__rule { align-self: center; }

.ddg-stat__row {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
}
.ddg-stat--center .ddg-stat__row { justify-content: center; }

.ddg-stat__value {
  font-family: var(--font-display);
  font-size: var(--text-display-3);
  line-height: 1;
  font-weight: var(--weight-display);
  letter-spacing: var(--tracking-display);
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
.ddg-stat__label {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-medium);
  color: var(--slate-500);
}
.ddg-stat__detail {
  font-size: var(--text-caption);
  line-height: var(--leading-snug);
  color: var(--text-tertiary);
  max-width: 30ch;
}
.ddg-stat--invert .ddg-stat__value  { color: var(--text-inverse); }
.ddg-stat--invert .ddg-stat__label  { color: var(--text-inverse-muted); }
.ddg-stat--invert .ddg-stat__detail { color: rgba(255, 255, 255, 0.5); }

/* Delta. It used to hard-code #4E8A38 for "up" and take `--accent-red` for
   "down" — the first because `--accent-green` fails on paper, which was right
   as far as it went. Neither had an answer for the dark ground: on a MetricTile
   or a console card those two measured 3.16:1 to 4.19:1, thirty instances of
   it. `--wash-green-fg` / `--wash-red-fg` carry both, and an always-dark
   container pins them to the ink cuts, so the same declaration is correct on a
   paper card, a dark card and the console. */
.ddg-delta {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: 500;
  white-space: nowrap;
}
.ddg-delta--up   { color: var(--wash-green-fg); }
.ddg-delta--down { color: var(--wash-red-fg); }

/* Quote ──────────────────────────────────────────────────────────────────── */

.ddg-quote {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 18px;
  max-width: var(--measure-prose);
}
.ddg-quote__text {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-h1);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}
.ddg-quote--sm .ddg-quote__text { font-size: var(--text-h2); }
.ddg-quote--lg .ddg-quote__text { font-size: var(--text-display-3); }
.ddg-quote__caption {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  color: var(--text-tertiary);
}
.ddg-quote__caption::before {
  content: '';
  width: 20px;
  height: 1px;
  flex: 0 0 auto;
  background: var(--slate-300);
}
/* The name is `--caption`, the role is `--role`, and the role is the half that
   says whether the quote is worth anything — "Founder, Dukes Matrix; Ex-Google"
   is the credential. It took `--slate-300` raw, which is 3.21:1 on white: below
   AA, on `/recommendations`, which is live and indexed. It is furniture-weight
   text, so it takes the furniture token rather than a lighter one of its own. */
.ddg-quote__role {
  font-weight: var(--weight-regular);
  color: var(--text-tertiary);
}
.ddg-quote--invert .ddg-quote__text    { color: var(--text-inverse); }
.ddg-quote--invert .ddg-quote__caption { color: var(--text-inverse-muted); }
.ddg-quote--invert .ddg-quote__caption::before { background: rgba(255, 255, 255, 0.34); }
/* 50% white is 3.74:1 on ink. `--text-inverse-muted` is the system's answer to
   exactly this question and is the only muted value that survives a surface
   which does not follow the theme. */
.ddg-quote--invert .ddg-quote__role    { color: var(--text-inverse-muted); }

/* ArticleCard ────────────────────────────────────────────────────────────── */

.ddg-article {
  display: flex;
  flex-direction: column;
  text-decoration: none;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  background: var(--surface-card);
  overflow: hidden;
  box-shadow: var(--shadow-xs);
  transition: var(--transition-lift);
}
.ddg-article:hover {
  border-color: var(--slate-300);
  box-shadow: var(--shadow-md);
  transform: translateY(-3px);
}
.ddg-article--row {
  flex-direction: row;
  gap: 22px;
}
.ddg-article__media { flex: 0 0 auto; overflow: hidden; }
.ddg-article--row .ddg-article__media { flex: 0 0 210px; }
.ddg-article__media img { display: block; width: 100%; height: 100%; object-fit: cover; }
.ddg-article__body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 20px;
  flex: 1 1 auto;
  min-width: 0;
}
.ddg-article--row .ddg-article__body { padding: 20px 20px 20px 0; }
.ddg-article__title {
  font-family: var(--font-display);
  font-size: var(--text-h2);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  font-weight: var(--weight-display);
  color: var(--text-primary);
  margin: 0;
}
.ddg-article__dek {
  font-size: var(--text-caption);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
  margin: 0;
}
.ddg-article__meta {
  margin-top: auto;
  padding-top: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-tertiary);
  overflow: hidden;
}
.ddg-article__meta > span { white-space: nowrap; }
/* `--text-link`, not `--accent-blue`. The raw hue is 4.18:1 on paper and 3.99:1
   on a dark card — 138 instances across thirteen pages, and it is the only
   affordance on an article card that says the card is a link. Same fix, same
   reason, as `.ddg-arrowlink--ink` and `.ddg-practicecard__more`: a brand hue
   is a decoration, and `--text-link` re-points with the ground while the raw
   ramp does not. */
.ddg-article__arrow {
  margin-left: auto;
  flex: 0 0 auto;
  padding-left: 8px;
  color: var(--text-link);
  transition: transform var(--duration-base) var(--ease-out);
}
.ddg-article:hover .ddg-article__arrow { transform: translateX(3px); }

/* ═══════════════════════════════════════════════════════════════════════════
   FEEDBACK
   ═══════════════════════════════════════════════════════════════════════════ */

/* Badge ──────────────────────────────────────────────────────────────────── */

/* Foreground values here are the prototype's TONES map. Several are darkened
   versions of the practice hue rather than the hue itself (#2A66D6 for cyber,
   #6748CE for AI, #0090A4 for digital, #D95E0C for retail, #4E8A38 for success,
   #8A6A00 for warning) because the bright plume values do not carry small text
   on an 8% wash. They are intentional. */
.ddg-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 9px;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  white-space: nowrap;
}
.ddg-badge__dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  flex: 0 0 auto;
  background: currentColor;
}
.ddg-badge__dot--pulse { animation: var(--anim-pulse); }

.ddg-badge--neutral { color: var(--slate-600);      background: var(--neutral-100); }
.ddg-badge--cyber   { color: var(--wash-cyber-fg);               background: var(--wash-cyber); }
.ddg-badge--it      { color: var(--wash-it-fg);               background: var(--wash-it); }
.ddg-badge--ai      { color: var(--wash-ai-fg);               background: var(--wash-ai); }
.ddg-badge--search  { color: var(--wash-search-fg);               background: var(--wash-search); }
.ddg-badge--digital { color: var(--wash-digital-fg);               background: var(--wash-digital); }
.ddg-badge--retail  { color: var(--wash-retail-fg);               background: var(--wash-retail); }
.ddg-badge--success { color: var(--wash-green-fg);               background: var(--status-success-wash); }
.ddg-badge--warning { color: var(--wash-amber-fg);               background: var(--status-warning-wash); }
.ddg-badge--danger  { color: var(--wash-red-fg);  background: var(--status-danger-wash); }
.ddg-badge--info    { color: var(--wash-digital-fg);               background: var(--status-info-wash); }

/* Outline variant keeps the tone's foreground and drops the wash. */
.ddg-badge--outline { background: transparent; border-color: currentColor; }

/* …which is fine on paper and 2.63:1 on ink, because every tone above is a cut
   darkened to clear its own wash. `inverse` is outline for a surface that stays
   dark in both themes: white label, the tone's hue kept on the border, which is
   the same trade `.ddg-practice--inverse` makes — the glyph or the frame
   decorates, the text carries. `--practice-*` rather than the darkened cut,
   because the darkened cut exists to sit on white. */
.ddg-badge--inverse { color: var(--text-inverse); }
.ddg-badge--inverse.ddg-badge--cyber   { border-color: var(--practice-cyber); }
.ddg-badge--inverse.ddg-badge--it      { border-color: var(--practice-it); }
.ddg-badge--inverse.ddg-badge--ai      { border-color: var(--practice-ai); }
.ddg-badge--inverse.ddg-badge--search  { border-color: var(--practice-search); }
.ddg-badge--inverse.ddg-badge--digital { border-color: var(--practice-digital); }
.ddg-badge--inverse.ddg-badge--retail  { border-color: var(--practice-retail); }
.ddg-badge--inverse.ddg-badge--neutral { border-color: var(--border-inverse-default); }

/* The status tones keep their hue on the border AND on the dot. A practice is
   an identity and the label already says which one, so white text loses
   nothing; a status is the meaning, and a white "Live" dot next to white text
   is a badge that has stopped saying anything. `--status-*` rather than the
   darkened text cuts above, for the same reason as the practices: those cuts
   exist to sit on white. */
.ddg-badge--inverse.ddg-badge--success { border-color: var(--status-success); }
.ddg-badge--inverse.ddg-badge--warning { border-color: var(--status-warning); }
.ddg-badge--inverse.ddg-badge--danger  { border-color: var(--status-danger); }
.ddg-badge--inverse.ddg-badge--info    { border-color: var(--status-info); }
.ddg-badge--inverse.ddg-badge--success .ddg-badge__dot { background: var(--status-success); }
.ddg-badge--inverse.ddg-badge--warning .ddg-badge__dot { background: var(--status-warning); }
.ddg-badge--inverse.ddg-badge--danger  .ddg-badge__dot { background: var(--status-danger); }
.ddg-badge--inverse.ddg-badge--info    .ddg-badge__dot { background: var(--status-info); }

/* Tag ────────────────────────────────────────────────────────────────────── */

.ddg-tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-default);
  background: var(--neutral-50);
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-medium);
  color: var(--slate-600);
  white-space: nowrap;
  transition: var(--transition-control);
}
a.ddg-tag:hover,
button.ddg-tag:hover {
  border-color: var(--slate-300);
  background: var(--neutral-100);
  color: var(--ink-900);
}
.ddg-tag--inverse {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(255, 255, 255, 0.14);
  color: var(--text-inverse-muted);
}

/* The selected state of a filter chip.
 *
 * `[data-ddg-filter-chip]` in design-system.js toggles `.is-on` and
 * `aria-pressed` together, but nothing defined `.is-on`, so which chip was
 * active reached assistive tech and nobody else: five identical buttons, one
 * of which was silently doing the filtering.
 *
 * --action-primary-* rather than --ink-700 and --neutral-0: both re-point
 * under [data-theme="dark"] as a pair (#2C6D94 on #FFFFFF), which is what
 * .ddg-btn--primary already relies on. The raw ramp does not — --neutral-0
 * becomes #0F2029 on dark, which is the 1.21:1 trap that cost three
 * components. */
.ddg-tag.is-on,
a.ddg-tag.is-on:hover,
button.ddg-tag.is-on:hover {
  background: var(--action-primary-bg);
  border-color: var(--action-primary-bg);
  color: var(--action-primary-fg);
}

/* Callout ────────────────────────────────────────────────────────────────── */

.ddg-callout {
  display: flex;
  gap: 12px;
  padding: 14px 16px;
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  border-left-width: 3px;
  font-size: var(--text-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-primary);
}
.ddg-callout__icon { flex: 0 0 auto; display: inline-flex; margin-top: 1px; }
.ddg-callout__body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.ddg-callout__title {
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-normal);
}

.ddg-callout--info    { background: var(--status-info-wash);    border-left-color: var(--status-info); }
.ddg-callout--success { background: var(--status-success-wash); border-left-color: var(--status-success); }
.ddg-callout--warning { background: var(--status-warning-wash); border-left-color: var(--status-warning); }
.ddg-callout--danger  { background: var(--status-danger-wash);  border-left-color: var(--status-danger); }
.ddg-callout--neutral { background: var(--surface-sunken);      border-left-color: var(--slate-300); }
.ddg-callout--info    .ddg-callout__icon { color: var(--wash-digital-fg); }
.ddg-callout--success .ddg-callout__icon { color: var(--wash-green-fg); }
.ddg-callout--warning .ddg-callout__icon { color: var(--wash-amber-fg); }
.ddg-callout--danger  .ddg-callout__icon { color: var(--wash-red-fg); }
.ddg-callout--neutral .ddg-callout__icon { color: var(--slate-500); }

/* Empty state. The thing a list says when it has nothing in it.
 *
 * NOT a Callout, though it was tempting. A Callout is a coloured aside about
 * something the reader should notice; an empty list is not an alert, it is the
 * page's ordinary content for a first-time visitor. Painting "No invoices yet"
 * in a status wash tells someone their account is broken.
 *
 * The site had six of these written six different ways as bare paragraphs in
 * --text-tertiary: "No bookings yet.", "No invoices yet.", "No documents here
 * yet." Each was true and none said what to do next, which is the only reason
 * an empty state exists. Two routes had none at all and rendered a heading
 * above nothing.
 *
 * Centred, because it is the only thing on that part of the page. Bordered
 * rather than filled, because a filled block reads as a card the reader is
 * meant to act on and there is nothing in it to act on. */
.ddg-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: var(--space-8) var(--space-5);
  border: 1px dashed var(--border-default);
  border-radius: var(--radius-lg);
  text-align: center;
}
.ddg-empty__icon {
  display: inline-flex;
  color: var(--text-tertiary);
}
.ddg-empty__title {
  margin: 0;
  font-size: var(--text-body);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  color: var(--text-primary);
}
.ddg-empty__body {
  margin: 0;
  max-width: 42ch;
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  color: var(--text-secondary);
}
.ddg-empty__action { margin-top: 6px; }

/* Inside a table or a bordered section the dashes double up on a border that
   is already there, so the variant drops its own. */
.ddg-empty--bare {
  border: 0;
  padding: var(--space-6) var(--space-4);
}

/* ═══════════════════════════════════════════════════════════════════════════
   BRAND
   ═══════════════════════════════════════════════════════════════════════════ */

/* Logo. The prototype composes the lockup from a raster mark plus live type.
   Here the wordmark is real vector (assets/brand/wordmark.svg, currentColor)
   and the crest is either the flat vector or the shaded WebP — see
   scripts/build-brand-assets.mjs for why both exist. */
.ddg-logo {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  border: none;
  color: var(--ink-700);
}
.ddg-logo--inverse { color: var(--text-inverse); }

.ddg-logo__mark {
  display: block;
  flex: 0 0 auto;
}

/* The flat crest and the wordmark are drawn in currentColor. An <img> is its
   own document and cannot inherit that, so they are painted as a currentColor
   fill through an SVG mask — same one cached file, and the mark now takes the
   colour of whatever it sits in. */
.ddg-logo__mark--flat,
.ddg-logo__wordmark {
  display: block;
  flex: 0 0 auto;
  background: currentColor;
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  -webkit-mask-size: contain;
}
.ddg-logo__mark--flat {
  mask-image: url('/assets/brand/wings-flat.svg');
  -webkit-mask-image: url('/assets/brand/wings-flat.svg');
}
.ddg-logo__wordmark {
  mask-image: url('/assets/brand/wordmark.svg');
  -webkit-mask-image: url('/assets/brand/wordmark.svg');
}
.ddg-logo__wordmark--tagline {
  mask-image: url('/assets/brand/wordmark-tagline.svg');
  -webkit-mask-image: url('/assets/brand/wordmark-tagline.svg');
}

/* Stacked is the default: crest above wordmark, centred. */
.ddg-logo--lockup {
  flex-direction: column;
  gap: 10px;
}
.ddg-logo--horizontal {
  gap: 14px;
}

/* Strapline. Four practices, as drawn in the artwork. If a fifth practice
   ships, this line and assets/brand/wordmark-tagline.svg both need reissuing —
   see docs/design-system-migration-plan-2026-08-09.md §10.1. */
.ddg-logo__strapline {
  font-family: var(--font-sans);
  font-weight: var(--weight-light);
  letter-spacing: var(--tracking-lockup);
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--slate-500);
}
.ddg-logo--inverse .ddg-logo__strapline { color: var(--text-inverse-muted); }

/* PracticeChip ───────────────────────────────────────────────────────────── */

.ddg-practice {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 12px 5px 10px;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-decoration: none;
  white-space: nowrap;
  transition: var(--transition-control);
}
.ddg-practice__icon { display: inline-flex; flex: 0 0 auto; }

/* The text colour is a cut of the practice hue, not the hue itself — none of
   the five clears 3:1 as text on its own wash at full strength.

   It used to be five hard-coded hex values here, four inherited from the
   prototype and inconsistent with each other, and cut for PAPER only. The
   washes re-point for dark and the foregrounds never did, so every chip on the
   site read between 2.34:1 and 3.70:1 at night — all five of them, since the
   system landed. `--wash-*-fg` now carries both grounds, each solved to ≥4.8:1
   against the ground it actually lands on, keeping the hue's H and S and moving
   only L. See scripts/build-design-tokens.mjs for the measurements. */
.ddg-practice--cyber   { color: var(--wash-cyber-fg); background: var(--wash-cyber);   }
.ddg-practice--it      { color: var(--wash-it-fg); background: var(--wash-it);      }
.ddg-practice--ai      { color: var(--wash-ai-fg); background: var(--wash-ai);      }
.ddg-practice--search  { color: var(--wash-search-fg); background: var(--wash-search);  }
.ddg-practice--digital { color: var(--wash-digital-fg); background: var(--wash-digital); }
.ddg-practice--retail  { color: var(--wash-retail-fg); background: var(--wash-retail);  }

a.ddg-practice:hover { border-color: currentColor; }

/* On ink, the LABEL is white and the hue moves to the glyph.

   This variant used to put the raw practice hue on the label, and the raw hues
   do not survive an ink ground any better than they survive their own wash:
   AI 2.79:1, cyber 3.28:1, retail 3.39:1, measured on the practice-page hero
   where every one of them ships. Only search and digital cleared, and those
   two only because they are the lightest.

   It is the same answer `.ddg-practicecard__more` reached on 2026-08-10, for
   the same reason: **the glyph identifies, the hue decorates.** There is no set
   of five hues that reads on both grounds, and the alternative — five extra
   lightened cuts, one per practice, for ink only — invents brand colour to
   solve a legibility problem the system already has a rule for. If those cuts
   are wanted they are Nigel's call, and they belong beside the open violet
   question rather than in a contrast pass.

   The icon keeps the hue. As a graphic accompanied by a text label saying the
   same thing, it is not carrying information on its own. */
.ddg-practice--inverse {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(255, 255, 255, 0.14);
  color: var(--text-inverse);
}
.ddg-practice--inverse.ddg-practice--cyber   .ddg-practice__icon { color: var(--practice-cyber); }
.ddg-practice--inverse.ddg-practice--it      .ddg-practice__icon { color: var(--practice-it); }
.ddg-practice--inverse.ddg-practice--ai      .ddg-practice__icon { color: var(--practice-ai); }
.ddg-practice--inverse.ddg-practice--search  .ddg-practice__icon { color: var(--practice-search); }
.ddg-practice--inverse.ddg-practice--digital .ddg-practice__icon { color: var(--practice-digital); }
.ddg-practice--inverse.ddg-practice--retail  .ddg-practice__icon { color: var(--practice-retail); }

/* SocialLinks ────────────────────────────────────────────────────────────── */

/* A <ul>, because it is a list of links and a screen reader should be told how
   many there are. The prototype uses role="list" on a <span> with role="listitem"
   children, which is the same thing with extra steps and no fallback. */
.ddg-social {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.ddg-social--bare { gap: 14px; }
.ddg-social__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--ddg-social-box, 44px);
  height: var(--ddg-social-box, 44px);
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  text-decoration: none;
  transition: var(--transition-control);
}
.ddg-social--circle .ddg-social__link { border-radius: 50%; }

.ddg-social--bare .ddg-social__link { color: var(--text-tertiary); }
.ddg-social--bare .ddg-social__link:hover { color: var(--text-primary); }

.ddg-social--outline .ddg-social__link {
  color: var(--text-secondary);
  border-color: var(--border-default);
}
.ddg-social--outline .ddg-social__link:hover {
  color: var(--text-primary);
  border-color: var(--border-emphasis);
  background: var(--surface-sunken);
}

.ddg-social--solid .ddg-social__link {
  color: var(--text-secondary);
  background: var(--surface-sunken);
}
.ddg-social--solid .ddg-social__link:hover {
  color: var(--text-primary);
  background: var(--surface-card);
  border-color: var(--border-emphasis);
}

.ddg-social--inverse .ddg-social__link {
  color: var(--text-inverse-muted);
  border-color: rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.06);
}
.ddg-social--inverse .ddg-social__link:hover {
  color: var(--text-inverse);
  border-color: rgba(255, 255, 255, 0.4);
  background: rgba(255, 255, 255, 0.12);
}

/* LogoWall ───────────────────────────────────────────────────────────────────

   A logo wall is a rhythm problem, not an image problem. Four marks drawn by
   four unrelated studios have to read as one row, and the only way that happens
   is if every one of them is set to the same OPTICAL height — not the same box,
   and certainly not the same width.

   So the height is the variable the wall controls (--ddg-wall-h), each logo
   carries a per-mark multiplier off it (--ddg-wall-k), and width is always
   auto. The multipliers are optical judgements, not arithmetic: a circular
   badge has to be taller than a wordmark to weigh the same, which is why
   Hawkesbury runs at 1.5.

   The prototype holds the hovered index in React.useState. Here it is :hover,
   which also means it works before — and without — any JavaScript. */

/* The heights are a range, not a number. Held at the desktop value a wall of
   four marks wraps to three ragged rows on a phone; a quarter smaller it is two
   even ones, and a client logo is proof rather than reading matter, so it can
   afford the loss. The floor is the size below which the finer wordmarks stop
   resolving. */
.ddg-wall {
  --ddg-wall-h: clamp(22px, 2.6vw, 26px);
  --ddg-wall-filter: grayscale(1);
  --ddg-wall-filter-filled: grayscale(1);
  --ddg-wall-rest: 0.62;
  --ddg-wall-rest-filled: 0.62;
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* Two different questions, one answer.
 *
 *   .ddg-wall--inverse   the wall is on a surface that is dark on BOTH grounds
 *                        — an ink or deep Panel.
 *   [data-theme="dark"]  the wall is on a surface that FOLLOWS the theme, and
 *                        the theme is currently dark.
 *
 * The second one is the one that bites. A paper Panel is `--surface-page`, and
 * `--surface-page` re-points to ink after dark — so a wall with no `tone` prop
 * at all, which looked right at midday, is four dark marks on a dark ground at
 * 8pm. Nothing catches it: the images load, the layout is correct, the page
 * returns 200, and `contrast:check` does not measure images. */
:root[data-theme='dark'] .ddg-wall,
.ddg-wall--inverse {
  --ddg-wall-filter: grayscale(1) brightness(0) invert(1);
  --ddg-wall-filter-filled: grayscale(1) brightness(1.45) contrast(0.92);
  --ddg-wall-rest: 0.52;
  --ddg-wall-rest-filled: 0.72;
}
.ddg-wall--sm { --ddg-wall-h: clamp(17px, 2vw, 20px); }
.ddg-wall--lg { --ddg-wall-h: clamp(26px, 3.4vw, 34px); }

.ddg-wall__label {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.ddg-wall--center .ddg-wall__label { text-align: center; }
.ddg-wall--inverse .ddg-wall__label { color: var(--text-inverse-muted); }

.ddg-wall__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: clamp(26px, 4vw, 56px);
  margin: 0;
  padding: 0;
  list-style: none;
}
.ddg-wall--center .ddg-wall__list { justify-content: center; }

/* `spread` asks for space-between, and space-between is only ever right on a
   row that does not wrap: the moment it does, every line but the last is pushed
   to the full width and the last is stranded hard left, which reads as a broken
   grid rather than as a wall. Four marks cannot fit one line on a phone at any
   height worth rendering, so below the width where they can, spread centres its
   wrapped rows instead. The tighter gap is what buys the single line back at
   the widths where one is possible. */
.ddg-wall--spread .ddg-wall__list {
  justify-content: center;
  gap: clamp(18px, 2vw, 32px);
}
@media (min-width: 860px) {
  .ddg-wall--spread .ddg-wall__list { justify-content: space-between; }
}

.ddg-wall__item {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  min-width: 0;
}
/* On a dark ground, grayscale alone leaves a dark mark on a dark ground, so the
   marks are knocked out white instead. A silhouette is the right answer for a
   single-colour wordmark and the wrong one for a filled badge — brightness(0)
   would collapse Hawkesbury's red field and its knocked-out white lettering
   into one solid blob — so the filled marks are lifted, not inverted. */
/* Height is authoritative and width follows it. There is no COMPONENT cap: the
   prototype's `max-width: 240px` is gone, because a cap is the one thing that
   quietly breaks the contract this whole block argues for — once a wide
   wordmark hits it the mark renders SHORTER than the ones beside it, and
   Decathlon already sits at ~215px of that 240. A wide mark is better left
   wide; the row wraps, which is a visible, correct degradation.

   `object-fit: contain` still has to stay, and it is not vestigial. The base
   reset in design-system.css sets `img,svg,video,canvas{max-width:100%}`, so
   the computed max-width here is 100% of the list, not `none` — this component
   cannot opt out of a cap, only out of its own. Without `contain`, a mark that
   reached that cap would be squashed by the default `fill` instead of
   letterboxed, and distorting a client's logo is the worse of the two failures.
   It takes an aspect ratio past ~10.8:1 to reach at 320px, so this is a guard
   for a case that does not exist yet rather than a rule doing work today.

   `transition` is spelled out rather than taking --transition-control, and that
   is not a stylistic choice: --transition-control covers color,
   background-color, border-color and box-shadow, and opacity is not among
   them. The hover below would have snapped instead of fading, and the
   component's own note would have promised a lift that never happened. Four
   other rules in this file (211, 265, 1725, 2178) write it out for the same
   reason. */
.ddg-wall__logo {
  display: block;
  width: auto;
  height: calc(var(--ddg-wall-h) * var(--ddg-wall-k, 1));
  object-fit: contain;
  filter: var(--ddg-wall-filter);
  opacity: var(--ddg-wall-rest);
  transition: opacity var(--duration-base) var(--ease-standard);
}
.ddg-wall__logo--filled {
  filter: var(--ddg-wall-filter-filled);
  opacity: var(--ddg-wall-rest-filled);
}
.ddg-wall__item:hover .ddg-wall__logo {
  opacity: 1;
}

/* The prototype also carries a display-serif fallback for a client whose image
   file is missing. There is no such case here: LogoWall.astro takes client KEYS
   off a closed table, so a mark either has a committed file or does not exist,
   and a rule that can never match is a rule nobody can verify. */

/* ═══════════════════════════════════════════════════════════════════════════
   PLATFORM
   ═══════════════════════════════════════════════════════════════════════════ */

/* MetricTile ─────────────────────────────────────────────────────────────── */

.ddg-metric {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px 18px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-card);
  border: 1px solid var(--border-default);
  box-shadow: var(--shadow-xs);
  transition: var(--transition-lift);
  /* Tone colour, set by the tone modifier and reused by the rule, icon and
     spark so they can never drift apart. */
  --ddg-metric-tone: var(--slate-400);
}
.ddg-metric:hover {
  border-color: var(--slate-300);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}
.ddg-metric::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 2px;
  background: var(--ddg-metric-tone);
  opacity: 0.5;
  transition: opacity var(--duration-base) var(--ease-standard);
}
.ddg-metric:hover::before { opacity: 0.9; }

.ddg-metric--primary { --ddg-metric-tone: var(--accent-blue); }
.ddg-metric--cyber   { --ddg-metric-tone: var(--practice-cyber); }
.ddg-metric--it      { --ddg-metric-tone: var(--practice-it); }
.ddg-metric--ai      { --ddg-metric-tone: var(--practice-ai); }
.ddg-metric--search  { --ddg-metric-tone: var(--practice-search); }
.ddg-metric--digital { --ddg-metric-tone: var(--practice-digital); }
.ddg-metric--retail  { --ddg-metric-tone: var(--practice-retail); }
.ddg-metric--success { --ddg-metric-tone: var(--accent-green); }
.ddg-metric--warning { --ddg-metric-tone: var(--accent-amber); }
.ddg-metric--danger  { --ddg-metric-tone: var(--accent-red); }

.ddg-metric__head { display: flex; align-items: center; gap: 8px; }
.ddg-metric__icon { display: inline-flex; color: var(--ddg-metric-tone); }
.ddg-metric__label {
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.ddg-metric__row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  column-gap: 6px;
  row-gap: 3px;
  min-width: 0;
}
.ddg-metric__value {
  font-family: var(--font-display);
  font-size: var(--text-h1);
  line-height: 1;
  font-weight: var(--weight-display);
  letter-spacing: var(--tracking-display);
  font-variant-numeric: tabular-nums;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text-primary);
}
.ddg-metric__unit {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  white-space: nowrap;
  color: var(--text-tertiary);
}
.ddg-metric__row .ddg-delta { margin-left: auto; flex: 0 0 auto; }
.ddg-metric__caption {
  font-size: var(--text-caption);
  line-height: var(--leading-snug);
  color: var(--text-tertiary);
}
.ddg-metric__spark {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 26px;
}
.ddg-metric__spark > i {
  flex: 1 1 0;
  background: var(--ddg-metric-tone);
  border-radius: 1px;
}

.ddg-metric--invert {
  background: rgba(255, 255, 255, 0.045);
  border-color: rgba(255, 255, 255, 0.1);
  box-shadow: var(--highlight-top);
}
.ddg-metric--invert:hover { border-color: rgba(255, 255, 255, 0.1); box-shadow: var(--highlight-top); }
.ddg-metric--invert .ddg-metric__label   { color: rgba(255, 255, 255, 0.52); }
.ddg-metric--invert .ddg-metric__value   { color: var(--text-inverse); }
.ddg-metric--invert .ddg-metric__unit,
.ddg-metric--invert .ddg-metric__caption { color: rgba(255, 255, 255, 0.5); }

/* Progress ───────────────────────────────────────────────────────────────── */

.ddg-progress {
  display: flex;
  flex-direction: column;
  gap: 7px;
  width: 100%;
  --ddg-progress-tone: var(--accent-blue);
}
.ddg-progress--cyber   { --ddg-progress-tone: var(--practice-cyber); }
.ddg-progress--it      { --ddg-progress-tone: var(--practice-it); }
.ddg-progress--ai      { --ddg-progress-tone: var(--practice-ai); }
.ddg-progress--search  { --ddg-progress-tone: var(--practice-search); }
.ddg-progress--digital { --ddg-progress-tone: var(--practice-digital); }
.ddg-progress--retail  { --ddg-progress-tone: var(--practice-retail); }
.ddg-progress--success { --ddg-progress-tone: var(--accent-green); }
.ddg-progress--warning { --ddg-progress-tone: var(--accent-amber); }
.ddg-progress--danger  { --ddg-progress-tone: var(--accent-red); }

.ddg-progress__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.ddg-progress__label {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}
.ddg-progress__hint {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-variant-numeric: tabular-nums;
  color: var(--text-tertiary);
}
.ddg-progress__track {
  position: relative;
  width: 100%;
  height: 6px;
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: var(--neutral-200);
}
.ddg-progress--sm .ddg-progress__track { height: 4px; }
.ddg-progress--lg .ddg-progress__track { height: 10px; }
.ddg-progress__fill {
  display: block;
  height: 100%;
  border-radius: var(--radius-pill);
  background: var(--ddg-progress-tone);
  box-shadow: 0 0 12px -2px var(--ddg-progress-tone);
  transition: width var(--duration-slow) var(--ease-out);
  /* Width is the one thing that must be inline — it is data, not design. */
}
.ddg-progress__fill--indeterminate {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 42%;
  animation: var(--anim-sweep);
}
.ddg-progress--invert .ddg-progress__label { color: rgba(255, 255, 255, 0.78); }
.ddg-progress--invert .ddg-progress__hint  { color: rgba(255, 255, 255, 0.5); }
.ddg-progress--invert .ddg-progress__track { background: rgba(255, 255, 255, 0.1); }

/* Tabs ───────────────────────────────────────────────────────────────────── */

.ddg-tabs {
  display: inline-flex;
  align-items: center;
  overflow-x: auto;
  max-width: 100%;
}
.ddg-tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  border: 1px solid transparent;
  background: transparent;
  color: var(--slate-500);
  transition: var(--transition-control);
}
.ddg-tabs__count {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  padding: 1px 5px;
  border-radius: var(--radius-pill);
  background: var(--neutral-200);
  color: var(--slate-600);
}

/* Segmented */
.ddg-tabs--segmented {
  gap: 2px;
  padding: 3px;
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  border: 1px solid var(--border-default);
}
.ddg-tabs--segmented .ddg-tabs__tab {
  padding: 6px 13px;
  border-radius: var(--radius-sm);
}
.ddg-tabs--segmented .ddg-tabs__tab:hover { background: rgba(255, 255, 255, 0.6); }
.ddg-tabs--segmented .ddg-tabs__tab[aria-selected='true'] {
  background: var(--neutral-0);
  box-shadow: var(--shadow-xs);
  color: var(--ink-900);
}

/* Underline */
.ddg-tabs--underline {
  gap: 22px;
  border-bottom: 1px solid var(--border-default);
}
.ddg-tabs--underline .ddg-tabs__tab {
  padding: 0 0 11px;
  border-radius: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}
.ddg-tabs--underline .ddg-tabs__tab[aria-selected='true'] {
  border-bottom-color: var(--accent-blue);
  color: var(--ink-900);
}

.ddg-tabs--invert.ddg-tabs--segmented {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.09);
}
.ddg-tabs--invert.ddg-tabs--underline { border-bottom-color: rgba(255, 255, 255, 0.1); }
.ddg-tabs--invert .ddg-tabs__tab { color: rgba(255, 255, 255, 0.58); }
.ddg-tabs--invert .ddg-tabs__tab:hover { background: rgba(255, 255, 255, 0.05); }
.ddg-tabs--invert.ddg-tabs--segmented .ddg-tabs__tab[aria-selected='true'] {
  background: rgba(255, 255, 255, 0.1);
  box-shadow: var(--highlight-top);
  color: var(--text-inverse);
}
.ddg-tabs--invert.ddg-tabs--underline .ddg-tabs__tab[aria-selected='true'] { color: var(--text-inverse); }
.ddg-tabs--invert .ddg-tabs__count {
  background: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.72);
}

/* CodeBlock ──────────────────────────────────────────────────────────────── */

.ddg-code {
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--surface-code);
  border: 1px solid rgba(255, 255, 255, 0.09);
  box-shadow: var(--highlight-top), var(--shadow-md);
}
.ddg-code__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(255, 255, 255, 0.03);
}
.ddg-code__name {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: rgba(255, 255, 255, 0.62);
}
.ddg-code__copy {
  margin-left: auto;
  cursor: pointer;
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  padding: 3px 9px;
  border-radius: var(--radius-xs);
  background: rgba(255, 255, 255, 0.07);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.78);
  transition: var(--transition-control);
}
.ddg-code__copy[data-copied] {
  background: rgba(128, 184, 104, 0.18);
  border-color: rgba(128, 184, 104, 0.42);
  color: #a9d695;
}
.ddg-code pre {
  margin: 0;
  padding: 14px 16px;
  overflow-x: auto;
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  line-height: 1.7;
  color: rgba(255, 255, 255, 0.88);
}
.ddg-code__line { display: flex; gap: 16px; }

/* Line numbers as a CSS counter, not as text in the markup. The prototype
   renders each number in its own <span>, which means selecting the block and
   copying it takes the numbers with the code. A ::before counter is not
   selectable, so a copy gets what a copy should get. */
.ddg-code pre[data-lines] { counter-reset: ddg-line; }
.ddg-code pre[data-lines] .ddg-code__line::before {
  counter-increment: ddg-line;
  content: counter(ddg-line);
  color: rgba(255, 255, 255, 0.26);
  user-select: none;
  min-width: 18px;
  text-align: right;
  flex: 0 0 auto;
}
.ddg-code__line > span { white-space: pre; }

/* Table ──────────────────────────────────────────────────────────────────── */

.ddg-table-wrap { width: 100%; overflow-x: auto; }
.ddg-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  color: var(--text-primary);
  --ddg-table-line: var(--border-default);
  --ddg-table-pad: 12px 16px;
}
.ddg-table--dense { --ddg-table-pad: 8px 14px; }

.ddg-table th {
  text-align: left;
  padding: var(--ddg-table-pad);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
  border-bottom: 1px solid var(--ddg-table-line);
  white-space: nowrap;
  background: var(--neutral-50);
}
.ddg-table td {
  padding: var(--ddg-table-pad);
  border-bottom: 1px solid var(--ddg-table-line);
  vertical-align: middle;
}
.ddg-table tbody tr { transition: background-color var(--duration-fast) var(--ease-standard); }
.ddg-table tbody tr:hover { background: var(--neutral-50); }

/* Column treatments, applied per cell so a numeric column lines up in the head
   and the body without the caller repeating itself. */
.ddg-table [data-align='right'] { text-align: right; }
.ddg-table [data-align='center'] { text-align: center; }
.ddg-table [data-numeric] { font-variant-numeric: tabular-nums; text-align: right; }
.ddg-table [data-mono] { font-family: var(--font-mono); }
.ddg-table [data-muted] { color: var(--text-secondary); }

.ddg-table__empty {
  padding: 26px 16px;
  text-align: center;
  color: var(--text-tertiary);
}

.ddg-table--invert {
  --ddg-table-line: rgba(255, 255, 255, 0.09);
  color: var(--text-inverse);
}
.ddg-table--invert th { color: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.03); }
.ddg-table--invert tbody tr:hover { background: rgba(255, 255, 255, 0.05); }
.ddg-table--invert [data-muted],
.ddg-table--invert .ddg-table__empty { color: rgba(255, 255, 255, 0.6); }

/* ═══════════════════════════════════════════════════════════════════════════
   LOADING
   ═══════════════════════════════════════════════════════════════════════════ */

/* Spinner ────────────────────────────────────────────────────────────────── */

.ddg-spinner {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  --ddg-spinner-d: 20px;
  --ddg-spinner-w: 2px;
  --ddg-spinner-tone: var(--accent-blue);
  --ddg-spinner-track: var(--neutral-200);
}
.ddg-spinner--center {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 12px;
  width: 100%;
}
.ddg-spinner--xs { --ddg-spinner-d: 12px; }
.ddg-spinner--sm { --ddg-spinner-d: 16px; }
.ddg-spinner--lg { --ddg-spinner-d: 28px; --ddg-spinner-w: 3px; }
.ddg-spinner--xl { --ddg-spinner-d: 38px; --ddg-spinner-w: 3px; }

.ddg-spinner--brand   { --ddg-spinner-tone: var(--ink-700); }
.ddg-spinner--cyber   { --ddg-spinner-tone: var(--practice-cyber); }
.ddg-spinner--it      { --ddg-spinner-tone: var(--practice-it); }
.ddg-spinner--ai      { --ddg-spinner-tone: var(--practice-ai); }
.ddg-spinner--search  { --ddg-spinner-tone: var(--practice-search); }
.ddg-spinner--digital { --ddg-spinner-tone: var(--practice-digital); }
.ddg-spinner--retail  { --ddg-spinner-tone: var(--practice-retail); }
.ddg-spinner--success { --ddg-spinner-tone: var(--accent-green); }
.ddg-spinner--warning { --ddg-spinner-tone: var(--accent-amber); }
.ddg-spinner--danger  { --ddg-spinner-tone: var(--accent-red); }
.ddg-spinner--neutral { --ddg-spinner-tone: var(--slate-400); }
.ddg-spinner--current { --ddg-spinner-tone: currentColor; }
.ddg-spinner--invert  { --ddg-spinner-track: rgba(255, 255, 255, 0.16); }

.ddg-spinner__ring {
  display: block;
  flex: 0 0 auto;
  width: var(--ddg-spinner-d);
  height: var(--ddg-spinner-d);
  border-radius: 50%;
  border: var(--ddg-spinner-w) solid var(--ddg-spinner-track);
  border-top-color: var(--ddg-spinner-tone);
  animation: var(--anim-spin);
}
.ddg-spinner__label {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
}
.ddg-spinner--invert .ddg-spinner__label { color: var(--text-inverse-muted); }

/* Skeleton ───────────────────────────────────────────────────────────────── */

.ddg-skel {
  display: block;
  background: var(--shimmer-fill);
  background-size: 220% 100%;
  animation: var(--anim-shimmer);
  border-radius: var(--radius-xs);
  height: 11px;
  width: 100%;
}
.ddg-skel--invert { background: var(--shimmer-fill-inverse); background-size: 220% 100%; }
.ddg-skel--static { animation: none; }

.ddg-skel--heading { height: 22px; width: 68%; }
.ddg-skel--label   { height: 9px;  width: 42%; }
.ddg-skel--block   { height: 80px; border-radius: var(--radius-lg); }
.ddg-skel--control { height: 38px; border-radius: var(--radius-sm); }
.ddg-skel--pill    { height: 24px; width: 88px; border-radius: var(--radius-pill); }
.ddg-skel--circle  { height: 40px; width: 40px; flex: 0 0 auto; border-radius: var(--radius-pill); }
.ddg-skel--media   { height: auto; aspect-ratio: 16 / 9; border-radius: var(--radius-lg); }

.ddg-skel-lines { display: flex; flex-direction: column; gap: 10px; width: 100%; }
/* The last line of a paragraph is short, because real paragraphs are. */
.ddg-skel-lines .ddg-skel:last-child { width: 62%; }

/* LoadingBar ─────────────────────────────────────────────────────────────── */

.ddg-loadbar {
  position: relative;
  width: 100%;
  height: 2px;
  overflow: hidden;
  background: var(--neutral-100);
  pointer-events: none;
  opacity: 1;
  transition: opacity var(--duration-base) var(--ease-standard);
  --ddg-loadbar-fill: var(--plume-gradient);
}
.ddg-loadbar--fixed { position: fixed; top: 0; left: 0; right: 0; z-index: 90; }
.ddg-loadbar[hidden],
.ddg-loadbar[data-idle] { opacity: 0; }
.ddg-loadbar--invert { background: rgba(255, 255, 255, 0.08); }

.ddg-loadbar--primary { --ddg-loadbar-fill: var(--accent-blue); }
.ddg-loadbar--brand   { --ddg-loadbar-fill: var(--ink-700); }
.ddg-loadbar--cyber   { --ddg-loadbar-fill: var(--practice-cyber); }
.ddg-loadbar--it      { --ddg-loadbar-fill: var(--practice-it); }
.ddg-loadbar--ai      { --ddg-loadbar-fill: var(--practice-ai); }
.ddg-loadbar--search  { --ddg-loadbar-fill: var(--practice-search); }
.ddg-loadbar--digital { --ddg-loadbar-fill: var(--practice-digital); }
.ddg-loadbar--retail  { --ddg-loadbar-fill: var(--practice-retail); }
.ddg-loadbar--success { --ddg-loadbar-fill: var(--accent-green); }
.ddg-loadbar--danger  { --ddg-loadbar-fill: var(--accent-red); }

.ddg-loadbar__fill {
  display: block;
  height: 100%;
  background: var(--ddg-loadbar-fill);
  transition: width var(--duration-slow) var(--ease-out);
}
.ddg-loadbar__fill--indeterminate {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 38%;
  animation: var(--anim-sweep);
}

/* LoadingOverlay ─────────────────────────────────────────────────────────── */

/* Covers content already on screen while it refetches. The children keep their
   layout — the overlay never collapses the box it sits in. */
.ddg-overlay { position: relative; border-radius: var(--radius-lg); }
.ddg-overlay__content {
  transition:
    opacity var(--duration-base) var(--ease-standard),
    filter var(--duration-base) var(--ease-standard);
}
.ddg-overlay__veil {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: inherit;
  /* The veil dims the surface it is over rather than always bleaching it. It
     was a flat white at 58%, which over a dark card in the dark theme makes a
     pale sheet — and the spinner's label, light on dark, then sat
     light-on-light at 1.36:1. "Refreshing" is the one string this component
     exists to show. `--overlay-veil` re-points with the theme. */
  background: var(--overlay-veil);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--duration-base) var(--ease-standard),
    visibility 0s linear var(--duration-base);
}
.ddg-overlay[aria-busy='true'] .ddg-overlay__content {
  opacity: 0.42;
  filter: saturate(0.7);
  pointer-events: none;
}
.ddg-overlay[aria-busy='true'] .ddg-overlay__veil {
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}
.ddg-overlay--invert .ddg-overlay__veil { background: rgba(8, 23, 31, 0.42); }
.ddg-overlay--invert[aria-busy='true'] .ddg-overlay__content { opacity: 0.34; }
.ddg-overlay--no-blur .ddg-overlay__veil { backdrop-filter: none; -webkit-backdrop-filter: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   NOTIFICATIONS
   ═══════════════════════════════════════════════════════════════════════════ */

.ddg-toast {
  position: relative;
  display: flex;
  gap: 11px;
  align-items: flex-start;
  overflow: hidden;
  width: 364px;
  max-width: 100%;
  padding: 12px 13px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  box-shadow: var(--shadow-lg);
  animation: var(--anim-slide-in);
  --ddg-toast-accent: var(--slate-400);
  --ddg-toast-wash: var(--neutral-100);
}
.ddg-toast--rise { animation: var(--anim-rise); }
.ddg-toast--static { animation: none; }

.ddg-toast--info    { --ddg-toast-accent: var(--status-info);    --ddg-toast-wash: var(--status-info-wash); }
.ddg-toast--success { --ddg-toast-accent: var(--status-success); --ddg-toast-wash: var(--status-success-wash); }
.ddg-toast--warning { --ddg-toast-accent: var(--status-warning); --ddg-toast-wash: var(--status-warning-wash); }
.ddg-toast--danger  { --ddg-toast-accent: var(--status-danger);  --ddg-toast-wash: var(--status-danger-wash); }
.ddg-toast--echo    { --ddg-toast-accent: var(--echo-accent);    --ddg-toast-wash: var(--echo-accent-soft); }

/* Echo's toast is dark, because Echo's surface is. */
.ddg-toast--echo,
.ddg-toast--invert {
  background: var(--surface-ink);
  border-color: rgba(255, 255, 255, 0.12);
  color: var(--text-inverse);
  box-shadow: var(--highlight-top), var(--shadow-lg);
}

.ddg-toast__lead {
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
  border-radius: var(--radius-sm);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--ddg-toast-wash);
  color: var(--ddg-toast-accent);
}
.ddg-toast--echo .ddg-toast__lead,
.ddg-toast--invert .ddg-toast__lead { background: rgba(255, 255, 255, 0.08); }
.ddg-toast__lead--dot::after {
  content: '';
  display: block;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--ddg-toast-accent);
}

.ddg-toast__body { display: flex; flex-direction: column; gap: 3px; min-width: 0; flex: 1 1 auto; }
.ddg-toast__title {
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  line-height: 1.35;
}
.ddg-toast__text {
  font-size: var(--text-caption);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}
.ddg-toast--echo .ddg-toast__text,
.ddg-toast--invert .ddg-toast__text { color: var(--text-inverse-muted); }
.ddg-toast__action { margin-top: 6px; }

.ddg-toast__dismiss {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: var(--radius-xs);
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: var(--transition-control);
}
.ddg-toast__dismiss:hover { background: var(--neutral-100); color: var(--ink-900); }
.ddg-toast--echo .ddg-toast__dismiss:hover,
.ddg-toast--invert .ddg-toast__dismiss:hover {
  background: rgba(255, 255, 255, 0.1);
  color: var(--text-inverse);
}

/* The countdown rail under an auto-dismissing toast. Its duration comes from
   --ddg-toast-duration, set inline because it is data. */
.ddg-toast__timer {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  transform-origin: left;
  background: var(--ddg-toast-accent);
  animation: ddg-grow-x var(--ddg-toast-duration, 5s) linear forwards reverse;
}

/* The rail is a CSS animation and the timer behind it is a setTimeout, so the
   two have to agree about pausing — a bar that stops while the countdown keeps
   running is a lie the visitor can watch happen. The runtime sets [data-paused]
   on hover and on focus-within and pauses its own timer in the same handler,
   which is why this selects the attribute rather than :hover directly. */
.ddg-toast[data-paused] .ddg-toast__timer { animation-play-state: paused; }

/* Leaving. The entrance is an animation, so the exit cannot also be one on the
   same element without them fighting; a transition on the [data-leaving] flag
   composes cleanly and lets the runtime keep a single 200ms number. Reduced
   motion drops it to nothing and the runtime removes the node immediately. */
.ddg-toast[data-leaving] {
  opacity: 0;
  transform: translateX(14px);
  transition: opacity var(--duration-base) var(--ease-out), transform var(--duration-base) var(--ease-out);
}
.ddg-toast-stack--top-left .ddg-toast[data-leaving],
.ddg-toast-stack--bottom-left .ddg-toast[data-leaving] { transform: translateX(-14px); }
.ddg-toast-stack--top-center .ddg-toast[data-leaving] { transform: translateY(-14px); }

@media (prefers-reduced-motion: reduce) {
  .ddg-toast[data-leaving] { transition: none; transform: none; }
  /* The countdown rail was NOT in this block until 2026-08-12. It is one of
     only two animations on the site written outside the --anim-* tokens, so it
     missed the blanket `none` those get at the token layer, and a five-second
     shrinking bar kept running for someone who had asked for no motion.
     `animation: none` alone would leave it at scaleX(1) forever, which reads as
     a toast that will never close — so it is held at the finished state
     instead, and the setTimeout behind it still fires. */
  .ddg-toast__timer { animation: none; transform: scaleX(0); }
}

.ddg-toast-stack {
  position: fixed;
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: var(--space-4);
  pointer-events: none;
  max-width: 100%;
}
.ddg-toast-stack > * { pointer-events: auto; }
.ddg-toast-stack--top-right    { top: 0; right: 0; align-items: flex-end; }
.ddg-toast-stack--top-left     { top: 0; left: 0; align-items: flex-start; }
.ddg-toast-stack--bottom-right { bottom: 0; right: 0; align-items: flex-end; flex-direction: column-reverse; }
.ddg-toast-stack--bottom-left  { bottom: 0; left: 0; align-items: flex-start; flex-direction: column-reverse; }
.ddg-toast-stack--top-center {
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  align-items: center;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CHARTS
   ═══════════════════════════════════════════════════════════════════════════ */

/* The charts themselves are SVG generated from tokens — see chart.ts. Only the
   legend has enough chrome to need CSS. */

.ddg-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}
.ddg-legend--column {
  flex-direction: column;
  flex-wrap: nowrap;
  gap: 9px;
}
.ddg-legend__item {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  flex: 0 0 auto;
}
.ddg-legend__swatch {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: var(--radius-pill);
}
.ddg-legend--bar .ddg-legend__swatch {
  width: 12px;
  height: 3px;
  border-radius: 2px;
}
.ddg-legend__label {
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  color: var(--slate-600);
  white-space: nowrap;
}
.ddg-legend__value {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-variant-numeric: tabular-nums;
  color: var(--ink-900);
}
/* In a column the values right-align into a readable number stack. */
.ddg-legend--column .ddg-legend__item { flex: 0 0 auto; }
.ddg-legend--column .ddg-legend__value { margin-left: auto; }

.ddg-legend--invert .ddg-legend__label { color: rgba(255, 255, 255, 0.62); }
.ddg-legend--invert .ddg-legend__value { color: var(--text-inverse); }

/* ═══════════════════════════════════════════════════════════════════════════
   ECHO
   ═══════════════════════════════════════════════════════════════════════════ */

/* Echo's identity is ink surface, violet light. Note that --echo-accent IS
   --accent-violet, which is also --practice-ai — and Echo appears on every
   page. See docs/design-system-migration-plan-2026-08-09.md §10.1: if AI keeps
   violet, the assistant and the practice are the same colour everywhere. */

.ddg-echo-avatar {
  position: relative;
  display: inline-flex;
  flex: 0 0 auto;
  width: var(--ddg-echo-d, 44px);
  height: var(--ddg-echo-d, 44px);
}
.ddg-echo-avatar--xs { --ddg-echo-d: 22px; }
.ddg-echo-avatar--sm { --ddg-echo-d: 30px; }
.ddg-echo-avatar--lg { --ddg-echo-d: 64px; }
.ddg-echo-avatar--xl { --ddg-echo-d: 104px; }

.ddg-echo-avatar__ring {
  position: absolute;
  inset: -3px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--echo-ring);
}
.ddg-echo-avatar--thinking .ddg-echo-avatar__ring { animation: var(--anim-echo-ring); }
.ddg-echo-avatar--muted .ddg-echo-avatar__ring { border-color: rgba(120, 88, 224, 0.22); }

.ddg-echo-avatar__img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* The supplied artwork sits high in its frame; 42% keeps the face centred. */
  object-position: 50% 42%;
  border-radius: var(--radius-pill);
  background: var(--neutral-100);
  box-shadow: 0 0 0 1px rgba(120, 88, 224, 0.3), 0 6px 18px -8px rgba(120, 88, 224, 0.55);
  transition: var(--transition-control);
}
.ddg-echo-avatar--muted .ddg-echo-avatar__img {
  box-shadow: none;
  filter: grayscale(1) opacity(0.6);
}
.ddg-echo-avatar__dot {
  position: absolute;
  right: 0;
  bottom: 0;
  width: max(7px, calc(var(--ddg-echo-d, 44px) * 0.2));
  height: max(7px, calc(var(--ddg-echo-d, 44px) * 0.2));
  border-radius: var(--radius-pill);
  background: var(--accent-green);
  border: 2px solid var(--ink-1000);
}

/* Message ────────────────────────────────────────────────────────────────── */

.ddg-echo-msg {
  display: flex;
  gap: 11px;
  align-items: flex-start;
  animation: var(--anim-rise);
}
.ddg-echo-msg--user { flex-direction: row-reverse; }

.ddg-echo-msg__initials {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-pill);
  background: var(--wing-gradient);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-nano);
  font-weight: 700;
  color: var(--text-inverse);
  letter-spacing: 0.04em;
}
.ddg-echo-msg__col {
  min-width: 0;
  max-width: min(560px, 84%);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
}
.ddg-echo-msg--user .ddg-echo-msg__col { align-items: flex-end; }

.ddg-echo-msg__bubble {
  padding: 10px 14px;
  font-size: var(--text-body-sm);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  /* The square corner points at the speaker. */
  border-radius: 4px var(--radius-lg) var(--radius-lg) var(--radius-lg);
  background: var(--echo-bubble);
  color: var(--text-primary);
  border: 1px solid var(--border-default);
}
.ddg-echo-msg--user .ddg-echo-msg__bubble {
  border-radius: var(--radius-lg) 4px var(--radius-lg) var(--radius-lg);
  background: var(--echo-user-bubble);
  color: var(--text-inverse);
  border: none;
  box-shadow: var(--shadow-xs);
}
.ddg-echo-msg--invert .ddg-echo-msg__bubble {
  background: var(--echo-bubble-inverse);
  color: var(--text-inverse);
  border-color: rgba(255, 255, 255, 0.09);
}
.ddg-echo-msg--invert.ddg-echo-msg--user .ddg-echo-msg__bubble {
  background: rgba(255, 255, 255, 0.1);
  border: none;
}

.ddg-echo-msg__thinking {
  display: inline-flex;
  gap: 4px;
  align-items: center;
  padding: 3px 0;
}
.ddg-echo-msg__thinking > i {
  width: 5px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--echo-accent);
  animation: ddg-echo-think 1.25s var(--ease-inout) infinite;
}
.ddg-echo-msg__thinking > i:nth-child(2) { animation-delay: 160ms; }
.ddg-echo-msg__thinking > i:nth-child(3) { animation-delay: 320ms; }
/* The second of the two animations written outside the --anim-* tokens, and so
   the second to miss their blanket `none`. Added 2026-08-12. The dots have to
   stay VISIBLE rather than resolve to their animated resting opacity: they are
   the only thing on screen saying Echo is still working, so stopping them must
   not read as Echo having stopped. */
@media (prefers-reduced-motion: reduce) {
  .ddg-echo-msg__thinking > i { animation: none; opacity: 0.6; }
}
.ddg-echo-msg--invert .ddg-echo-msg__thinking > i { background: rgba(255, 255, 255, 0.7); }

.ddg-echo-msg__sources {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.ddg-echo-msg__source {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border-radius: var(--radius-xs);
  background: var(--wash-ai);
  border: 1px solid rgba(120, 88, 224, 0.18);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-700);
}
.ddg-echo-msg__source::before {
  content: '';
  width: 4px;
  height: 4px;
  border-radius: var(--radius-pill);
  background: var(--echo-accent);
}
.ddg-echo-msg--invert .ddg-echo-msg__source {
  background: var(--echo-surface-soft);
  border-color: rgba(120, 88, 224, 0.24);
  color: rgba(255, 255, 255, 0.72);
}

.ddg-echo-msg__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.ddg-echo-msg--invert .ddg-echo-msg__meta { color: rgba(255, 255, 255, 0.52); }

/* Composer ───────────────────────────────────────────────────────────────── */

.ddg-echo-composer { display: flex; flex-direction: column; gap: 10px; }

/* `hidden` on a flex container needs saying — display:flex wins over the user
   agent's [hidden] rule otherwise. The offline path hides the composer and
   puts a ticket form in its place; without this the two stack. */
.ddg-echo-composer[hidden] { display: none; }

.ddg-echo-composer__suggestions {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}
.ddg-echo-composer__suggestion {
  cursor: pointer;
  padding: 5px 11px;
  border-radius: var(--radius-pill);
  background: var(--neutral-0);
  border: 1px solid var(--border-default);
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  color: var(--text-secondary);
  transition: var(--transition-control);
}
.ddg-echo-composer__suggestion:hover {
  border-color: var(--echo-accent);
  color: var(--text-primary);
}

.ddg-echo-composer__shell {
  display: flex;
  align-items: flex-end;
  gap: 9px;
  padding: 9px 9px 9px 14px;
  border-radius: var(--radius-lg);
  background: var(--neutral-0);
  border: 1px solid var(--border-default);
  box-shadow: var(--shadow-xs);
  transition: var(--transition-control);
}
.ddg-echo-composer__shell:focus-within {
  border-color: var(--echo-accent);
  box-shadow: 0 0 0 3px rgba(120, 88, 224, 0.22);
}
.ddg-echo-composer__input {
  flex: 1;
  min-width: 0;
  resize: none;
  border: none;
  outline: none;
  background: transparent;
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  color: var(--text-primary);
  padding: 6px 0;
  max-height: 120px;
  field-sizing: content;
}
.ddg-echo-composer__send {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--echo-accent);
  color: var(--text-inverse);
  box-shadow: var(--highlight-top), var(--glow-echo);
  cursor: pointer;
  transition: var(--transition-control);
}
.ddg-echo-composer__send:disabled {
  background: var(--neutral-200);
  color: var(--text-tertiary);
  box-shadow: none;
  cursor: not-allowed;
}
.ddg-echo-composer__hint {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.ddg-echo-composer--invert .ddg-echo-composer__suggestion {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.72);
}
.ddg-echo-composer--invert .ddg-echo-composer__shell {
  background: rgba(255, 255, 255, 0.05);
  border-color: rgba(255, 255, 255, 0.13);
  box-shadow: var(--highlight-top);
}
.ddg-echo-composer--invert .ddg-echo-composer__input { color: var(--text-inverse); }
.ddg-echo-composer--invert .ddg-echo-composer__input::placeholder { color: rgba(255, 255, 255, 0.52); }
.ddg-echo-composer--invert .ddg-echo-composer__send:disabled {
  background: rgba(255, 255, 255, 0.09);
  color: rgba(255, 255, 255, 0.42);
}
.ddg-echo-composer--invert .ddg-echo-composer__hint { color: rgba(255, 255, 255, 0.52); }

/* Panel ──────────────────────────────────────────────────────────────────── */

.ddg-echo-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-radius: var(--radius-xl);
  overflow: hidden;
  background: var(--echo-surface);
  color: var(--text-inverse);
  border: 1px solid rgba(255, 255, 255, 0.1);
  /* --glow-echo, not --shadow-lg. The design system is explicit that the
     violet ring is what marks the panel as the one live thing on the page, and
     that is also why there is one panel per view. A neutral drop shadow makes
     it read as another card. */
  box-shadow: var(--highlight-top), var(--glow-echo);
}
.ddg-echo-panel__head {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 13px 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.09);
  background: rgba(255, 255, 255, 0.03);
  flex: 0 0 auto;
}
.ddg-echo-panel__title {
  font-family: var(--font-sans);
  font-size: var(--text-h4);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-normal);
}
.ddg-echo-panel__subtitle {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.52);
  margin-top: 2px;
}

/* Presence. Green at rest, amber and breathing while Echo is working, grey
   when it is switched off — the status TEXT can be missed at nano size, and
   this is the part that reads at a glance. */
.ddg-echo-panel__dot {
  flex: 0 0 auto;
  width: 5px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--accent-green);
}
.ddg-echo-panel--thinking .ddg-echo-panel__dot {
  background: var(--accent-amber);
  animation: var(--anim-pulse);
}
.ddg-echo-panel--muted .ddg-echo-panel__dot {
  background: rgba(255, 255, 255, 0.32);
}
.ddg-echo-panel__thread {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 18px 16px;
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
}
.ddg-echo-panel__foot {
  padding: 13px 16px 16px;
  border-top: 1px solid rgba(255, 255, 255, 0.09);
  flex: 0 0 auto;
}

/* ═══════════════════════════════════════════════════════════════════════════
   ICONS
   ═══════════════════════════════════════════════════════════════════════════ */

/* Lucide, from the build-time sprite. Stroke 1.75, currentColor, no fills —
   set on the <symbol> so a bare <use> inherits them. Sizes 16/20/24 only. */
.ddg-icon {
  display: inline-block;
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  vertical-align: middle;
}
.ddg-icon--16 { width: 16px; height: 16px; }
.ddg-icon--24 { width: 24px; height: 24px; }

/* ═══════════════════════════════════════════════════════════════════════════
   FORMS
   ═══════════════════════════════════════════════════════════════════════════ */

/* Field shell ────────────────────────────────────────────────────────────── */

.ddg-field {
  display: flex;
  flex-direction: column;
  gap: 7px;
  width: 100%;
}
.ddg-field__label {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  line-height: 1.3;
  color: currentColor;
  opacity: 0.72;
}
/* `--text-required`, not `--accent-orange`. The raw hue is 2.87:1 on paper —
   and this `*` is on the sign-in, register and both code screens, which is
   every form a stranger meets. No one value clears both grounds; the token
   carries the `-deep` cut on paper and the base hue on ink, which is the rule
   the design skill already states. */
.ddg-field__required { color: var(--text-required); }
.ddg-field__hint {
  font-size: var(--text-caption);
  line-height: 1.45;
  color: currentColor;
  opacity: 0.5;
}
.ddg-field__error {
  font-size: var(--text-caption);
  line-height: 1.45;
  color: var(--text-danger);
}

/* Control ────────────────────────────────────────────────────────────────── */

/* A quiet sunken field on a hairline; focus lifts it to white with the blue
   ring. The inset keeps it engraved rather than floating, and the shadow is
   cool navy, never black. Transcribed from fieldControl(). */
.ddg-control {
  width: 100%;
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  letter-spacing: var(--tracking-normal);
  line-height: 1.4;
  color: var(--text-primary);
  background: var(--neutral-50);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  padding: 10px 13px;
  min-height: 42px;
  outline: none;
  box-shadow: inset 0 1px 2px rgba(24, 64, 88, 0.045);
  transition: var(--transition-control);
}
.ddg-control:hover:not(:disabled) { border-color: var(--slate-300); }
.ddg-control:focus {
  background: var(--neutral-0);
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus);
}
.ddg-control:disabled {
  color: var(--text-tertiary);
  background: var(--surface-sunken);
  cursor: not-allowed;
}
.ddg-control[aria-invalid='true'] {
  border-color: var(--accent-red);
  box-shadow: inset 0 1px 2px rgba(217, 58, 43, 0.07);
}
.ddg-control[aria-invalid='true']:focus { box-shadow: var(--shadow-focus); }

.ddg-control--textarea {
  resize: vertical;
  line-height: var(--leading-snug);
  padding: 10px 12px;
}
.ddg-control--numeric { font-variant-numeric: tabular-nums; }

/* Affixed input ──────────────────────────────────────────────────────────── */

/* The prototype absolutely-positions the affixes and then measures them with
   useLayoutEffect on every render, so the input's padding can clear them —
   "affixes are measured, not assumed". The guarantee is right; the measurement
   is not needed. A flex shell that OWNS the border, with the input transparent
   inside it, means the control physically cannot run underneath an affix. No
   JS, no layout thrash, no flash of wrong padding before the effect runs. */
.ddg-affix {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 0 13px;
  background: var(--neutral-50);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  min-height: 42px;
  box-shadow: inset 0 1px 2px rgba(24, 64, 88, 0.045);
  transition: var(--transition-control);
}
.ddg-affix:hover { border-color: var(--slate-300); }
.ddg-affix:focus-within {
  background: var(--neutral-0);
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus);
}
.ddg-affix[data-invalid] {
  border-color: var(--accent-red);
  box-shadow: inset 0 1px 2px rgba(217, 58, 43, 0.07);
}
.ddg-affix[data-disabled] {
  background: var(--surface-sunken);
  cursor: not-allowed;
}
.ddg-affix__slot {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  color: var(--text-tertiary);
  font-size: var(--text-body-sm);
}
/* Inside the shell the input contributes nothing but text. */
.ddg-affix .ddg-control {
  flex: 1 1 auto;
  min-width: 0;
  padding: 0;
  border: none;
  background: none;
  box-shadow: none;
  min-height: 40px;
}
.ddg-affix .ddg-control:focus { box-shadow: none; }

/* Select ─────────────────────────────────────────────────────────────────── */

.ddg-select-wrap { position: relative; display: flex; }
.ddg-control--select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 36px;
  cursor: pointer;
}
.ddg-control--select:disabled { cursor: not-allowed; }
.ddg-select-wrap__chevron {
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  color: var(--text-tertiary);
}

/* Date and time ──────────────────────────────────────────────────────────── */

/* tokens/base.css already hides the browser's own indicator on these types and
   makes room for ours — this is the button that replaces it. */
.ddg-date-wrap { position: relative; display: flex; align-items: center; }
.ddg-date-wrap .ddg-control { padding-right: 38px; }
.ddg-date-wrap__glyph {
  position: absolute;
  right: 11px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: var(--transition-control);
}
.ddg-date-wrap:focus-within .ddg-date-wrap__glyph { color: var(--accent-blue); }

/* Checkbox ───────────────────────────────────────────────────────────────── */

.ddg-check {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  cursor: pointer;
}
.ddg-check:has(input:disabled) { cursor: not-allowed; opacity: 0.5; }

.ddg-check__box {
  position: relative;
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  margin-top: 2px;
}
.ddg-check__box input {
  appearance: none;
  -webkit-appearance: none;
  position: absolute;
  inset: 0;
  margin: 0;
  width: 18px;
  height: 18px;
  border-radius: var(--radius-xs);
  background: var(--neutral-0);
  border: 1px solid var(--border-emphasis);
  cursor: inherit;
  transition: var(--transition-control);
}
.ddg-check:hover input:not(:disabled):not(:checked) { border-color: var(--text-tertiary); }
.ddg-check__box input:checked {
  background: var(--ink-700);
  border-color: var(--ink-700);
}
.ddg-check__box input:focus-visible { box-shadow: var(--shadow-focus); }

/* The tick is a rotated corner, drawn only when checked. Doing it as ::after on
   the input means no extra element and no JS to keep the two in step. */
.ddg-check__box input:checked::after {
  content: '';
  position: absolute;
  left: 4px;
  top: 4px;
  width: 8px;
  height: 4px;
  border-left: 2px solid var(--action-primary-fg);
  border-bottom: 2px solid var(--action-primary-fg);
  transform: rotate(-45deg);
}

.ddg-check__label {
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  line-height: 1.4;
}
.ddg-check__description {
  display: block;
  font-size: var(--text-caption);
  opacity: 0.55;
  margin-top: 2px;
  line-height: 1.45;
}

/* Switch ─────────────────────────────────────────────────────────────────── */

.ddg-switch {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  --ddg-switch-w: 42px;
  --ddg-switch-h: 24px;
}
.ddg-switch--sm { --ddg-switch-w: 36px; --ddg-switch-h: 20px; }
.ddg-switch:has(button:disabled) { opacity: 0.5; }

.ddg-switch__track {
  position: relative;
  flex: 0 0 auto;
  width: var(--ddg-switch-w);
  height: var(--ddg-switch-h);
  padding: 0;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-emphasis);
  background: var(--neutral-200);
  box-shadow: var(--shadow-xs);
  cursor: pointer;
  transition: var(--transition-control);
}
.ddg-switch__track:disabled { cursor: not-allowed; }
.ddg-switch__track[aria-checked='true'] {
  background: var(--accent-blue);
  border-color: var(--accent-blue);
  box-shadow: var(--glow-brand);
}
.ddg-switch__track::after {
  content: '';
  position: absolute;
  top: 1px;
  left: 1px;
  width: calc(var(--ddg-switch-h) - 4px);
  height: calc(var(--ddg-switch-h) - 4px);
  border-radius: 50%;
  background: var(--neutral-0);
  box-shadow: 0 1px 3px rgba(12, 34, 51, 0.28);
  transition: left var(--duration-base) var(--ease-standard);
}
.ddg-switch__track[aria-checked='true']::after {
  left: calc(var(--ddg-switch-w) - var(--ddg-switch-h) + 1px);
}
.ddg-switch__label {
  display: block;
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
  line-height: 1.4;
  cursor: pointer;
}
.ddg-switch__description {
  display: block;
  font-size: var(--text-caption);
  opacity: 0.55;
  margin-top: 2px;
}

/* FileUpload ─────────────────────────────────────────────────────────────── */

/* A drop zone, not a naked file input: dragging over tints the whole target so
   the affordance is the surface itself. The drag state is the one thing here
   that genuinely needs script — it is exposed as [data-over] so the CSS holds
   all the appearance and the JS holds only the event. */
.ddg-upload { display: flex; flex-direction: column; gap: 9px; width: 100%; }
.ddg-upload__zone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 22px 18px;
  text-align: center;
  border-radius: var(--radius-sm);
  border: 1px dashed var(--border-default);
  background: var(--neutral-50);
  box-shadow: inset 0 1px 2px rgba(24, 64, 88, 0.045);
  cursor: pointer;
  transition: var(--transition-control);
}
.ddg-upload__zone:hover { border-color: var(--slate-300); }
.ddg-upload__zone:focus-visible,
.ddg-upload__zone[data-over] {
  border-color: var(--border-focus);
  background: rgba(56, 120, 232, 0.05);
  box-shadow: var(--shadow-focus);
}
.ddg-upload__zone[data-over] .ddg-upload__icon,
.ddg-upload__zone[data-over] .ddg-upload__prompt { color: var(--accent-blue); }
.ddg-upload__zone[data-invalid] { border-color: var(--accent-red); }
.ddg-upload__zone[data-disabled] {
  background: var(--surface-sunken);
  cursor: not-allowed;
}
.ddg-upload__icon { color: var(--text-tertiary); }
.ddg-upload__prompt {
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}
.ddg-upload__note {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  letter-spacing: 0.04em;
  color: var(--text-tertiary);
}
.ddg-upload__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.ddg-upload__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px 8px 12px;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
}
.ddg-upload__name {
  flex: 1;
  min-width: 0;
  font-size: var(--text-caption);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ddg-upload__size {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-tertiary);
  font-variant-numeric: tabular-nums;
}
.ddg-upload__remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  border-radius: var(--radius-xs);
  background: none;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: var(--transition-control);
}
.ddg-upload__remove:hover { background: var(--neutral-100); color: var(--ink-900); }

/* Inverse forms ──────────────────────────────────────────────────────────── */

/* The prototype's fields are paper-only. On an ink panel the sunken white field
   is a hole in the surface, so the whole set gets an inverse cut. Labels and
   hints already use currentColor with opacity, so they need nothing. */
.ddg-field--inverse .ddg-control,
.ddg-field--inverse .ddg-affix,
.ddg-field--inverse .ddg-upload__zone {
  background: rgba(255, 255, 255, 0.045);
  border-color: rgba(255, 255, 255, 0.16);
  color: var(--text-inverse);
  box-shadow: var(--highlight-top);
}
.ddg-field--inverse .ddg-control::placeholder { color: rgba(255, 255, 255, 0.52); }
.ddg-field--inverse .ddg-control:hover:not(:disabled),
.ddg-field--inverse .ddg-affix:hover { border-color: rgba(255, 255, 255, 0.3); }
.ddg-field--inverse .ddg-control:focus,
.ddg-field--inverse .ddg-affix:focus-within {
  background: rgba(255, 255, 255, 0.08);
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus);
}

/* ═══════════════════════════════════════════════════════════════════════════
   QUESTIONS
   ═══════════════════════════════════════════════════════════════════════════ */

/* The machine-readability reference: inline microdata plus one JSON-LD block,
   stable per-question anchors, and answers in the HTML rather than behind a
   click. A crawler, a language model and a reader all get the same text. */

.ddg-questions { display: flex; flex-direction: column; gap: clamp(32px, 4vw, 56px); }

.ddg-questions__header {
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-width: var(--measure-prose);
}
.ddg-questions__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-display-3);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  font-weight: var(--weight-display);
  color: var(--text-primary);
}
.ddg-questions__lead {
  margin: 0;
  font-size: var(--text-lead);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  color: var(--text-secondary);
  max-width: var(--measure-lead);
  text-wrap: pretty;
}
.ddg-questions__meta,
.ddg-questions__count {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.ddg-questions__jump ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.ddg-questions__group {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 56px);
}
.ddg-questions__group + .ddg-questions__group {
  border-top: 1px solid var(--border-default);
  padding-top: clamp(32px, 4vw, 56px);
}
.ddg-questions__aside {
  flex: 1 1 240px;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.ddg-questions__rule {
  width: 40px;
  height: 2px;
  border-radius: 1px;
  background: var(--slate-400);
}
.ddg-questions__group-title {
  margin: 0;
  font-size: var(--text-h1);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}
.ddg-questions__summary {
  margin: 0;
  font-size: var(--text-caption);
  line-height: var(--leading-body);
  color: var(--text-secondary);
  text-wrap: pretty;
}
.ddg-questions__list { flex: 3 1 460px; min-width: 0; }

.ddg-question {
  border-top: 1px solid var(--border-default);
  padding: 20px 0;
}
.ddg-question:last-child { border-bottom: 1px solid var(--border-default); }

.ddg-question__head {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  list-style: none;
}
/* Safari still needs this to drop the disclosure triangle. */
.ddg-question__head::-webkit-details-marker { display: none; }
details.ddg-question__wrap > .ddg-question__head { cursor: pointer; }

.ddg-question__index {
  flex: 0 0 26px;
  padding-top: 3px;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  color: var(--text-tertiary);
  text-decoration: none;
}
a.ddg-question__index:hover { color: var(--accent-blue); }

.ddg-question__q {
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-h3);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
  text-wrap: pretty;
}

/* The +/− marker, driven entirely by the <details> open state. No script. */
.ddg-question__toggle {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  line-height: 1;
  color: var(--text-tertiary);
}
.ddg-question__toggle::before { content: '+'; }
details[open] > .ddg-question__head .ddg-question__toggle::before { content: '\2212'; }

.ddg-question__body {
  padding-left: 40px;
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: var(--measure-prose);
}
.ddg-question__a {
  margin: 0;
  font-size: var(--text-body);
  line-height: var(--leading-body);
  letter-spacing: var(--tracking-normal);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
  text-wrap: pretty;
}
.ddg-question__detail {
  margin: 0;
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  letter-spacing: var(--tracking-normal);
  color: var(--text-secondary);
  text-wrap: pretty;
}

/* Ask Echo ───────────────────────────────────────────────────────────────── */

.ddg-questions__echo {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-xl);
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: var(--echo-surface);
  color: var(--text-inverse);
  box-shadow: var(--highlight-top), var(--glow-echo);
  padding: clamp(24px, 3vw, 36px);
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 3vw, 44px);
  align-items: flex-start;
}
.ddg-questions__echo::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(620px 300px at 14% -18%, rgba(120, 88, 224, 0.24), transparent 62%),
    radial-gradient(480px 260px at 94% 4%, rgba(0, 184, 208, 0.1), transparent 64%);
}
.ddg-questions__echo > * { position: relative; }
.ddg-questions__echo-intro {
  flex: 1 1 260px;
  max-width: 380px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.ddg-questions__echo-title {
  margin: 0;
  font-size: var(--text-h1);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  font-weight: var(--weight-semibold);
  color: var(--text-inverse);
}
.ddg-questions__echo-note {
  margin: 0;
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  color: var(--text-inverse-muted);
  text-wrap: pretty;
}
.ddg-questions__echo-form { flex: 2 1 380px; min-width: 0; padding-top: 6px; }

/* ═══════════════════════════════════════════════════════════════════════════
   UTILITIES
   ═══════════════════════════════════════════════════════════════════════════ */

/* Visible to a screen reader, not to the eye. Used where a glyph carries
   meaning — the ↑ / ↓ on a delta, for instance, which otherwise announces as
   nothing or as "up arrow" depending on the reader.

   `.visually-hidden` is Harbour's name for the same thing, aliased rather than
   restated because `src/components/PostEngagement.astro` renders one set of
   markup for both systems and writes that name on four labels. Without it they
   are four visible labels beside two visible honeypots. It retires with
   site.css; `.sr-only` is the name to write in anything new. */
.sr-only,
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   FOCUS
   ═══════════════════════════════════════════════════════════════════════════ */

/* One focus treatment for everything interactive. The prototype has none —
   its components track hover and press but never :focus-visible, so a
   keyboard user gets no indicator at all. That is a defect, not a style. */
.ddg-btn:focus-visible,
.ddg-iconbtn:focus-visible,
.ddg-arrowlink:focus-visible,
.ddg-card:focus-visible,
.ddg-article:focus-visible,
.ddg-tabs__tab:focus-visible,
.ddg-tag:focus-visible,
.ddg-practice:focus-visible,
.ddg-code__copy:focus-visible,
.ddg-logo:focus-visible,
.ddg-skip:focus-visible,
.ddg-appbar__menu:focus-visible,
.ddg-appbar__searchbtn:focus-visible,
.ddg-appbar__icon:focus-visible,
.ddg-appbar__account:focus-visible,
.ddg-practicecard:focus-visible,
.ddg-themepills__pill:focus-visible,
.ddg-cmd__item:focus-visible,
.ddg-rail__link:focus-visible,
.ddg-rail__search:focus-visible,
.ddg-envswitch__btn:focus-visible,
.ddg-echotoggle:focus-visible,
.ddg-pagehead__tab:focus-visible,
.engage-like:focus-visible,
.engage-share__btn:focus-visible,
.engage-sub__form button:focus-visible,
.engage-comments__form button:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
  box-shadow: var(--shadow-focus);
}

/* The console is ink. Its focus ring has to be visible ON ink, and
   --border-focus is tuned for paper. */
.ddg-console :focus-visible {
  outline-color: var(--accent-cyan);
}

/* ═══════════════════════════════════════════════════════════════════════════
   APP SHELLS
   ═══════════════════════════════════════════════════════════════════════════

   Two shells, because DDG has two audiences and they are not the same job.

   .ddg-shell    the public surface. Paper ground, follows [data-theme], one
                 sticky bar, a mega menu, and a footer that carries the brand.
                 From templates/app-shell/AppShell.dc.html.
   .ddg-console  the operator surface. Permanently ink — it does NOT follow
                 the theme, because it is lit rather than coloured and there is
                 no light version of it in the system. Rail, breadcrumb bar,
                 Echo drawer, status strip. From ui_kits/axion/.

   Both are laid out here rather than in the .astro files for the reason at the
   top of this file: the prototype's inline style objects would be re-sent in
   the HTML of every page, and its hover states live in React state, so they
   would not exist at all on a static page.

   The one structural departure from the prototype in both shells: every
   overlay is a real <dialog>. The prototype builds each one from a fixed div
   plus a hand-rolled Escape listener, which means no focus trap, no inert
   background, and a Tab that walks straight out of the open modal into the
   page behind it.                                                            */

/* Skip link ──────────────────────────────────────────────────────────────── */

/* Moved off-screen rather than display:none — a display:none skip link is not
   reachable by the keyboard user it exists for. */
.ddg-skip {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: 100;
  padding: 10px 16px;
  border-radius: var(--radius-sm);
  background: var(--surface-card);
  color: var(--text-primary);
  border: 1px solid var(--border-emphasis);
  box-shadow: var(--shadow-md);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  transform: translateY(calc(-100% - var(--space-4)));
  transition: transform var(--duration-fast) var(--ease-out);
}

/* :focus, not :focus-visible. The link sits off-screen, so no pointer can
   reach it and the two can only ever differ in the case that matters —
   :focus-visible is a heuristic, and it declines to fire when focus arrives
   programmatically (a router restoring position, a "skip to content" invoked
   by script). Losing the ring in those cases means the link takes focus while
   staying invisible, which is worse than having no skip link at all. */
.ddg-skip:focus {
  transform: none;
}

/* Front-end shell ────────────────────────────────────────────────────────── */

.ddg-shell {
  --ddg-appbar-height: 60px;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--font-sans);
}

.ddg-shell__main {
  flex: 1 1 auto;
  min-width: 0;
}

/* App bar ────────────────────────────────────────────────────────────────── */

/* position:sticky, not fixed. Fixed needs a matching top padding on <main>
   that every page has to remember, and the one page that forgets it hides its
   own <h1> under the bar. */
.ddg-appbar {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--surface-glass);
  border-bottom: 1px solid var(--border-default);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .ddg-appbar {
    -webkit-backdrop-filter: var(--frost-blur);
    backdrop-filter: var(--frost-blur);
  }
}

.ddg-appbar__inner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 0 var(--gutter);
  height: var(--ddg-appbar-height);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.ddg-appbar__brand {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  flex: 0 0 auto;
  color: var(--text-primary);
  text-decoration: none;
}

.ddg-appbar__wordmark {
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  white-space: nowrap;
}

/* The wordmark is the first thing to go: below 640px the crest alone still
   identifies the site, and the row has four other controls to fit. */
@media (max-width: 640px) {
  .ddg-appbar__wordmark {
    display: none;
  }
}

.ddg-appbar__menu {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  padding: 0 11px 0 12px;
  margin-right: var(--space-1);
  flex: 0 0 auto;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-appbar__menu:hover {
  border-color: var(--border-emphasis);
  background: var(--surface-sunken);
}

/* One glyph that rotates, not two icons swapped on state — a swap cannot be
   animated and reads as a flicker. */
.ddg-appbar__chevron {
  opacity: 0.6;
  transition: transform var(--duration-fast) var(--ease-out);
}

.ddg-appbar__menu[aria-expanded='true'] .ddg-appbar__chevron {
  transform: rotate(180deg);
}

.ddg-appbar__search {
  flex: 1 1 auto;
  display: flex;
  justify-content: center;
  min-width: 0;
  padding: 0 var(--space-2);
}

.ddg-appbar__searchbtn {
  display: flex;
  align-items: center;
  gap: 9px;
  width: min(420px, 100%);
  height: 34px;
  padding: 0 var(--space-2) 0 var(--space-3);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-default);
  background: var(--surface-sunken);
  color: var(--text-tertiary);
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  text-align: left;
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-appbar__searchbtn:hover {
  border-color: var(--border-emphasis);
  background: var(--surface-card);
}

.ddg-appbar__searchlabel {
  flex: 1 1 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Below the width where it can be read, the label stops being visible text and
   becomes the button's accessible NAME only.

   `text-overflow: ellipsis` degrades gracefully until it does not: on a 390px
   phone this label was **2px wide holding 220px of text**, which renders as a
   sliver of one character beside the magnifier. Measured on the live site, not
   inferred — and at 600px it is still clipped mid-word.

   A media query cannot express this. The app bar gains controls as it widens,
   so the space left for the label is NOT monotonic in viewport width: the label
   fits at 840 and 880, is clipped again from 920 to 1000, and fits from 1040.
   A container query asks the only question that matters — is there room in the
   button — and is right at every width without knowing what else the bar is
   carrying.

   `.visually-hidden` rather than `display: none`, and that is the whole reason
   this is not two lines: this span IS the button's accessible name. The icon is
   decorative. Hiding it outright leaves a button that a screen reader announces
   as "button", and the site's only search would become unusable for exactly the
   people who most need a name on it. */
.ddg-appbar__searchbtn {
  container-type: inline-size;
}

/* 150px of content box, which leaves the label about 125 after the glyph and
   the gap. The threshold is "still says something", not "fits": ellipsised to
   "Search pages, per…" the label is doing its job, and a rule tuned to the full
   220px would hide it on a 1000px laptop where it very nearly fits. What it
   must never do again is render the 2px shard. */
@container (max-width: 150px) {
  .ddg-appbar__searchlabel {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  /* Nothing left to sit beside, so the button becomes the glyph. */
  .ddg-appbar__searchbtn .ddg-kbd {
    display: none;
  }
}

.ddg-kbd {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  padding: 3px 6px;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  color: var(--text-tertiary);
}

/* A touch device has no ⌘, so the hint is a lie there — and it is the widest
   thing in the button. */
@media (hover: none), (max-width: 860px) {
  .ddg-appbar__searchbtn .ddg-kbd {
    display: none;
  }
}

.ddg-appbar__tools {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: 0 0 auto;
}

.ddg-appbar__clock {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  height: 24px;
  padding-right: 10px;
  margin-right: var(--space-1);
  border-right: 1px solid var(--border-default);
}

.ddg-appbar__clock time {
  font-family: var(--font-mono);
  font-size: var(--text-caption);
  font-variant-numeric: tabular-nums;
  color: var(--text-secondary);
}

.ddg-appbar__zone {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.ddg-appbar__live {
  width: 6px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--accent-green);
  box-shadow: 0 0 0 3px var(--wash-green);
  animation: var(--anim-pulse);
}

.ddg-appbar__icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-appbar__icon:hover {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

.ddg-appbar__icon--echo:hover {
  color: var(--echo-accent);
}

/* The unread pip. aria-hidden in the markup — a count, when there is one,
   belongs in the button's accessible name, not in a coloured dot. */
.ddg-appbar__pip {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 5px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--echo-accent);
  box-shadow: 0 0 0 2px var(--surface-glass);
}

.ddg-appbar__sep {
  width: 1px;
  height: 24px;
  background: var(--border-default);
  margin: 0 var(--space-1);
  flex: 0 0 auto;
}

.ddg-appbar__account {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 36px;
  padding: 0 9px 0 var(--space-1);
  flex: 0 0 auto;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  color: var(--text-primary);
  font-family: var(--font-sans);
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-appbar__account:hover {
  border-color: var(--border-emphasis);
}

.ddg-appbar__name {
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

@media (max-width: 760px) {
  .ddg-appbar__name {
    display: none;
  }
}

.ddg-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex: 0 0 auto;
  border-radius: var(--radius-pill);
  background: var(--ink-700);
  color: var(--text-inverse);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.04em;
  box-shadow: var(--highlight-top);
}

.ddg-avatar--lg {
  width: 38px;
  height: 38px;
  font-size: var(--text-body-sm);
}

/* Below 900px the clock and the fullscreen and ticket controls come out. They
   are conveniences, and the row cannot hold seven controls plus a search field
   on a phone without the search field collapsing to nothing. */
@media (max-width: 900px) {
  .ddg-appbar__clock,
  .ddg-appbar__icon--optional {
    display: none;
  }
}

/* One compact row on phones. The menu and search keep full accessible names,
   but their visible treatment becomes a 44px icon target. */
@media (max-width: 640px) {
  html.ddg-scroll-locked {
    overflow: hidden;
  }

  .ddg-appbar__inner {
    gap: 6px;
  }

  .ddg-appbar__menu {
    width: 44px;
    height: 44px;
    justify-content: center;
    padding: 0;
    margin-right: 0;
  }

  .ddg-appbar__menulabel,
  .ddg-appbar__menu .ddg-appbar__chevron,
  .ddg-appbar__tools > [data-ddg-theme='toggle'],
  .ddg-appbar__icon--echo,
  .ddg-appbar__sep {
    display: none;
  }

  .ddg-appbar__brand {
    gap: 0;
  }

  .ddg-appbar__search {
    flex: 1 1 auto;
    justify-content: flex-end;
    padding: 0;
  }

  .ddg-appbar__searchbtn {
    width: 44px;
    height: 44px;
    padding: 0;
    justify-content: center;
  }

  .ddg-appbar__searchlabel {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .ddg-appbar__tools {
    gap: 0;
  }

  .ddg-appbar__tools > .ddg-btn,
  .ddg-appbar__account {
    min-height: 44px;
  }

  .ddg-appbar__account {
    width: 44px;
    justify-content: center;
    padding: 0;
  }

  .ddg-appbar__account .ddg-appbar__chevron {
    display: none;
  }
}

/* Mega menu ──────────────────────────────────────────────────────────────── */

.ddg-megamenu {
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  z-index: 35;
  background: var(--surface-page);
  border-bottom: 1px solid var(--border-default);
  box-shadow: var(--shadow-lg);
  animation: var(--anim-rise);
  max-height: calc(100dvh - var(--ddg-appbar-height));
  overflow-y: auto;
}

/* The plume, as a hairline. The only place the full spectrum appears at once. */
.ddg-megamenu__plume {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 2px;
  background: var(--plume-gradient);
}

.ddg-megamenu__grid {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--space-6) var(--gutter) var(--space-5);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 192px), 1fr));
  gap: 18px;
}

.ddg-megamenu__aside {
  display: flex;
  flex-direction: column;
  gap: 11px;
  padding-left: 18px;
  border-left: 1px solid var(--border-default);
}

/* That border reads as a column rule at three-plus columns and as a stray
   vertical line once the grid has wrapped to one. */
@media (max-width: 720px) {
  .ddg-megamenu__aside {
    padding-left: 0;
    padding-top: var(--space-4);
    border-left: none;
    border-top: 1px solid var(--border-default);
  }
}

.ddg-megamenu__label {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.ddg-megamenu__links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 11px;
}

.ddg-megamenu__link {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
  text-decoration: none;
  transition: var(--transition-control);
}

.ddg-megamenu__link:hover {
  color: var(--text-primary);
}

.ddg-megamenu__link .ddg-icon {
  opacity: 0.7;
}

.ddg-megamenu__foot {
  border-top: 1px solid var(--border-default);
  background: var(--surface-page-alt);
}

.ddg-megamenu__foot > div {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 11px var(--gutter);
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

@media (max-width: 640px) {
  .ddg-megamenu {
    height: calc(100dvh - var(--ddg-appbar-height));
    max-height: calc(100dvh - var(--ddg-appbar-height));
    overscroll-behavior: contain;
  }

  .ddg-megamenu__grid {
    grid-template-columns: 1fr;
    gap: 7px;
    padding-top: var(--space-3);
    padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom));
  }

  .ddg-megamenu .ddg-practicecard {
    display: block;
    min-height: 52px;
    padding: 10px 12px;
    border-radius: var(--radius-sm);
  }

  .ddg-megamenu .ddg-practicecard:hover {
    transform: none;
  }

  .ddg-megamenu .ddg-practicecard__name {
    font-size: var(--text-body-sm);
  }

  .ddg-megamenu .ddg-practicecard__body,
  .ddg-megamenu .ddg-practicecard__more {
    display: none;
  }

  .ddg-megamenu__aside {
    gap: var(--space-2);
    padding-top: var(--space-3);
  }

  .ddg-megamenu__link {
    min-height: 44px;
  }

  .ddg-megamenu__foot > div {
    padding-top: var(--space-2);
    padding-bottom: calc(var(--space-2) + env(safe-area-inset-bottom));
  }

  .ddg-megamenu__foot > div span:last-child {
    display: none;
  }
}

/* Practice card ──────────────────────────────────────────────────────────── */

.ddg-practicecard {
  display: flex;
  flex-direction: column;
  gap: 9px;
  padding: 15px 15px 17px;
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  box-shadow: var(--shadow-xs);
  text-decoration: none;
  transition: var(--transition-lift);
}

.ddg-practicecard:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
  border-color: var(--border-emphasis);
}

@media (prefers-reduced-motion: reduce) {
  .ddg-practicecard:hover {
    transform: none;
  }
}

.ddg-practicecard__head {
  display: flex;
  align-items: center;
  gap: 9px;
}

.ddg-practicecard__glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
  border-radius: var(--radius-sm);
}

.ddg-practicecard__name {
  font-size: var(--text-h4);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}

.ddg-practicecard__body {
  font-size: var(--text-caption);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}

/* Home's density: capability chips and a hue rule. Both are opt-in props, so
   the mega menu's cell is untouched. See the note in PracticeCard.astro.

   The chips are mono because they are a list of names lifted verbatim out of
   the practice's own capability titles, which is machine output in the sense
   the design skill's type section means: not furniture, and not a tracked
   uppercase sans label pretending to be monospaced. They read --text-tertiary
   on --surface-sunken: 5.29:1 on paper and 4.57:1 on ink, measured on the
   shipped tokens. 4.19:1 is the figure that pairing gave BEFORE the token
   generator re-pointed --text-tertiary off --slate-400 — do not quote that one
   here, it reads as if we ship 10px text below AA. */
.ddg-practicecard__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 2px;
}

.ddg-practicecard__tag {
  display: inline-flex;
  align-items: center;
  padding: 3px 8px;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-default);
  background: var(--surface-sunken);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-tertiary);
  white-space: nowrap;
}

/* The rule is a pseudo-element rather than a border-top, so it sits inside the
   card's radius instead of squaring off its top two corners. overflow:hidden
   would do the same and would also clip the hover lift's shadow. */
.ddg-practicecard--accent {
  position: relative;
}

.ddg-practicecard--accent::before {
  content: '';
  position: absolute;
  top: 0;
  left: var(--radius-lg);
  right: var(--radius-lg);
  height: 2px;
  border-radius: 0 0 var(--radius-xs) var(--radius-xs);
  background: var(--ddg-practicecard-hue, var(--border-emphasis));
}

.ddg-practicecard--cyber { --ddg-practicecard-hue: var(--practice-cyber); }
.ddg-practicecard--it { --ddg-practicecard-hue: var(--practice-it); }
.ddg-practicecard--ai { --ddg-practicecard-hue: var(--practice-ai); }
.ddg-practicecard--search { --ddg-practicecard-hue: var(--practice-search); }
.ddg-practicecard--digital { --ddg-practicecard-hue: var(--practice-digital); }
.ddg-practicecard--retail { --ddg-practicecard-hue: var(--practice-retail); }

/* The affordance reads --text-link, not the practice hue.
   ────────────────────────────────────────────────────────────────────────────
   It used to take the card's hue, which put an 11px semibold label in the raw
   plume colour on the card surface. Measured on paper: retail 2.40:1, search
   2.45:1, digital 2.87:1, cyber 4.18:1 — four of five below AA, and in the dark
   theme cyber (3.30) and ai (2.81) fail instead. So there is no single set of
   five hues that works on both grounds.

   The darkened text hues the chips use do not rescue it either: on white they
   give digital 3.80:1 and retail 3.78:1, still short, and on the dark card they
   are WORSE than the bright ones. Fixing it in the hue would mean inventing two
   new brand colours, which is a decision rather than a repair.

   So it follows the rule the greyscale render produced on 2026-08-10: the glyph
   identifies a practice, the hue only decorates it (DESIGN.md). The hue stays on
   the glyph tile, where it is a background rather than small text, and the "View
   practice" affordance reads as what it is — a link — in the token built for
   that, which re-points correctly on both grounds. */
.ddg-practicecard__more {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: 2px;
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  color: var(--text-link);
}
.ddg-practicecard:hover .ddg-practicecard__more { color: var(--text-link-hover); }

/* Practice hue is set per card, on the glyph tile. Six since 2026-08-13, when
   Cyber Security and IT stopped being one practice. */
.ddg-practicecard--cyber .ddg-practicecard__glyph { background: var(--wash-cyber); color: var(--practice-cyber); }
.ddg-practicecard--it .ddg-practicecard__glyph { background: var(--wash-it); color: var(--practice-it); }
.ddg-practicecard--ai .ddg-practicecard__glyph { background: var(--wash-ai); color: var(--practice-ai); }
.ddg-practicecard--search .ddg-practicecard__glyph { background: var(--wash-search); color: var(--practice-search); }
.ddg-practicecard--digital .ddg-practicecard__glyph { background: var(--wash-digital); color: var(--practice-digital); }
.ddg-practicecard--retail .ddg-practicecard__glyph { background: var(--wash-retail); color: var(--practice-retail); }

/* Account menu ───────────────────────────────────────────────────────────── */

.ddg-accountmenu {
  position: absolute;
  right: clamp(14px, 2.4vw, 26px);
  top: calc(100% - 2px);
  z-index: 46;
  width: 288px;
  max-width: calc(100vw - 2 * clamp(14px, 2.4vw, 26px));
  border-radius: var(--radius-lg);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
  animation: var(--anim-rise);
}

.ddg-accountmenu__head {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 15px 16px;
  border-bottom: 1px solid var(--border-default);
  background: var(--surface-page-alt);
}

.ddg-accountmenu__id {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

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

.ddg-accountmenu__email {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.05em;
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ddg-accountmenu__links {
  list-style: none;
  margin: 0;
  display: flex;
  flex-direction: column;
  padding: var(--space-1) var(--space-2) var(--space-2);
}

.ddg-accountmenu__link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: var(--space-2);
  border-radius: var(--radius-xs);
  font-size: var(--text-body-sm);
  color: var(--text-secondary);
  text-decoration: none;
  transition: var(--transition-control);
}

.ddg-accountmenu__link:hover {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

.ddg-accountmenu__foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-3);
  padding: 10px 14px;
  border-top: 1px solid var(--border-default);
  background: var(--surface-page-alt);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.ddg-accountmenu__signout {
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  letter-spacing: normal;
  text-transform: none;
  color: var(--text-secondary);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-accountmenu__signout:hover {
  color: var(--text-danger);
}

/* App footer ─────────────────────────────────────────────────────────────── */

.ddg-appfoot {
  border-top: 1px solid var(--border-default);
  background: var(--surface-page-alt);
}

/* The sitemap columns. Not in the kit — see the prop comment on AppShell. */
.ddg-appfoot__sitemap {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: var(--space-10) var(--gutter) var(--space-6);
  display: grid;
  gap: var(--space-8) var(--space-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
  border-bottom: 1px solid var(--border-default);
}

.ddg-appfoot__col {
  display: flex;
  flex-direction: column;
  gap: 11px;
  min-width: 0;
}

.ddg-appfoot__coltitle {
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.ddg-appfoot__collinks {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.ddg-appfoot__nap {
  display: flex;
  flex-direction: column;
  gap: 5px;
  font-style: normal;
  font-size: var(--text-caption);
  color: var(--text-secondary);
}

.ddg-appfoot__nap a {
  color: var(--text-secondary);
  text-decoration: none;
  transition: var(--transition-control);
}

.ddg-appfoot__nap a:hover {
  color: var(--text-primary);
}

.ddg-appfoot__cta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-top: var(--space-1);
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  color: var(--text-link);
  text-decoration: none;
  transition: var(--transition-control);
}

.ddg-appfoot__cta:hover {
  color: var(--text-link-hover);
}

.ddg-appfoot__top {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 22px var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}

.ddg-appfoot__brand {
  display: flex;
  align-items: center;
  gap: 14px;
}

.ddg-appfoot__brandtext {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.ddg-appfoot__brandname {
  font-size: var(--text-caption);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}

.ddg-appfoot__strapline,
.ddg-appfoot__bottom {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.ddg-appfoot__right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 13px;
}

.ddg-appfoot__links {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 18px;
  flex-wrap: wrap;
  list-style: none;
  margin: 0;
  padding: 0;
}

.ddg-appfoot__link {
  font-size: var(--text-caption);
  color: var(--text-secondary);
  text-decoration: none;
  transition: var(--transition-control);
}

.ddg-appfoot__link:hover {
  color: var(--text-primary);
}

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

/* At one column the right-hand stack is no longer on the right. */
@media (max-width: 720px) {
  .ddg-appfoot__right {
    align-items: flex-start;
    width: 100%;
  }

  .ddg-appfoot__links,
  .ddg-appfoot__status {
    justify-content: flex-start;
  }
}

.ddg-appfoot__bottom {
  border-top: 1px solid var(--border-default);
}

/* The bottom padding clears the Echo dock, and it is not spacing.

   `.ddg-echo-dock` is `position: fixed` at `bottom: clamp(12px, 2vw, 22px)`
   with a 44px launcher, so it occupies the bottom ~66px of the viewport on
   every page. The theme pills sit at the bottom-right of this bar, which is
   exactly where the dock lands once a reader scrolls to the end —
   `elementFromPoint` on the pill's own centre returned the launcher. The
   site's ONLY light/dark control was unclickable at the foot of every page, on
   both viewports, and on a phone it was worse: the dock goes `left: 12px;
   right: 12px` under 460px, so it covers the whole row rather than one corner.

   It renders perfectly and the button is not disabled — the click simply lands
   on something else. Nothing about it looks wrong in a screenshot, which is why
   it took a hit test to find.

   The dock's own offset plus its height, so the row is pushed clear rather than
   nudged. `--space-2` on top of that is the gap between the two.

   `-webkit-fill-available` is not involved and `env(safe-area-inset-bottom)` is
   not needed: the dock already respects the safe area through its own `bottom`,
   so measuring from the dock rather than from the viewport picks it up. */
.ddg-appfoot__bottominner {
  max-width: var(--container-max);
  margin: 0 auto;
  padding: 11px var(--gutter);
  padding-bottom: calc(11px + clamp(12px, 2vw, 22px) + 44px + var(--space-2));
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  gap: var(--space-4);
  align-items: center;
}

.ddg-appfoot__legal {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  min-width: 0;
}

.ddg-appfoot__bottominner > .ddg-themepills {
  justify-self: center;
}

.ddg-appfoot__bottominner > .ddg-textscale {
  justify-self: end;
}

@media (max-width: 720px) {
  .ddg-appfoot__bottominner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    row-gap: var(--space-3);
  }

  .ddg-appfoot__legal {
    grid-column: 1 / -1;
    justify-self: center;
    align-items: center;
    text-align: center;
  }

  .ddg-appfoot__bottominner > .ddg-themepills {
    justify-self: center;
  }

  .ddg-appfoot__bottominner > .ddg-textscale {
    justify-self: center;
  }
}

@media (max-width: 420px) {
  .ddg-appfoot__bottominner {
    grid-template-columns: 1fr;
  }

  .ddg-appfoot__legal,
  .ddg-appfoot__bottominner > .ddg-themepills,
  .ddg-appfoot__bottominner > .ddg-textscale {
    grid-column: 1;
    justify-self: center;
  }
}

/* Theme pills ────────────────────────────────────────────────────────────── */

/* Two aria-pressed buttons, not a Switch: "light" and "dark" are two named
   destinations, and a switch would have to pick one of them to mean "on". */
.ddg-themepills {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
}

.ddg-themepills__pill {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  border: none;
  background: transparent;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-themepills__pill:hover {
  color: var(--text-primary);
}

.ddg-themepills__pill[aria-pressed='true'] {
  background: var(--surface-sunken);
  color: var(--text-primary);
  box-shadow: var(--shadow-xs);
}

/* Text scale ─────────────────────────────────────────────────────────────────
   Sits opposite the theme pills in the footer and borrows their shell so the
   two read as one set of preferences rather than unrelated widgets.

   The readout is `--text-tertiary` at 4.4:1 — the same rung the footer's own
   metadata uses. It is a value, not body copy, and it is announced through
   aria-live regardless of whether it is read visually.

   `min-width` on the readout, and tabular numerals: without both, stepping
   100% → 110% → 125% moves the A+ button sideways under the cursor. */
.ddg-textscale {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 2px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
}

.ddg-textscale__step {
  font-family: var(--font-sans);
  font-size: var(--text-nano);
  font-weight: var(--weight-semibold);
  line-height: 1;
  padding: 4px 9px;
  border-radius: var(--radius-pill);
  border: none;
  background: transparent;
  color: var(--text-tertiary);
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-textscale__step--lg {
  font-size: var(--text-micro);
}

.ddg-textscale__step:hover:not(:disabled) {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

/* Reduced rather than hidden. A control that vanishes at the end of its range
   is a control the reader has to rediscover; one that dims has told them where
   they are. */
.ddg-textscale__step:disabled {
  opacity: 0.4;
  cursor: default;
}

.ddg-textscale__value {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
  min-width: 4ch;
  text-align: center;
  color: var(--text-tertiary);
}

.ddg-textscale__step:focus-visible {
  outline: 2px solid var(--border-focus);
  outline-offset: 2px;
  box-shadow: var(--shadow-focus);
}

/* Modal ──────────────────────────────────────────────────────────────────── */

/* A <dialog> gets NO inherited colour: it is in the top layer, outside the
   flow, and the UA sheet sets `color: CanvasText` on it. On a dark theme that
   is near-black text on a dark card — legible in the DOM, invisible on screen,
   and 200 all the way. Both properties are set explicitly for that reason. */
.ddg-modal {
  padding: 0;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-xl);
  background: var(--surface-card);
  color: var(--text-primary);
  box-shadow: var(--shadow-lg);
  font-family: var(--font-sans);
  overflow: hidden;
}

.ddg-modal[open] {
  animation: var(--anim-rise);
}

.ddg-modal::backdrop {
  background: var(--surface-overlay);
}

@supports (backdrop-filter: blur(1px)) {
  .ddg-modal::backdrop {
    backdrop-filter: blur(3px);
  }
}

.ddg-modal__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 20px 22px 16px;
  border-bottom: 1px solid var(--border-default);
}

.ddg-modal__titles {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.ddg-modal__title {
  font-size: var(--text-h2);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
  margin: 0;
}

.ddg-modal__body {
  padding: 20px 22px;
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.ddg-modal__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-top: var(--space-1);
}

.ddg-modal__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  color: var(--text-secondary);
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-modal__close:hover {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

/* Sizes. A modal <dialog> is centred by the UA; only width is ours. */
.ddg-modal--sm { width: min(560px, calc(100vw - 40px)); }
.ddg-modal--md { width: min(680px, calc(100vw - 40px)); }
.ddg-modal--lg { width: min(920px, calc(100vw - 40px)); }

/* Legal reader ─────────────────────────────────────────────────────────────
   A native dialog aligned to the right edge. The page behind it is inert and
   focus stays inside, while the body has its own scroll so the head and the
   full-page fallback remain available through a long policy. */
.ddg-legaldrawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(720px, 100vw);
  height: 100dvh;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  color: var(--text-primary);
  background: var(--surface-panel);
  border: 0;
  border-left: 1px solid var(--border-default);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.ddg-legaldrawer[open] { animation: ddg-drawer-in var(--duration-base) var(--ease-out); }
.ddg-legaldrawer::backdrop {
  background: var(--surface-overlay);
  animation: ddg-fade-in var(--duration-fast) var(--ease-out);
}
.ddg-legaldrawer__panel {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  height: 100%;
  background: var(--surface-panel);
}
.ddg-legaldrawer__head,
.ddg-legaldrawer__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-5) var(--space-6);
  background: var(--surface-panel);
}
.ddg-legaldrawer__head { border-bottom: 1px solid var(--border-default); }
.ddg-legaldrawer__foot { border-top: 1px solid var(--border-default); }
.ddg-legaldrawer__eyebrow {
  display: block;
  margin-bottom: var(--space-1);
  color: var(--text-tertiary);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}
.ddg-legaldrawer__title {
  margin: 0;
  color: var(--text-primary);
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}
.ddg-legaldrawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  padding: 0;
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: var(--transition-control);
}
.ddg-legaldrawer__close:hover {
  color: var(--text-primary);
  border-color: var(--border-emphasis);
}
.ddg-legaldrawer__close:focus-visible { box-shadow: var(--shadow-focus); }
.ddg-legaldrawer__scroll { overflow: auto; overscroll-behavior: contain; }
.ddg-legaldrawer__content { padding: var(--space-6); }
.ddg-legaldrawer__content [data-ddg-drawer-content] {
  display: grid;
  gap: var(--space-4);
  max-width: none;
  margin: 0;
  padding: 0;
  color: var(--text-secondary);
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}
.ddg-legaldrawer__content [data-ddg-drawer-content] > * { margin-block: 0; }
.ddg-legaldrawer__content h1,
.ddg-legaldrawer__content h2,
.ddg-legaldrawer__content h3,
.ddg-legaldrawer__content h4,
.ddg-legaldrawer__content h5 {
  margin: var(--space-5) 0 var(--space-2);
  color: var(--text-primary);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}
.ddg-legaldrawer__content h1 { font-size: var(--text-h2); }
.ddg-legaldrawer__content h2 { font-size: var(--text-h3); }
.ddg-legaldrawer__content h3,
.ddg-legaldrawer__content h4,
.ddg-legaldrawer__content h5 { font-size: var(--text-h4); }
.ddg-legaldrawer__content p,
.ddg-legaldrawer__content li {
  color: var(--text-secondary);
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
}
.ddg-legaldrawer__content p { margin: 0 0 var(--space-4); }
.ddg-legaldrawer__content ul,
.ddg-legaldrawer__content ol {
  display: grid;
  gap: var(--space-2);
  margin: 0 0 var(--space-4);
  padding-left: var(--space-5);
}
.ddg-legaldrawer__content a { color: var(--text-link); }
.ddg-legaldrawer__content .ddg-panel {
  padding: var(--space-5) 0;
  background: transparent;
  border: 0;
}
.ddg-legaldrawer__content .ddg-panel__inner { padding: 0; }
.ddg-legaldrawer__content .ddg-card { box-shadow: none; }
.ddg-legaldrawer__content .ax-grid { display: grid; gap: var(--space-4); }
.ddg-legaldrawer__loading { color: var(--text-tertiary); }
.ddg-legaldrawer__full {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-link);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  text-decoration: none;
}
.ddg-legaldrawer__full:hover { color: var(--text-link-hover); }

@keyframes ddg-drawer-in {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}
@keyframes ddg-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .ddg-legaldrawer[open],
  .ddg-legaldrawer::backdrop { animation: none; }
}
@media (max-width: 640px) {
  .ddg-legaldrawer__head,
  .ddg-legaldrawer__foot,
  .ddg-legaldrawer__content { padding-inline: var(--space-4); }
}

/* Command palette ────────────────────────────────────────────────────────── */

/* Sits high rather than centred: results grow downward, and a centred palette
   moves its own field as it fills. */
/* NO `display` on the base rule. A <dialog> without [open] is display:none by
   the UA sheet, and any author display here beats it — which rendered the whole
   command palette into the page, over the hero, on every surface using
   AppShell. It looked like a JS failure and was not: nothing had run yet.
   `flex-direction` is safe to state here because it does nothing until the
   [open] rule below supplies the display. */
.ddg-modal--command {
  margin-top: 11vh;
  max-height: 74vh;
  flex-direction: column;
}

.ddg-modal--command[open] {
  display: flex;
}

.ddg-cmd__field {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 15px 17px;
  border-bottom: 1px solid var(--border-default);
  flex: 0 0 auto;
}

.ddg-cmd__input {
  flex: 1 1 auto;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  font-family: var(--font-sans);
  font-size: var(--text-lead);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}

.ddg-cmd__esc {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 4px 8px;
  border-radius: var(--radius-xs);
  border: 1px solid var(--border-default);
  background: var(--surface-sunken);
  color: var(--text-tertiary);
  cursor: pointer;
}

.ddg-cmd__esc-mobile {
  display: none;
}

.ddg-cmd__results {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--space-2);
}

.ddg-cmd__grouplabel {
  display: block;
  padding: var(--space-2) 10px var(--space-1);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.ddg-cmd__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.ddg-cmd__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 10px 11px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: var(--transition-control);
}

.ddg-cmd__item:hover,
.ddg-cmd__item.is-active {
  background: var(--surface-sunken);
}

.ddg-cmd__glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex: 0 0 auto;
  border-radius: var(--radius-xs);
  background: var(--surface-sunken);
  color: var(--text-secondary);
}

.ddg-cmd__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1 1 auto;
}

.ddg-cmd__title {
  font-size: var(--text-body-sm);
  font-weight: var(--weight-medium);
  color: var(--text-primary);
}

.ddg-cmd__meta {
  font-size: var(--text-caption);
  color: var(--text-tertiary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ddg-cmd__enter {
  color: var(--text-tertiary);
  opacity: 0.6;
  flex: 0 0 auto;
}

.ddg-cmd__empty {
  padding: 34px 14px;
  text-align: center;
}

.ddg-cmd__state {
  padding: 34px 14px;
  text-align: center;
  font-size: var(--text-body-sm);
  color: var(--text-secondary);
}

.ddg-cmd__foot {
  flex: 0 0 auto;
  padding: 10px 16px;
  border-top: 1px solid var(--border-default);
  background: var(--surface-page-alt);
  display: flex;
  justify-content: space-between;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

@media (max-width: 640px) {
  .ddg-modal--command {
    inset: 0;
    width: 100%;
    max-width: none;
    height: 100dvh;
    max-height: 100dvh;
    margin: 0;
    border: 0;
    border-radius: 0;
  }

  .ddg-cmd__field {
    min-height: calc(60px + env(safe-area-inset-top));
    padding: calc(8px + env(safe-area-inset-top)) var(--gutter) 8px;
  }

  .ddg-cmd__input {
    font-size: var(--text-body);
  }

  .ddg-cmd__esc {
    min-width: 44px;
    min-height: 44px;
  }

  .ddg-cmd__esc-desktop {
    display: none;
  }

  .ddg-cmd__esc-mobile {
    display: inline;
  }

  .ddg-cmd__results {
    padding: var(--space-2) max(var(--space-2), env(safe-area-inset-left));
  }

  .ddg-cmd__item {
    min-height: 52px;
  }

  .ddg-cmd__foot {
    padding: 12px var(--gutter) calc(12px + env(safe-area-inset-bottom));
  }

  .ddg-cmd__foot > span:last-child {
    display: none;
  }
}

/* Sign-in modal ──────────────────────────────────────────────────────────── */

.ddg-authmodal__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
}

.ddg-authmodal__form {
  padding: 26px 26px 24px;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.ddg-authmodal__aside {
  position: relative;
  padding: 26px;
  background: var(--surface-ink-deep);
  color: var(--text-inverse);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-5);
}

.ddg-authmodal__mesh {
  position: absolute;
  inset: -10%;
  background: var(--mesh-ink);
  opacity: 0.7;
  animation: var(--anim-drift);
  pointer-events: none;
}

.ddg-authmodal__grid-lines {
  position: absolute;
  inset: 0;
  background: var(--grid-lines-inverse);
  background-size: var(--grid-size) var(--grid-size);
  -webkit-mask-image: radial-gradient(100% 70% at 50% 0%, #000 10%, transparent 78%);
  mask-image: radial-gradient(100% 70% at 50% 0%, #000 10%, transparent 78%);
  pointer-events: none;
}

.ddg-authmodal__asideinner {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.ddg-authmodal__asidetitle {
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
}

.ddg-authmodal__asidebody {
  font-size: var(--text-caption);
  line-height: var(--leading-snug);
  color: var(--text-inverse-muted);
  max-width: 34ch;
}

.ddg-authmodal__points {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}

.ddg-authmodal__point {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: var(--text-caption);
  color: var(--text-inverse-muted);
}

.ddg-authmodal__point::before {
  content: '';
  width: 5px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--accent-cyan);
  flex: 0 0 auto;
}

/* Echo drawer ────────────────────────────────────────────────────────────── */

/* Not a <dialog>. Echo reads the page you are on, so the page must stay
   readable and reachable behind it — showModal() would make it inert. */
.ddg-echodrawer {
  position: fixed;
  top: 70px;
  right: 12px;
  bottom: 12px;
  z-index: 52;
  width: min(420px, calc(100% - 24px));
  animation: var(--anim-slide-in);
}

@media (max-width: 560px) {
  .ddg-echodrawer {
    top: 68px;
    left: 12px;
    width: auto;
  }
}

/* Essay ──────────────────────────────────────────────────────────────────── */

/* From ui_kits/website/EssayScreen.jsx. Every number below is that file's,
   transcribed rather than approximated — this is the one page type where the
   measure, the rhythm between blocks and the weight of the lead are the design
   rather than decoration. */

/* The reading rule. The ONE fixed element the design allows on an essay. */
.ddg-essay__progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  /* 90, matching .ddg-loadbar--fixed, and not a number picked to be large.
     .ddg-appbar is position:sticky at z-index 40 and is the shell's first
     child, so it is pinned to the viewport top from the first pixel of scroll.
     At 30 this rule spent the whole page behind it — visible only as a smear
     through the app bar's own backdrop-filter, which reads as a rendering
     artefact rather than as progress. */
  z-index: 90;
  background: var(--plume-gradient);
  transform-origin: left;
  /* Server-rendered at zero and invisible: with the runtime absent this is a
     hairline that never moves, which is better than one stuck at full. */
  transform: scaleX(0);
  opacity: 0;
  transition: transform 120ms linear;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .ddg-essay__progress {
    transition: none;
  }
}

.ddg-essay__crumb {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 34px;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

.ddg-essay__crumb ol {
  display: flex;
  align-items: center;
  gap: 10px;
  list-style: none;
  margin: 0;
  padding: 0;
  flex-wrap: wrap;
}

.ddg-essay__crumb a {
  color: inherit;
  text-decoration: none;
}

.ddg-essay__crumb a:hover {
  color: var(--text-primary);
}

/* --text-brand, not --ink-700. The essay header is a theme-FOLLOWING paper
   panel, and --ink-700 never re-points, so on dark this crumb was #184058 on
   #08151D — 1.69:1, the same absent-text trap that cost .ddg-arrowlink--ink.
   --text-brand is #184058 on light and #8CC6F0 on dark, so the intent is
   unchanged and the crumb survives the ground. */
.ddg-essay__crumb li:last-child {
  color: var(--text-brand);
}

/* Weight 700, not --weight-display's 600. An essay title is the one place the
   system asks for the heaviest cut it has. */
.ddg-essay__title {
  font-family: var(--font-display);
  font-size: var(--text-display-2);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-display);
  line-height: var(--leading-display);
  margin: 18px 0 0;
  color: var(--text-primary);
  /* Eight of the Harbour posts break their own title with an <em> —
     `They said AI<em>wasn't ready.</em>` — which is a line break wearing an
     emphasis tag, and it breaks at whatever width the author was looking at.
     This template takes the title as plain text, so the break has to come from
     here. Harbour's .post-title balanced too; it is not a new behaviour. */
  text-wrap: balance;
}

/* Light, and narrower than the body. A lead paragraph is a different voice,
   not a bigger one. */
.ddg-essay__dek {
  margin-top: 24px;
  font-size: var(--text-lead);
  font-weight: var(--weight-light);
  line-height: var(--leading-snug);
  color: var(--slate-600);
  max-width: var(--measure-lead);
}

.ddg-essay__byline {
  margin-top: 32px;
  padding-top: 18px;
  border-top: 1px solid var(--border-default);
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-tertiary);
}

/* Same trap as the breadcrumb's last crumb above: the byline sits on a
   theme-following panel, so it needs the token that re-points. */
.ddg-essay__author {
  color: var(--text-brand);
}

/* The prose column. gap, not margins — the space between blocks is one number
   and a <p> next to a <blockquote> gets the same as two paragraphs. */
.ddg-essay__body {
  display: flex;
  flex-direction: column;
  gap: 26px;
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--text-primary);
}

.ddg-essay__body > * {
  margin: 0;
}

/* --text-h1 for an h2, because the h1 above is a display size. The heading
   scale on an essay runs one step up from the document outline. */
.ddg-essay__body h2 {
  font-size: var(--text-h1);
  letter-spacing: var(--tracking-display);
  margin: 14px 0 0;
}

.ddg-essay__body h3 {
  font-size: var(--text-h2);
  letter-spacing: var(--tracking-tight);
  margin: 8px 0 0;
}

.ddg-essay__body a {
  color: var(--text-link);
}

.ddg-essay__body a:hover {
  color: var(--text-link-hover);
}

.ddg-essay__body ul,
.ddg-essay__body ol {
  padding-left: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.ddg-essay__body strong {
  font-weight: var(--weight-semibold);
}

/* What the 24 Harbour posts put in their prose ───────────────────────────────
 *
 * Each post carries ~200 lines of `<style is:global>`, which is the same
 * pattern written twenty-four times and is why every copy of it drifted. The
 * rules below are everything those blocks style that the essay template did
 * not already cover, and nothing they style that it does — the hero, byline,
 * back link and footer nav are `Essay.astro`'s job now, so they are dropped
 * rather than ported.
 *
 * Audited from the markup, not from the stylesheets: `hr` in 12 posts,
 * `blockquote` in 8, `.post-signoff` in 8, `figure`/`img` and `.embed-card`
 * in one each. `em`, `code` and `li` need no rule — the UA and the token
 * layer already have them. Every class here is scoped to the body: they are
 * generic words in a global namespace, and `.frame` in particular would
 * collide with the first component that wants it.
 */

/* Harbour set this in a serif italic on a coral bar. There is no serif in this
   system, so it takes `.ddg-quote`'s treatment instead — display face, medium,
   one step up from body — with the rule moved to a full-width hairline. */
.ddg-essay__body blockquote {
  margin: var(--space-2) 0;
  padding: var(--space-5) 0 var(--space-5) var(--space-6);
  border-left: 2px solid var(--border-emphasis);
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}
.ddg-essay__body blockquote p { margin: 0; }

/* A section break in the prose, not a border. It gets its own air because the
   body's flex gap is tuned for paragraphs. */
.ddg-essay__body hr {
  border: none;
  border-top: 1px solid var(--border-default);
  margin: var(--space-5) 0;
  width: 100%;
}

.ddg-essay__body img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-lg);
}

.ddg-essay__body figure { margin: 0; }
.ddg-essay__body figcaption {
  margin-top: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: var(--tracking-wide);
  line-height: var(--leading-loose);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.ddg-essay__body figcaption a { color: inherit; text-decoration: underline; }
.ddg-essay__body figcaption a:hover { color: var(--text-link-hover); }

/* A screenshot of a white interface needs its own white card, or it burns a
   hole through the dark theme. `--neutral-0` re-points under dark, so the mat
   is `--text-inverse`, which is white on both grounds. The min-width and the
   scroll are the point: a 560px screenshot squeezed into a phone column is
   not a smaller screenshot, it is an unreadable one. */
.ddg-essay__body .frame {
  display: block;
  padding: var(--space-3);
  background: var(--text-inverse);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
  overflow-x: auto;
}
.ddg-essay__body .frame img {
  margin: 0;
  min-width: 560px;
  border-radius: var(--radius-xs);
}

/* The author's closing note. A rule above it, and a step down in size and
   weight — it is about the piece rather than part of it. */
.ddg-essay__body .post-signoff {
  margin-top: var(--space-4);
  padding-top: var(--space-6);
  border-top: 1px solid var(--border-default);
  font-size: var(--text-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}

/* An aside carrying a reference — one post uses it. `.ddg-card--sunken`'s box. */
.ddg-essay__body .embed-card {
  padding: var(--space-5);
  background: var(--surface-sunken);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-lg);
}
.ddg-essay__body .embed-card > * + * { margin-top: var(--space-2); }

.ddg-essay__tags {
  margin-top: 40px;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.ddg-essay__author-card {
  margin-top: 44px;
}

.ddg-essay__author-row {
  display: flex;
  gap: 22px;
  align-items: flex-start;
}

.ddg-essay__author-mark {
  width: 72px;
  flex: 0 0 auto;
  opacity: 0.9;
}

.ddg-essay__author-text {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.ddg-essay__author-name {
  font-size: var(--text-h2);
  font-weight: var(--weight-semibold);
}

.ddg-essay__author-bio {
  font-size: var(--text-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}

/* The edge-injected author card ──────────────────────────────────────────────
 *
 * `AuthorBio.astro` renders an empty `[data-author-bio]` shell and worker.js
 * fills it from ddg_billing on the way out, so a bio edited in Axion appears
 * without a rebuild. That is a live feature with its own test suite
 * (scripts/lib/author-bio.test.mjs), which is why the essay's hard-coded
 * author paragraph is a fallback and this is what a post actually renders.
 *
 * Ported from site.css the same way the .engage-* block was: the class names
 * are Harbour's and stay Harbour's, because ONE component serves both systems
 * and the injector matches on these exact names — renaming them would strip
 * the card off every post the moment the worker filled it. Only the values are
 * the new system's. site.css keeps its own copy until the last Harbour page
 * goes; the two are never on one page.
 *
 * Specificity note: `.author-bio[hidden]` (0,2,0) must keep beating
 * `.author-bio` (0,1,0), or the safe-default hidden shell paints an empty
 * bordered box under every article when the worker does not run. */
.author-bio[hidden] {
  display: none;
}

.author-bio {
  display: block;
  font-family: var(--font-sans);
}

.author-bio__inner {
  display: grid;
  grid-template-columns: 72px minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
  /* Harbour's 28px is not on the 4px scale and --space-7 does not exist;
     --space-8 is the nearest step that sits on the editorial rhythm. */
  padding-top: var(--space-8);
  border-top: 1px solid var(--border-default);
}

/* No portrait uploaded: the text takes the whole measure rather than sitting
   beside an empty 72px column. */
.author-bio__inner:not(:has(.author-bio__portrait)) {
  grid-template-columns: minmax(0, 1fr);
}

.author-bio__portrait {
  width: 72px;
  height: 72px;
  border-radius: var(--radius-pill);
  object-fit: cover;
  background: var(--surface-sunken);
  border: 1px solid var(--border-default);
}

/* Sans, not mono. A tracked uppercase label looks monospaced and is not —
   mono in this system is data and machine output, not furniture. */
.author-bio__eyebrow {
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin: 0 0 6px;
}

/* The system has no serif; Harbour set this in italic Newsreader. Weight and
   negative tracking do that job here. */
.author-bio__name {
  font-size: var(--text-h1);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  color: var(--text-primary);
  margin: 0;
}

.author-bio__headline {
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin: 6px 0 0;
}

.author-bio__body {
  margin-top: var(--space-4);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--text-secondary);
}

.author-bio__body p {
  margin: 0 0 0.9em;
}

.author-bio__body p:last-child {
  margin-bottom: 0;
}

.author-bio__attrs {
  list-style: none;
  margin: var(--space-5) 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-6);
  font-size: var(--text-body-sm);
  color: var(--text-secondary);
}

.author-bio__attrs i {
  font-style: normal;
  font-size: var(--text-nano);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-right: 6px;
}

.author-bio__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px var(--space-5);
  margin: var(--space-4) 0 0;
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}

.author-bio__loc {
  color: var(--text-tertiary);
}

/* --text-link, not a raw hue: this sits on the essay's theme-following panel
   and --text-link re-points on dark, which --ink-700 does not. */
.author-bio__link {
  color: var(--text-link);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color var(--duration-base) var(--ease-standard);
}

.author-bio__link:hover {
  border-bottom-color: var(--text-link);
}

@media (max-width: 640px) {
  .author-bio__inner {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-5);
  }
  .author-bio__portrait {
    width: 60px;
    height: 60px;
  }
}

.ddg-essay__more {
  margin-top: 28px;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 22px;
}

/* Two columns is a desktop decision. Below 720px the cards are 160px wide and
   the deks wrap to five lines each. */
@media (max-width: 720px) {
  .ddg-essay__more {
    grid-template-columns: 1fr;
  }

  .ddg-essay__author-row {
    flex-direction: column;
    gap: var(--space-4);
  }
}

/* Engagement ─────────────────────────────────────────────────────────────── */

/* The blog engagement block — like, share, subscribe, comment. Ported out of
 * site.css on 2026-08-10 so `Essay.astro` can carry it.
 *
 * WHY THESE CLASS NAMES ARE NOT `.ddg-` PREFIXED
 * ----------------------------------------------
 * `src/components/PostEngagement.astro` renders one set of markup for both
 * systems, and twenty-four Harbour posts still mount it. Renaming the classes
 * would have to land in the component, which would strip the styling off all
 * twenty-four at a 200. `Base.astro` inlines site.css and `Site.astro` inlines
 * this file — the two are never on one page, so the duplicate definitions
 * cannot collide. site.css's copy dies with site.css.
 *
 * The VALUES are not Harbour's. That block was a coral pill, a twilight panel
 * and a serif italic byline. The controls below carry this system's metrics —
 * `.ddg-btn--secondary`'s box for the like, `.ddg-iconbtn--outline`'s for the
 * share buttons, `.ddg-control`'s for the fields — so the block reads as the
 * rest of the site rather than as a widget bolted to it.
 *
 * `engagement.js` also writes `.sk-shimmer` onto the comment skeleton spans.
 * It needs no rule here: the geometry classes it writes alongside it (`.l`,
 * `.l-meta`, `.l-1`, `.l-2`) carry the shimmer themselves, from the same
 * `--shimmer-fill` / `--anim-shimmer` tokens as `.ddg-skel`.
 */

.post-engage {
  margin-top: 48px;
  font-family: var(--font-sans);
}

/* Both honeypots. Unstyled they are two visible text inputs that a reader can
   type into, and the API rejects whatever they send as spam — silently, at a
   200. This one rule is the difference. */
.engage-hp {
  position: absolute !important;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* Like + share bar ───────────────────────────────────────────────────────── */

.engage-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding: var(--space-5) 0;
  border-top: 1px solid var(--border-default);
  border-bottom: 1px solid var(--border-default);
}

/* `.ddg-btn--secondary` at `--md`, in a button the component cannot put a
   class on. Same padding, same box, same tokens. */
.engage-like {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 9px 18px;
  min-height: 40px;
  background: var(--neutral-0);
  color: var(--action-secondary-fg);
  border: 1px solid var(--action-secondary-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-xs);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  cursor: pointer;
  transition: var(--transition-control), transform var(--duration-instant) var(--ease-standard);
}
.engage-like:hover {
  background: var(--action-secondary-bg-hover);
  border-color: var(--slate-300);
  box-shadow: var(--shadow-sm);
}
.engage-like:active { transform: translateY(1px); }

.engage-like__heart {
  display: inline-flex;
  color: inherit;
  transition: transform var(--duration-base) var(--ease-standard);
}
/* A count is machine output, so it is mono and tabular — the button must not
   change width when 9 becomes 10. */
.engage-like__count {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  min-width: 1.4em;
  text-align: right;
}

/* Liked is `--text-link`, not a red heart. Red in this system is
   `--status-danger` and nothing else, and the raw ramps do not re-point in
   dark — `--ink-700` here would be navy on navy. `--text-link` flips with the
   ground and already means "this is the interactive one". */
.engage-like.is-liked {
  color: var(--text-link);
  border-color: var(--text-link);
  background: var(--surface-sunken);
}
.engage-like.is-liked .engage-like__heart svg { fill: currentColor; }
.engage-like.is-pulse .engage-like__heart { transform: scale(1.25); }

/* Shimmer pill until the count lands. `--anim-shimmer` is `none` under
   prefers-reduced-motion at the token layer, so this needs no media query. */
.engage-like.is-count-loading .engage-like__count {
  display: inline-block;
  min-width: 18px;
  color: transparent;
  border-radius: var(--radius-xs);
  background: var(--shimmer-fill);
  background-size: 220% 100%;
  animation: var(--anim-shimmer);
}

.engage-share {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
/* An eyebrow: sans, not mono. A tracked uppercase label looks monospaced and
   is not — see the type rules. */
.engage-share__label {
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
  margin-right: var(--space-1);
}

/* `.ddg-iconbtn--outline` at `--md`. Square, not circular: nothing in this
   system is pill-shaped by default — that was Harbour. */
.engage-share__btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  background: var(--neutral-0);
  color: var(--slate-600);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-xs);
  cursor: pointer;
  transition: var(--transition-control);
}
.engage-share__btn:hover {
  background: var(--neutral-100);
  border-color: var(--neutral-300);
  color: var(--ink-900);
}
.engage-share__btn:active { transform: translateY(1px); }
/* The button is 36px and the minimum target is 44px. An invisible ::after
   makes up the difference without moving anything on the page. */
.engage-share__btn::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

.engage-share__copied {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  padding: 5px 9px;
  background: var(--surface-ink);
  color: var(--text-inverse);
  border-radius: var(--radius-xs);
  box-shadow: var(--highlight-top), var(--shadow-md);
  font-family: var(--font-sans);
  font-size: var(--text-nano);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
}

/* Subscribe ──────────────────────────────────────────────────────────────── */

/* Ink in BOTH themes, which is why everything inside it is written against
   the constant tokens. `--neutral-0` and `--ink-900` re-point under
   [data-theme="dark"]; `--surface-ink`, `--text-inverse` and
   `--text-inverse-muted` do not. */
.engage-sub {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-8);
  align-items: center;
  margin: var(--space-10) 0;
  padding: var(--space-8) var(--space-10);
  background: var(--surface-ink);
  border-radius: var(--radius-lg);
  box-shadow: var(--highlight-top), var(--shadow-md);
  color: var(--text-inverse);
}
.engage-sub.is-done {
  display: block;
  text-align: center;
}

.engage-sub__eyebrow {
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-inverse-muted);
  margin-bottom: var(--space-3);
}
.engage-sub__title {
  font-size: var(--text-display-3);
  font-weight: var(--weight-display);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  color: var(--text-inverse);
  margin: 0 0 10px;
}
.engage-sub__lead {
  font-size: var(--text-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-inverse-muted);
}

.engage-sub__form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}
/* `.ddg-control` re-pointed for ink. The system has no inverse variant of it,
   and a paper field here would be the brightest thing on the page. */
.engage-sub__form input[type='email'] {
  flex: 1 1 180px;
  min-width: 0;
  padding: 10px 13px;
  min-height: 42px;
  background: var(--surface-panel-inverse);
  border: 1px solid var(--border-inverse-default);
  border-radius: var(--radius-sm);
  color: var(--text-inverse);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  letter-spacing: var(--tracking-normal);
  line-height: 1.4;
  outline: none;
  transition: var(--transition-control);
}
.engage-sub__form input::placeholder { color: var(--text-inverse-muted); }
/* Cyan, not `--border-focus`. The blue ring is tuned for paper; on ink it is
   the same move `.ddg-console :focus-visible` already makes. */
.engage-sub__form input:focus {
  background: rgba(255, 255, 255, 0.1);
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px rgba(0, 184, 208, 0.28);
}

/* White on ink, and the label colour is `--surface-ink` rather than
   `--ink-900` for the reason above: on a panel that does not change with the
   theme, a token that does would put light text on a white button. */
.engage-sub__form button {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 10px 20px;
  min-height: 42px;
  background: var(--text-inverse);
  color: var(--surface-ink);
  border: 1px solid var(--text-inverse);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-sm);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  cursor: pointer;
  transition: var(--transition-control), transform var(--duration-instant) var(--ease-standard);
}
.engage-sub__form button:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.86);
  border-color: rgba(255, 255, 255, 0.86);
}
.engage-sub__form button:active:not(:disabled) { transform: translateY(1px); }
.engage-sub__form button:disabled { opacity: 0.55; cursor: not-allowed; }
.engage-sub__form button:focus-visible { outline-color: var(--accent-cyan); }

.engage-sub__status {
  flex: 1 1 100%;
  margin: var(--space-1) 0 0;
  min-height: 1em;
  font-size: var(--text-caption);
  color: var(--text-inverse-muted);
}
/* The status hues are the console health pill's, not the raw accents:
   `--accent-red` measures about 3.7:1 on this panel, which is under the floor
   for a line of small text that is only ever read when something went wrong. */
.engage-sub__status[data-tone='error'] { color: #f09a90; }
.engage-sub__status[data-tone='ok'] { color: #b9dca6; }

/* Comments ───────────────────────────────────────────────────────────────── */

.engage-comments { margin: var(--space-12) 0 var(--space-2); }

.engage-comments__title {
  font-size: var(--text-h1);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-6);
}
.engage-comments__count {
  font-family: var(--font-mono);
  font-size: 0.62em;
  font-weight: var(--weight-regular);
  color: var(--text-tertiary);
}

.engage-comments__list {
  list-style: none;
  margin: 0 0 var(--space-8);
  padding: 0;
}
.engage-comments__item {
  padding: var(--space-5) 0;
  border-top: 1px solid var(--border-default);
}
.engage-comments__item:first-child { border-top: none; }
.engage-comments__meta {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-bottom: 7px;
}
.engage-comments__author {
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-primary);
}
.engage-comments__date {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.engage-comments__body {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--text-secondary);
  white-space: pre-wrap;
}
.engage-comments__empty {
  padding: 4px 0 var(--space-6);
  color: var(--text-tertiary);
}

/* `.ddg-control`, for inputs the component cannot put a class on. */
.engage-comments__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.engage-comments__form input,
.engage-comments__form textarea {
  width: 100%;
  padding: 10px 13px;
  min-height: 42px;
  color: var(--text-primary);
  background: var(--neutral-50);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 1px 2px rgba(24, 64, 88, 0.045);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  letter-spacing: var(--tracking-normal);
  line-height: 1.4;
  outline: none;
  transition: var(--transition-control);
}
.engage-comments__form input::placeholder,
.engage-comments__form textarea::placeholder { color: var(--text-tertiary); }
.engage-comments__form input:hover,
.engage-comments__form textarea:hover { border-color: var(--slate-300); }
.engage-comments__form input:focus,
.engage-comments__form textarea:focus {
  background: var(--neutral-0);
  border-color: var(--border-focus);
  box-shadow: var(--shadow-focus);
}
.engage-comments__form textarea {
  resize: vertical;
  min-height: 84px;
  line-height: var(--leading-snug);
  padding: 10px 12px;
}

.engage-comments__actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
}
.engage-comments__note {
  font-size: var(--text-caption);
  color: var(--text-tertiary);
}

/* `.ddg-btn--primary` at `--md`. */
.engage-comments__form button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 9px 18px;
  min-height: 40px;
  background: var(--action-primary-bg);
  color: var(--action-primary-fg);
  border: 1px solid var(--action-primary-bg);
  border-radius: var(--radius-sm);
  box-shadow: var(--highlight-top), var(--shadow-xs);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  cursor: pointer;
  transition: var(--transition-control), transform var(--duration-instant) var(--ease-standard);
}
.engage-comments__form button:hover:not(:disabled) {
  background: var(--action-primary-bg-hover);
  border-color: var(--action-primary-bg-hover);
  box-shadow: var(--highlight-top), var(--shadow-md);
}
.engage-comments__form button:active:not(:disabled) { transform: translateY(1px); }
.engage-comments__form button:disabled {
  background: var(--action-disabled-bg);
  color: var(--action-disabled-fg);
  border-color: var(--action-disabled-bg);
  box-shadow: none;
  cursor: not-allowed;
}

.engage-comments__status {
  margin: 2px 0 0;
  min-height: 1em;
  font-size: var(--text-caption);
  color: var(--text-tertiary);
}
.engage-comments__status[data-tone='error'] { color: var(--text-danger); }
/* --accent-green fails contrast as small text on paper; #4e8a38 is the value
   the system already uses for a positive delta. */
.engage-comments__status[data-tone='ok'] { color: var(--wash-green-fg); }

/* Comment placeholder rows, until the fetch resolves. Six at most, per the
   skeleton rule — engagement.js draws three. */
.engage-comments__sk {
  list-style: none;
  padding: var(--space-4) 0;
  border-top: 1px solid var(--border-default);
}
.engage-comments__sk:first-child { border-top: 0; }
.engage-comments__sk .l {
  display: block;
  height: 12px;
  margin-bottom: 9px;
  border-radius: var(--radius-xs);
  background: var(--shimmer-fill);
  background-size: 220% 100%;
  animation: var(--anim-shimmer);
}
.engage-comments__sk .l-meta { width: 150px; height: 11px; margin-bottom: 13px; }
.engage-comments__sk .l-1 { width: 96%; }
.engage-comments__sk .l-2 { width: 78%; margin-bottom: 0; }

/* In flight ──────────────────────────────────────────────────────────────── */

/* `ddgSetBusy()` disables the button and adds .is-busy. The ring LEADS the
   label rather than replacing it: swapping the label either changes the
   button's width mid-request or hides it with `color: transparent`, which
   also kills the currentColor the ring is drawn in. */
.engage-sub__form button.is-busy,
.engage-comments__form button.is-busy {
  position: relative;
  pointer-events: none;
}
.engage-sub__form button.is-busy::before,
.engage-comments__form button.is-busy::before {
  content: '';
  display: inline-block;
  width: 13px;
  height: 13px;
  margin-right: 8px;
  vertical-align: -2px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: var(--radius-pill);
  animation: var(--anim-spin);
}
/* A ring that cannot turn is a broken circle, so it closes instead. The state
   is still carried by aria-busy either way. */
@media (prefers-reduced-motion: reduce) {
  .engage-sub__form button.is-busy::before,
  .engage-comments__form button.is-busy::before {
    border-top-color: currentColor;
    opacity: 0.5;
  }
}

@media (max-width: 720px) {
  .engage-sub {
    grid-template-columns: 1fr;
    gap: var(--space-6);
    padding: var(--space-6);
  }
  .engage-bar { gap: var(--space-3); }
}

/* ComingSoon ─────────────────────────────────────────────────────────────── */

/* The body of a placeholder page. Here rather than in a `<style>` block on the
   component, because every other component in this system is laid out here and
   one exception is how a second CSS system starts. The classes are `.ddg-`
   prefixed for the same reason: `.stub` in the global namespace collides with
   the first page that wants that word. */

.ddg-stub {
  max-width: var(--measure-prose);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Dashed, because the border is saying "not finished". It is the one place in
   the system that uses a dashed edge, and that is the point. */
.ddg-stub__note {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  margin: 0;
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  border: 1px dashed var(--border-emphasis);
  background: var(--surface-sunken);
  font-size: var(--text-body-sm);
  line-height: var(--leading-snug);
  color: var(--text-secondary);
}

.ddg-stub__note .ddg-icon {
  flex: 0 0 auto;
  margin-top: 2px;
  color: var(--text-tertiary);
}

.ddg-stub__alt {
  margin: 0;
  font-size: var(--text-body-sm);
  color: var(--text-secondary);
}

/* Echo dock ──────────────────────────────────────────────────────────────── */

/* The floating launcher and the panel it opens.
 *
 * Everything INSIDE the panel is the design system's own Echo components —
 * EchoPanel, EchoMessage, EchoComposer, EchoAvatar — so what is left here is
 * only what those do not cover: where the dock sits on the page, how the
 * launcher behaves, the link and vote affordances the static components have
 * no need for, and the ticket form Echo falls back to when it is switched off.
 *
 * This replaced ~400 lines of `.echo-*` selectors carried over from Harbour.
 * They kept the old names because site.js wrote that markup and the runtime
 * was shared with the Harbour pages, and the block said so: "When Phase 4
 * deletes site.css this block becomes the only one and can be renamed then, in
 * one move, with nothing else depending on the old names." Phase 4 has been
 * and gone — site.css and site.js are deleted, echo.js feeds one bundle — so
 * that condition was met and this is the move it described. */

.ddg-echo-dock {
  position: fixed;
  right: clamp(12px, 2vw, 22px);
  bottom: clamp(12px, 2vw, 22px);
  z-index: 45;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);
  font-family: var(--font-sans);
}

.ddg-echo-dock__launch {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 44px;
  /* Tighter on the left: the avatar is already inset by its own violet ring,
     which sits 3px outside the portrait. */
  padding: 0 16px 0 9px;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border-default);
  background: var(--surface-card);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  box-shadow: var(--shadow-md);
  cursor: pointer;
  transition: var(--transition-lift);
}

.ddg-echo-dock__launch:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lg);
  border-color: var(--border-emphasis);
}

@media (prefers-reduced-motion: reduce) {
  .ddg-echo-dock__launch:hover {
    transform: none;
  }
}

.ddg-echo-dock__label {
  white-space: nowrap;
}

/* The launcher gets out of the way while the panel is up. Not display:none —
   `setOpen(false)` returns focus to it, and you cannot focus a display:none
   element, so the focus ring would land nowhere and the next Tab would start
   from the top of the document. */
.ddg-echo-dock.is-open .ddg-echo-dock__launch {
  opacity: 0;
  pointer-events: none;
  height: 0;
  padding: 0;
  border: none;
  overflow: hidden;
}

/* EchoPanel supplies the surface, the border, the light and the internal
   scroll; the dock supplies only the box it has to fit in. A fixed height
   rather than a max — the design system's rule is that the transcript scrolls
   inside the panel and never grows the page, and a max-height panel with two
   messages in it collapses to a floating pill with a composer stuck to it. */
.ddg-echo-dock__panel {
  width: min(380px, calc(100vw - 24px));
  height: min(560px, calc(100vh - 120px));
}

/* `hidden` on a flex container needs saying — display:flex would win. */
.ddg-echo-dock__panel[hidden] {
  display: none;
}

.ddg-echo-dock__close {
  margin-left: auto;
}

/* A source chip that links somewhere. Same chip; the browser's link colour and
   underline are what have to go, and a hover state has to arrive, or a
   clickable citation is indistinguishable from a static one. */
a.ddg-echo-msg__source {
  text-decoration: none;
  transition: var(--transition-control);
}

a.ddg-echo-msg__source:hover {
  border-color: var(--echo-accent);
  color: var(--text-inverse);
}

/* Thumbs, in the meta row under a finished answer. */
.ddg-echo-msg__vote {
  border: none;
  background: none;
  padding: 0 2px;
  font-size: var(--text-body-sm);
  line-height: 1;
  cursor: pointer;
  opacity: 0.62;
  transition: var(--transition-control);
}

.ddg-echo-msg__vote:hover {
  opacity: 1;
}

/* Offline — the ticket form that replaces the composer when Echo is switched
   off in Axion, or when the config call fails outright. The point of it is
   that nobody is left at a dead end: it raises a real ticket through the same
   endpoint /contact posts to, and hands back a reference.
 *
 * It carries `.ddg-field--inverse`, which is what makes `.ddg-control` legible
 * here — the panel is ink, and the light-palette control renders near-black on
 * near-black. */
.ddg-echo-offline {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.ddg-echo-offline textarea {
  resize: vertical;
}

@media (max-width: 460px) {
  .ddg-echo-dock {
    left: 12px;
    right: 12px;
    align-items: stretch;
  }

  .ddg-echo-dock__launch {
    align-self: flex-end;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   CONSOLE SHELL — the operator surface
   ═══════════════════════════════════════════════════════════════════════════ */

.ddg-console {
  /* The console's structural hairline. NOT --border-inverse-default (.16):
     that is tuned for a card sitting on ink, and at .16 the rail edge, the top
     bar and the status strip all read as drawn lines rather than as seams.
     The kit uses .09 throughout for exactly that reason. Scoped here so it is
     one value defined once, not thirteen literals.

     --border-inverse is deliberately not used anywhere in this block: despite
     the name it is a full `1px solid rgba(…)` shorthand, so `1px solid
     var(--border-inverse)` is invalid and the whole border declaration is
     dropped — no border, no warning, 200. */
  /* Muted text on this shell is `rgba(255,255,255,.52)` and no lower.
     Eleven declarations sat between .32 and .45 — the rail's group labels
     ("Observe", "Build"), the owner role under the avatar, the breadcrumb, the
     status bar's build hash and incident line, the console card's meta, Echo's
     hint and timestamps — measuring 2.88:1 to 4.07:1 on the grounds they land
     on. .52 gives 5.59 on `--surface-ink-deep`, 5.40 on the card and 4.87 on
     the raised surface, so one value clears all three.
     It is not a new number: `.ddg-metric--invert .ddg-metric__label` was
     already .52, and it is the floor the dsry inverse ramp settled on for the
     same job on the same ink. Everything below it was drift.
     `:disabled` is deliberately left alone — disabled text is exempt. */
  --console-hairline: rgba(255, 255, 255, 0.09);
  --console-hairline-strong: rgba(255, 255, 255, 0.18);

  min-height: 100vh;
  display: flex;
  flex-direction: column;
  position: relative;
  background: var(--surface-ink-deep);
  color: var(--text-inverse);
  font-family: var(--font-sans);
  overflow: hidden;
}

/* Five stacked atmosphere layers, all decorative and all pointer-events:none.
   They are the difference between "dark theme" and "lit" — the surface has a
   light source at 26% from the left, and everything else in the shell is
   shaded against it. */
.ddg-console__layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.ddg-console__layer--aurora {
  inset: -10%;
  background: var(--bg-aurora-ink);
  opacity: 0.7;
  animation: var(--anim-aurora);
}

.ddg-console__layer--blueprint {
  background: var(--bg-blueprint-inverse);
  background-size: var(--bg-blueprint-size);
  -webkit-mask-image: radial-gradient(120% 74% at 26% 0%, #000 8%, transparent 76%);
  mask-image: radial-gradient(120% 74% at 26% 0%, #000 8%, transparent 76%);
}

.ddg-console__layer--noise {
  background-image: var(--bg-noise);
  opacity: var(--bg-noise-opacity-ink);
  mix-blend-mode: screen;
}

.ddg-console__layer--vignette {
  background: var(--vignette-ink);
}

.ddg-console__body {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}

/* Rail ───────────────────────────────────────────────────────────────────── */

.ddg-rail {
  width: 226px;
  flex: 0 0 226px;
  border-right: 1px solid var(--console-hairline);
  padding: 16px 13px 14px;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  background: rgba(8, 23, 31, 0.5);
  box-shadow: var(--shade-rim-inverse);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .ddg-rail {
    -webkit-backdrop-filter: var(--frost-blur-heavy);
    backdrop-filter: var(--frost-blur-heavy);
  }
}

.ddg-rail__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 2px 5px;
}

.ddg-rail__product {
  font-size: var(--text-body-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-inverse);
}

.ddg-rail__search {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  background: transparent;
  border: 1px solid var(--console-hairline);
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  color: rgba(255, 255, 255, 0.5);
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-rail__search:hover {
  color: var(--text-inverse);
  border-color: var(--console-hairline-strong);
}

.ddg-rail__searchlabel {
  flex: 1 1 auto;
  text-align: left;
}

.ddg-rail__search .ddg-kbd {
  background: rgba(255, 255, 255, 0.07);
  border-color: transparent;
  color: rgba(255, 255, 255, 0.52);
}

.ddg-rail__nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  overflow-y: auto;
  min-height: 0;
}

.ddg-rail__group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ddg-rail__grouplabel {
  padding: 0 11px 6px;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.52);
}

.ddg-rail__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ddg-rail__link {
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  padding: var(--space-2) 11px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: transparent;
  font-size: var(--text-caption);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  color: rgba(255, 255, 255, 0.6);
  text-decoration: none;
  transition: var(--transition-control);
}

.ddg-rail__link:hover {
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-inverse);
}

/* aria-current, not a class. The nav is a list of links and the browser's own
   word for "you are here" is the one a screen reader already announces. */
.ddg-rail__link[aria-current='page'] {
  border-color: rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.09);
  box-shadow: var(--highlight-top);
  font-weight: var(--weight-semibold);
  color: var(--text-inverse);
}

/* The 2px cyan marker is the only chrome in the console that moves. */
.ddg-rail__link[aria-current='page']::before {
  content: '';
  position: absolute;
  left: -13px;
  top: 9px;
  bottom: 9px;
  width: 2px;
  border-radius: 0 2px 2px 0;
  background: var(--accent-cyan);
  box-shadow: 0 0 10px rgba(0, 184, 208, 0.7);
}

.ddg-rail__link[aria-current='page'] .ddg-icon {
  color: var(--accent-cyan);
}

.ddg-rail__foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px var(--space-2);
  border-top: 1px solid var(--console-hairline);
  width: 100%;
  background: none;
  border-left: none;
  border-right: none;
  border-bottom: none;
  color: inherit;
  font-family: var(--font-sans);
  cursor: pointer;
  text-align: left;
}

.ddg-rail__me {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.ddg-rail__mename {
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  color: rgba(255, 255, 255, 0.86);
}

.ddg-rail__merole {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.52);
}

.ddg-rail__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
  border-radius: var(--radius-pill);
  background: var(--wing-gradient);
  font-size: var(--text-nano);
  font-weight: var(--weight-bold);
  color: var(--text-inverse);
}

/* Environment switcher ───────────────────────────────────────────────────── */

/* Above the nav, because a backend operator changes environment more often
   than they change page — and being on the right page in the wrong
   environment is the expensive mistake. */
.ddg-envswitch {
  position: relative;
}

.ddg-envswitch__btn {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: var(--space-2) 10px;
  border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.11);
  box-shadow: var(--highlight-top);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.82);
  text-align: left;
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-envswitch__btn:hover {
  border-color: var(--console-hairline-strong);
}

.ddg-envswitch__name {
  flex: 1;
  min-width: 0;
}

.ddg-envswitch__dot {
  width: 6px;
  height: 6px;
  flex: 0 0 auto;
  border-radius: var(--radius-pill);
  background: currentColor;
}

.ddg-envswitch__menu {
  list-style: none;
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 20;
  margin: 0;
  padding: 5px;
  border-radius: var(--radius-md);
  background: rgba(12, 34, 51, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.13);
  box-shadow: var(--shadow-lg);
}

@supports (backdrop-filter: blur(1px)) {
  .ddg-envswitch__menu {
    backdrop-filter: blur(14px);
  }
}

.ddg-envswitch__option {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  border-radius: var(--radius-xs);
  border: none;
  background: transparent;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
  text-align: left;
  cursor: pointer;
}

.ddg-envswitch__option:hover {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-inverse);
}

.ddg-envswitch__option[aria-current='true'] {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-inverse);
}

/* Top bar ────────────────────────────────────────────────────────────────── */

.ddg-console__main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.ddg-topbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--space-5);
  height: 54px;
  flex: 0 0 auto;
  border-bottom: 1px solid var(--console-hairline);
  background: rgba(8, 23, 31, 0.42);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .ddg-topbar {
    -webkit-backdrop-filter: var(--frost-blur);
    backdrop-filter: var(--frost-blur);
  }
}

/* A path, not a title. It says which environment's page you are on. */
.ddg-topbar__crumb {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.52);
}

.ddg-topbar__crumb ol {
  display: flex;
  align-items: center;
  gap: 7px;
  list-style: none;
  margin: 0;
  padding: 0;
  min-width: 0;
}

.ddg-topbar__crumb li:last-child {
  color: var(--text-inverse);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.ddg-topbar__crumb a {
  color: inherit;
  text-decoration: none;
}

.ddg-topbar__crumb a:hover {
  color: var(--text-inverse);
}

.ddg-topbar__sep {
  opacity: 0.5;
}

.ddg-topbar__tools {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
}

/* Health sits left of Echo so a red fleet reads before the assistant does. */
.ddg-health {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 10px;
  border-radius: var(--radius-pill);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}

.ddg-health__dot {
  width: 5px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: currentColor;
  animation: var(--anim-pulse);
}

/* The washes are back to tokens because `.ddg-console` now pins them, and the
   history is worth keeping: `.ddg-health` only ever appears in `ConsoleShell`,
   which does not follow `[data-theme]` — but `--wash-green` and friends do, and
   in the light theme they are opaque pale pills. So on the light theme this
   rendered a near-white pill on the console's ink bar with #b9dca6 text on it:
   **1.39:1**, the "All services healthy" indicator, unreadable.

   It has one turn the others do not: the foreground and the background flipped
   TOGETHER, so a check that measures text against its own background sees
   nothing wrong. The pill is simply the wrong colour in the wrong place and
   reads fine. Only a light-theme render of a dark shell shows it. */
.ddg-health--ok { background: var(--wash-green); border: 1px solid rgba(128, 184, 104, 0.28); color: #b9dca6; }
.ddg-health--warn { background: var(--wash-amber); border: 1px solid rgba(248, 192, 0, 0.28); color: #f4cf5a; }
.ddg-health--down { background: var(--wash-red); border: 1px solid rgba(217, 58, 43, 0.32); color: #f09a90; }

.ddg-echotoggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 4px 11px 4px 5px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.12);
  font-family: var(--font-sans);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  color: var(--text-inverse);
  cursor: pointer;
  white-space: nowrap;
  transition: var(--transition-control);
}

.ddg-echotoggle[aria-expanded='true'] {
  background: rgba(120, 88, 224, 0.2);
  border-color: rgba(120, 88, 224, 0.44);
  box-shadow: var(--glow-echo);
}

.ddg-console__content {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
}

.ddg-console__page {
  flex: 1 1 auto;
  min-width: 0;
  overflow-y: auto;
  padding: var(--space-6) var(--space-5) var(--space-8);
}

/* A drawer, not a bubble: it takes width from the content rather than
   floating over it, because Echo is reading the page you are looking at. */
.ddg-console__drawer {
  flex: 0 0 372px;
  width: 372px;
  border-left: 1px solid var(--console-hairline);
  padding: 14px;
  overflow: hidden;
  animation: var(--anim-slide-in);
}

/* Status strip ───────────────────────────────────────────────────────────── */

/* Always-true facts that would otherwise each be a widget. No interaction. */
.ddg-statusbar {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 0 18px;
  height: 28px;
  border-top: 1px solid var(--console-hairline);
  background: rgba(8, 23, 31, 0.6);
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.52);
  overflow-x: auto;
  scrollbar-width: none;
}

.ddg-statusbar::-webkit-scrollbar {
  display: none;
}

.ddg-statusbar__item {
  white-space: nowrap;
}

.ddg-statusbar__item--lead {
  color: rgba(255, 255, 255, 0.66);
}

.ddg-statusbar__incident {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
}

.ddg-statusbar__beacon {
  position: relative;
  width: 5px;
  height: 5px;
  flex: 0 0 auto;
  color: var(--accent-green);
}

.ddg-statusbar__beacon::before {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: var(--radius-pill);
  border: 1px solid currentColor;
  animation: var(--anim-ring-out);
}

.ddg-statusbar__beacon::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--radius-pill);
  background: currentColor;
}

.ddg-statusbar__incident[data-state='open'] .ddg-statusbar__beacon {
  color: var(--accent-red);
}

/* Page head ──────────────────────────────────────────────────────────────── */

.ddg-pagehead {
  margin-bottom: var(--space-5);
  animation: var(--anim-rise);
}

.ddg-pagehead__row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-5);
  flex-wrap: wrap;
}

.ddg-pagehead__title {
  font-size: var(--text-h1);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-display);
  color: var(--text-inverse);
  margin: 0;
}

.ddg-pagehead__desc {
  margin: 6px 0 0;
  font-size: var(--text-caption);
  color: rgba(255, 255, 255, 0.55);
  max-width: 64ch;
}

/* Page actions cap at two. A third is a menu. */
.ddg-pagehead__actions {
  display: flex;
  gap: 9px;
  flex-wrap: wrap;
}

/* Tabs sit inside the page head, under the title, on a hairline. */
.ddg-pagehead__tabs {
  display: flex;
  gap: 2px;
  margin-top: 18px;
  border-bottom: 1px solid var(--console-hairline);
  overflow-x: auto;
}

.ddg-pagehead__tab {
  display: inline-block;
  padding: var(--space-2) 13px;
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  font-family: var(--font-sans);
  font-size: var(--text-caption);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  color: rgba(255, 255, 255, 0.5);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: var(--transition-control);
}

.ddg-pagehead__tab:hover {
  color: var(--text-inverse);
}

/* aria-current, because these tabs are links to other pages. aria-selected
   would be a claim that this is a tablist, and a tablist owes panels. */
.ddg-pagehead__tab[aria-current='page'] {
  border-bottom-color: var(--accent-cyan);
  font-weight: var(--weight-semibold);
  color: var(--text-inverse);
}

/* Console card ───────────────────────────────────────────────────────────── */

.ddg-consolecard {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: var(--radius-lg);
  box-shadow: var(--highlight-top);
  padding: 15px 17px 13px;
  min-width: 0;
}

.ddg-consolecard--flush {
  padding: 0;
}

.ddg-consolecard__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: 13px;
}

.ddg-consolecard--flush .ddg-consolecard__head {
  padding: 15px 17px 0;
}

.ddg-consolecard__title {
  font-size: var(--text-h4);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
  color: var(--text-inverse);
  margin: 0;
}

.ddg-consolecard__meta {
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.52);
}

/* Console responsive ─────────────────────────────────────────────────────── */

/* The rail is 226px of fixed width against a viewport that can be 360px. Below
   960px it becomes an off-canvas drawer and the top bar grows the button that
   opens it. Without this the console is 226px of chrome and 134px of content,
   and the page scrolls sideways. */
.ddg-console__railtoggle {
  display: none;
}

@media (max-width: 960px) {
  .ddg-console__railtoggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: 0 0 auto;
    margin-right: var(--space-1);
    border-radius: var(--radius-sm);
    border: 1px solid var(--console-hairline);
    background: transparent;
    color: var(--text-inverse);
    cursor: pointer;
  }

  /* The rail carries data-ddg-state, so the runtime toggles [data-open] and
     never touches [hidden] — see setDisclosure. `hidden` cannot describe a
     panel that is permanent furniture at one width and a drawer at another.

     visibility, not just transform. A rail translated off-screen is still in
     the tab order: Tab from the top bar walks into an invisible navigation
     and focus disappears. visibility:hidden takes it out, and delaying the
     visibility change by the slide duration lets the closing animation
     finish before it vanishes. */
  .ddg-rail {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 60;
    box-shadow: var(--shadow-lg);
    visibility: hidden;
    transform: translateX(-100%);
    transition:
      transform var(--duration-base) var(--ease-out),
      visibility 0s linear var(--duration-base);
  }

  .ddg-rail[data-open] {
    visibility: visible;
    transform: none;
    transition:
      transform var(--duration-base) var(--ease-out),
      visibility 0s;
  }

  .ddg-console__drawer {
    position: fixed;
    top: 54px;
    right: 0;
    bottom: 28px;
    z-index: 55;
    flex: none;
    width: min(372px, 100%);
    background: var(--surface-ink-deep);
  }
}

@media (prefers-reduced-motion: reduce) {
  .ddg-rail {
    transition: none;
  }
}

/* Form completion ─────────────────────────────────────────────────────────────
   What the contact and support forms are replaced BY once they succeed, from
   design-system.js's showDone(). It replaces the form rather than sitting above
   it, because a sent message with the form still under it invites a second
   send, and the endpoint would happily write a second ticket.

   Tokens only, and the surface is `--surface-page-alt` rather than a success
   wash: this is a confirmation, not a status, and green here would read as a
   validation state on a form that is no longer there. */
.ddg-formdone {
  padding: var(--space-6);
  border: 1px solid var(--border-default);
  border-left: 3px solid var(--status-success);
  border-radius: var(--radius-md);
  background: var(--surface-page-alt);
}
.ddg-formdone:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}
.ddg-formdone__eyebrow {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
.ddg-formdone__lead {
  margin: 0;
  font-size: var(--text-lead);
  line-height: var(--leading-snug);
  color: var(--text-primary);
}
.ddg-formdone__ref {
  display: grid;
  gap: var(--space-1);
  margin: var(--space-5) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-default);
}
.ddg-formdone__reflabel {
  font-size: var(--text-micro);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-tertiary);
}
/* Mono, because it is a value to be copied and compared character by
   character. A reference set in the body face invites a misread 0 for O. */
.ddg-formdone__ref b {
  font-family: var(--font-mono);
  font-size: var(--text-h3);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  color: var(--text-primary);
}
.ddg-formdone__refnote {
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  color: var(--text-secondary);
}

/* The live region under a form that has not succeeded yet. `pending` is muted
   on purpose — it is transient and must not compete with the field it sits
   under; `error` is the one a reader has to see. */
.ddg-formstatus {
  margin: var(--space-3) 0 0;
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  color: var(--text-secondary);
  min-height: 1.4em;
}
.ddg-formstatus[data-tone='pending'] {
  color: var(--text-tertiary);
}
.ddg-formstatus[data-tone='error'] {
  color: var(--status-danger);
  font-weight: var(--weight-semibold);
}

/* Reveal ─────────────────────────────────────────────────────────────────────

   The entrance every page shares: `data-ddg-reveal` on anything that should
   arrive rather than simply be there. Three things about it are deliberate.

   · **The stylesheet alone hides nothing.** Every rule is scoped under
     `[data-ddg-motion]`, which the head snippet sets before first paint and
     only when the visitor has not asked for reduced motion. No JS, JS that
     failed, reader mode, print, a crawler — all of them get the content, and
     the served HTML is already the final state.
   · **The hidden state is `:not([data-ddg-revealed])`**, not a class the
     runtime strips. Revealing is therefore adding an attribute, so an element
     the observer never reaches is the only thing that stays down — not a whole
     section that lost a class name.
   · **It moves `translate`, not `transform`.** The independent transform
     properties compose instead of overriding, so a card that lifts on hover
     keeps its `transform: translateY(-3px)` while this owns the entrance. With
     `transform` the two are one property at equal specificity and the last
     rule in the file silently wins the hover.

   `--reveal-shift` and `--reveal-delay` are tokens the system already carried
   and nothing consumed. Stagger a group by setting `--reveal-delay` per child;
   keep the step small — a queue you can count is a page that loads slowly. */
[data-ddg-motion] [data-ddg-reveal] {
  transition:
    opacity var(--duration-reveal) var(--ease-out) var(--reveal-delay),
    translate var(--duration-reveal) var(--ease-out) var(--reveal-delay);
}
[data-ddg-motion] [data-ddg-reveal]:not([data-ddg-revealed]) {
  opacity: 0;
  translate: 0 var(--reveal-shift);
}

/* Belt and braces. The snippet already skips arming under reduced motion, but
   the preference can change after load, and a visitor who turns it on mid-page
   should not be left with whatever was still below the fold held at zero. */
@media (prefers-reduced-motion: reduce) {
  [data-ddg-motion] [data-ddg-reveal] {
    transition: none;
  }
  [data-ddg-motion] [data-ddg-reveal]:not([data-ddg-revealed]) {
    opacity: 1;
    translate: none;
  }
}

/* ── Consent banner ────────────────────────────────────────────────────────
   A pinned region, not an overlay. There is no backdrop and no scroll lock:
   the page underneath stays readable while the choice is open, because a
   notice you cannot dismiss is not a choice. See ConsentBanner.astro.

   `hidden` is honoured by the UA sheet, but `position: fixed` on a hidden
   element still participates in some layout calculations, so display is
   forced off explicitly rather than relied upon. */
.ddg-consent[hidden] {
  display: none;
}
.ddg-consent {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  /* Matches the toast stack. Above the sticky header, below a native
     <dialog>, which lives in the top layer and outranks any z-index. */
  z-index: 100;
  padding: var(--space-4);
  /* Not transparent, and not glass. This sits over arbitrary page content
     including the ink panels, and a translucent surface here made the text
     unreadable over the aurora on Home. */
  background: var(--surface-card);
  border-top: 1px solid var(--border-default);
  box-shadow: var(--shadow-lg);
}
.ddg-consent__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  max-width: var(--container-max);
  margin-inline: auto;
}
.ddg-consent__body {
  flex: 1 1 32ch;
  min-width: 0;
}
.ddg-consent__title {
  margin: 0 0 var(--space-1);
  font-size: var(--text-body);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  color: var(--text-primary);
}
.ddg-consent__text {
  margin: 0;
  max-width: var(--measure-prose);
  font-size: var(--text-body-sm);
  line-height: var(--leading-body);
  color: var(--text-secondary);
}
/* Buttons stay side by side and equal. They do not stretch: a full-width
   Accept above a full-width Decline reads as a primary and a fallback even
   when both are styled identically. */
.ddg-consent__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

@media (max-width: 40rem) {
  .ddg-consent__inner {
    align-items: stretch;
  }
  .ddg-consent__actions > * {
    flex: 1 1 auto;
  }
}

/* ── Splash ────────────────────────────────────────────────────────────────
   The brand entrance, once per session. See Splash.astro for the whole
   argument; the two things that matter in this file are these.

   · **Nothing here paints without `:root[data-ddg-splash]`.** The base rule is
     `display: none`, and only the head snippet sets that attribute. A page
     with no JavaScript, or whose JavaScript was blocked, therefore renders no
     splash at all — rather than a full-screen overlay with no way out.
   · **The dismissal is CSS, not script.** `ddg-splash-out` ends on
     `visibility: hidden` with `forwards`, so the splash clears at ~5.3s off the
     stylesheet alone. Script — all of which lives in ThemeScript's inline head
     snippet, none in design-system.js — can only bring that forward.
     Everything else in this codebase treats a missing runtime as "the feature
     does not happen"; here it has to be "the feature still ends", because the
     failure is a page nobody can read.

   Fixed `rgba(255,255,255,…)` throughout, and it is the system's own exception
   rather than a shortcut: this surface is ink on BOTH grounds, so a token that
   re-points under [data-theme="dark"] would put a light fill on a light fill.
   `.ddg-practice--inverse` hand-writes the same values for the same reason. */

.ddg-splash {
  display: none;
}

/* One keyframe, and it is here rather than in design-system.css because that
   file is GENERATED from the design bundle by scripts/build-design-tokens.mjs —
   a keyframe hand-added there is deleted by the next `npm run design:tokens`.
   `visibility` is the whole point of it: opacity alone leaves an invisible
   surface still swallowing every click on the page underneath. It animates
   discretely and flips only at the end, which is what makes fade-then-hide
   work as one animation. */
@keyframes ddg-splash-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
    visibility: hidden;
  }
}

:root[data-ddg-splash] .ddg-splash {
  position: fixed;
  inset: 0;
  /* The highest z-index in this file: above the consent banner and the skip
     link (100), the load bar (90) and the sticky appbar (40).
     A <dialog> opened with showModal() still outranks it, from the top layer,
     and there IS one on every shell page — the ⌘K command palette. That is the
     right outcome and it is reachable on purpose: the dismiss handler ignores
     modified keys, so ⌘K opens the palette above the splash, the page behind
     goes inert, and the splash clears on its own timer underneath. */
  z-index: 120;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  cursor: pointer;
  background: var(--surface-ink-deep);
  color: var(--text-inverse);
  /* The hold, then the exit. 5000ms on Nigel's instruction (2026-08-13); it
     was 2700ms.

     The rail below has to move with it. It is a PROGRESS indicator — it tells
     the visitor how long they are waiting — so its 200ms delay plus its wipe
     must land on this number. Against the old 2200ms wipe, a 5000ms hold would
     leave the bar full and motionless for two and a half seconds, which reads
     as a page that has stalled rather than one about to move on. Change one of
     these three numbers and change the other two. */
  animation: ddg-splash-out var(--duration-slow) 5000ms var(--ease-standard) forwards;
}

/* Belt and braces. ThemeScript does not arm the splash under reduced motion,
   so this should be unreachable — but the preference can be turned on while
   the splash is up, and the one outcome that must never happen is a static
   overlay left sitting on the page. */
@media (prefers-reduced-motion: reduce) {
  :root[data-ddg-splash] .ddg-splash {
    display: none;
  }
}

.ddg-splash__plume {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 2px;
  background: var(--plume-gradient);
}
.ddg-splash__air {
  position: absolute;
  inset: -14%;
  background: var(--mesh-ink);
  opacity: 0.85;
  animation: var(--anim-drift);
}
.ddg-splash__grid {
  position: absolute;
  inset: 0;
  background: var(--grid-lines-inverse);
  background-size: var(--grid-size) var(--grid-size);
  /* `#000` to match the file's two existing radial masks — the value is a mask
     alpha, not a colour, and writing it a third way invites a "fix" that turns
     it into a token. The mask centre is 50% 40% rather than the shared
     --mask-fade-radial's 50% 0%, because this one is tuned to centred content. */
  -webkit-mask-image: radial-gradient(120% 80% at 50% 40%, #000 8%, transparent 74%);
  mask-image: radial-gradient(120% 80% at 50% 40%, #000 8%, transparent 74%);
}
.ddg-splash__noise {
  position: absolute;
  inset: 0;
  background: var(--bg-noise);
  opacity: var(--bg-noise-opacity-ink);
}

.ddg-splash__inner {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-6);
  padding: 0 var(--space-6);
  text-align: center;
}

/* The stagger. --anim-rise is a token and carries `both`, so each block holds
   its first keyframe through its delay rather than flashing in and then
   animating. A hand-rolled `animation:` shorthand here would opt itself out of
   the reduced-motion override the token layer applies. */
.ddg-splash__lockup {
  animation: var(--anim-rise);
}
.ddg-splash__practices {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-2) var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  letter-spacing: var(--tracking-widest);
  text-transform: uppercase;
  /* 58% white on --surface-ink-deep is 6.42:1 — this is real text naming the
     practices, so it owes 4.5:1 rather than a graphic's 3:1. */
  color: rgba(255, 255, 255, 0.58);
  animation: var(--anim-rise);
  animation-delay: calc(var(--duration-instant) * 2);
}
.ddg-splash__practice {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.ddg-splash__dot {
  width: 5px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--ddg-splash-hue, currentColor);
}
.ddg-splash__practice--cyber { --ddg-splash-hue: var(--practice-cyber); }
.ddg-splash__practice--it { --ddg-splash-hue: var(--practice-it); }
.ddg-splash__practice--ai { --ddg-splash-hue: var(--practice-ai); }
.ddg-splash__practice--search { --ddg-splash-hue: var(--practice-search); }
.ddg-splash__practice--digital { --ddg-splash-hue: var(--practice-digital); }
.ddg-splash__practice--retail { --ddg-splash-hue: var(--practice-retail); }

.ddg-splash__rail {
  display: block;
  width: min(320px, 60vw);
  height: 2px;
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.14);
}
/* The one raw `animation:` shorthand on this component, because there is no
   --anim-grow-x token to reach for — only the ddg-grow-x keyframe the token
   file already ships. It is safe to write by hand here for the same reason the
   splash exists at all: reduced motion never arms it, and the rule above hides
   it outright if the preference changes mid-animation. */
.ddg-splash__railfill {
  display: block;
  width: 100%;
  height: 100%;
  transform-origin: left center;
  background: var(--plume-gradient);
  animation: ddg-grow-x 4800ms 200ms linear both;
}

/* The entrance delay, and nothing else. Everything this rule used to declare —
   fill, border, radius, type, weight, tracking, highlight, transition, hover
   and a bespoke focus ring — is `.ddg-btn--sm.ddg-btn--inverse-outline`, which
   the markup now uses. The hand-written copy differed from it only in four
   alpha values, by 0.01 to 0.02: invisible alone, and visibly two different
   buttons beside each other. */
.ddg-splash__enter {
  animation: var(--anim-rise);
  animation-delay: calc(var(--duration-instant) * 4);
}

.ddg-splash__hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--space-5);
  text-align: center;
  font-family: var(--font-mono);
  font-size: var(--text-nano);
  padding: 0 var(--space-6);
  letter-spacing: var(--tracking-widest);
  /* NOT uppercased, unlike the template and unlike the practice rail above it.
     The type rule is that uppercase is a device for LABELS — eyebrows, badges,
     straplines, mono keys — and never for a sentence. The rail is a list of
     labels and is legitimately uppercase; this is a sentence, which is the
     exact case the rule names. */
  /* The template has this at 36%, which measures 3.32:1 on --surface-ink-deep.
     It is aria-hidden and the button above it says the same thing, so a case
     could be made that it is decoration owing 3:1 — but it is a sentence a
     sighted visitor reads, and `npm run contrast:check` is right to score it as
     text. 48% clears AA (4.88:1) and still leaves it visibly quieter than the
     practice rail above at 58%. */
  color: rgba(255, 255, 255, 0.48);
}

/* A short landscape viewport — a phone turned sideways — is the one shape where
   the centred stack and this absolutely-positioned line can meet, and
   `.ddg-splash` clips rather than scrolls. The hint is the part that can go: it
   is decoration, and the button above it already says the same thing. */
@media (max-height: 520px) {
  .ddg-splash__hint {
    display: none;
  }
}
