/*
 * P10.5 §2 — the design baseline, in one file.
 *
 * Source of truth: the WIP redesign of davidnewkerk.com,
 * github.com/dnewkerk/davidnewkerk-astro branch `p1-build`, src/styles/global.css
 * (read 2026-08-18; the live dev server was also reachable this session over an
 * ngrok tunnel and served the same values). The extracted table is in
 * site-forge qa/pm-app-p10-5/design-tokens.md; the Tailwind @theme block it
 * quotes is --color-background #003043, --color-container #003E54,
 * --color-accent #4FBDD6, --color-accent-dark #3A8FA3, --color-gray #ffffffaf.
 *
 * This file loads AFTER css-zero/variables and after every other app stylesheet
 * (Propshaft's `:app` directory bundle is alphabetical, and "theme" sorts last),
 * so it re-points css-zero's own custom properties instead of fighting them with
 * specificity. Nothing here is a utility class; feature views never touch these
 * names directly.
 */

/* ------------------------------------------------------------------ fonts
 * Self-hosted variable woff2, latin subset only, via Propshaft. No CDN: the CSP
 * has no external font-src, and a webfont that fails closed is a broken page.
 * The @font-face src uses Propshaft's asset-path resolution, which rewrites the
 * digest in; that is why these live in app/assets/fonts and not public/.
 */
@font-face {
  font-family: "Geist Variable";
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url("/assets/geist-latin-wght-normal-90ac0fe7.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Merriweather Variable";
  font-style: normal;
  font-display: swap;
  font-weight: 300 900;
  src: url("/assets/merriweather-latin-wght-normal-95bbc1ac.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ----------------------------------------------------------------- palette
 * Verbatim from the Astro @theme block. Named for their role there so a future
 * reader can diff the two files without a decoder ring.
 */
:root {
  --brand-accent: #4FBDD6;
  --brand-accent-dark: #3A8FA3;
  --brand-navy: #003043;      /* the "blue body background" of §0.1 */
  --brand-navy-deep: #001D28; /* gradient start */
  --brand-navy-lift: #003E54; /* gradient end, and the Astro "container" colour */
  --brand-mist: #ffffffaf;    /* body text on navy */
  --brand-signal: #00ff41;

  /*
   * Round 2, dark mode: surfaces LIFT off the navy instead of darkening into
   * zinc. Measured off the running frontend, not read off its source -- every
   * card there is `bg-white/5 border-white/10` and every callout pill is
   * `bg-accent/10..20`. (David's note said `bg-background/60`; that class does
   * not appear on the rendered page at all -- 15x bg-white/5, 0x
   * bg-background/60 -- and #003043 at 60% over a gradient whose midpoint IS
   * #003043 would have been a no-op in the middle and a DARKENING at the
   * bottom, which is the opposite of the ask. So: the site's real values.)
   *
   * --brand-surface-lift is left translucent so a card picks up the gradient
   * beneath it exactly as the site's do. --brand-surface-solid is the same
   * colour flattened over the gradient's midpoint, for the layers that must not
   * be see-through: popovers and dialogs float over page content, and inputs
   * over cards.
   */
  --brand-surface-lift: rgb(from white r g b / .05);
  --brand-surface-solid: #0d3a4c;
  --brand-callout: rgb(from var(--brand-accent) r g b / .1);
  --brand-hairline: rgb(from white r g b / .1);   /* border-white/10 */

  /* The page backdrop, verbatim from the Astro body rule. */
  --brand-backdrop: linear-gradient(
    135deg,
    var(--brand-navy-deep) 0%,
    var(--brand-navy) 50%,
    var(--brand-navy-lift) 100%
  );

  --font-body: "Geist Variable", ui-sans-serif, system-ui, sans-serif;
  --font-heading: "Merriweather Variable", ui-serif, Georgia, serif;

  /*
   * §3 + round 2: the header carries the frontend's own gradient rather than a
   * flat navy, so the app's chrome reads as the same family as the site.
   * David's note gave the declaration verbatim, `fixed` included -- with
   * background-attachment: fixed the gradient is sized to the VIEWPORT, so the
   * bar shows a stable slice of it instead of squeezing all three stops into
   * its own 3rem height. That is the effect on the site and it is why the
   * attachment keyword is load-bearing here, not decoration.
   */
  --header-bg: linear-gradient(
    75deg,
    var(--brand-navy-deep),
    var(--brand-navy),
    var(--brand-navy-lift)
  );

  /* The light-mode alternative used by treatment B (see below). */
  --brand-backdrop-tint: linear-gradient(
    135deg,
    #eef4f7 0%,
    #e3edf2 50%,
    #dbe9f0 100%
  );
}

/* ------------------------------------------------ css-zero token overrides
 * base.css defines these with light-dark(); re-declaring them here (later in the
 * cascade, same specificity) re-points the whole component set at the brand
 * palette without editing a single component file.
 */
:root {
  --color-link: light-dark(var(--brand-accent-dark), var(--brand-accent));
  --color-selected: light-dark(#e2f4f9, #06323f);
  --color-selected-dark: light-dark(var(--brand-accent), var(--brand-accent-dark));
  /*
   * Usability spec 03 §1: selected text. The dark half is the accent itself with
   * the deepest navy on it -- the highlight has to stand off the navy as a shape
   * (>= 3:1 against every dark surface) and the words on it stay readable
   * (>= 4.5:1). The light half is the accent mixed halfway to white. Both pairs
   * are re-measured on every run by test/assets/selection_contrast_test.rb.
   */
  --color-selection-bg: light-dark(#a7deeb, var(--brand-accent));
  --color-selection-text: light-dark(var(--brand-navy-deep), var(--brand-navy-deep));
  --color-primary: light-dark(var(--brand-navy), var(--brand-accent));
  --color-text-reversed: light-dark(white, var(--brand-navy-deep));

  /*
   * Round 2: the dark half of every neutral. css-zero's darks are the zinc
   * ramp, which over a navy backdrop reads as dirty grey rather than as a
   * darker shade of the page. Re-pointed at white-alpha values so a surface,
   * a divider and a hover all sit ON the navy instead of beside it.
   *
   * Only the dark halves move. Every light-mode value here is css-zero's own,
   * repeated verbatim, so nothing about light mode changes.
   */
  --color-bg: light-dark(white, var(--brand-navy-deep));
  --color-surface: light-dark(white, var(--brand-surface-solid));
  --color-secondary: light-dark(var(--zinc-100), rgb(from white r g b / .1));
  --color-border-light: light-dark(var(--zinc-100), rgb(from white r g b / .1));
  --color-border: light-dark(var(--zinc-200), var(--brand-hairline));
  --color-border-dark: light-dark(var(--zinc-400), rgb(from white r g b / .3));
  --color-text-subtle: light-dark(var(--zinc-500), var(--brand-mist));

  /* Negative surfaces: red-950 under navy is a bruise. A red tint is not. */
  --color-surface-negative: light-dark(var(--red-100), rgb(from var(--red-400) r g b / .18));
  --color-text-negative: light-dark(var(--red-600), var(--red-300));

  /*
   * The faint wash: a hovered row, a checked row, a footer strip, a muted row.
   *
   * Every one of those used to be written inline as
   * `rgb(from var(--color-border-light) r g b / .5)` -- and relative colour syntax
   * DISCARDS the origin's own alpha: it takes r, g, b and substitutes the slash
   * value. Harmless while --color-border-light was an opaque zinc-100. The dark
   * half two lines up re-points it at `white / .1`, the .1 is thrown away, and the
   * wash resolves to white at FIFTY percent -- a flash, not a wash, over the navy.
   *
   * Round 3 fixed the three list.css sites and named the token --list-row-wash.
   * F1's measured sweep then found the same expression at seven more sites over the
   * same token, two of them live (`.table tr:hover` and `.dialog__footer`, both
   * measured at `color(srgb 1 1 1 / 0.5)` in dark). The token is renamed and moved
   * here because the wash was never a property of list rows: one definition, one
   * value, every consumer. `test/assets/relative_color_alpha_test.rb` is the gate
   * that keeps the expression from coming back.
   *
   * The light half is zinc-100 at .5, byte-identical to what the old expression
   * produced in light mode at every one of those sites (measured), so light mode
   * does not move. The dark half is white at .12 -- David's approved round-3 value.
   */
  --surface-wash: light-dark(
    rgb(from var(--zinc-100) r g b / .5),
    rgb(from white r g b / .12)
  );
}

/*
 * A card is the one surface that should keep showing the gradient through it,
 * the way the site's do -- so it takes the translucent lift rather than the
 * flattened --color-surface that popovers, dialogs and inputs need.
 */
.card {
  background-color: light-dark(white, var(--brand-surface-lift));
}

/* The accent callout, the site's `bg-accent/10`: a card that wants attention. */
.card--callout {
  background-color: light-dark(#eaf6fa, var(--brand-callout));
  border-color: light-dark(var(--brand-accent-dark), rgb(from var(--brand-accent) r g b / .4));
}

html {
  color-scheme: light dark;
  font-family: var(--font-body);
}

/*
 * ------------------------------------------------------- §0.1 body treatment
 *
 * The backdrop follows the colour scheme: a light tint in light mode, the navy
 * gradient in dark. This was prototype B of the §0.1 A/B pair, and it is David's
 * pick as of 2026-08-18 -- "the light blue background with bright white center
 * content does work well".
 *
 * Treatment A (the navy in BOTH schemes, the way davidnewkerk.com does it) is
 * gone from here, as §0.1 requires: the loser is deleted, not kept behind a flag.
 * It is not lost -- the tag `p10.5-treatment-a` is the commit where it still
 * lived, with the round-2 feedback already rolled into it, so restoring is
 * `git revert` of the commit that removed it rather than a rebuild.
 *
 * NOT `light-dark()`. That function returns a <color>, and these are gradients:
 * `background: light-dark(<gradient>, <gradient>)` is invalid, the whole
 * declaration is dropped, and base.css's plain --color-bg wins. Measured on this
 * install -- the first cut of this file did exactly that and rendered white.
 * Scheme-dependent gradients have to be selected, not interpolated.
 */
body {
  background: var(--brand-backdrop-tint);
  background-attachment: fixed;
  font-family: var(--font-body);
}

:root[data-color-scheme="dark"] body { background: var(--brand-backdrop); }

@media (prefers-color-scheme: dark) {
  :root[data-color-scheme="system"] body { background: var(--brand-backdrop); }
  :root:not([data-color-scheme]) body { background: var(--brand-backdrop); }
}

/*
 * Headings are the serif. Scoped to real headings rather than a utility so a
 * card title and a page title cannot drift apart.
 */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  letter-spacing: -0.01em;
}

/*
 * A badge inside a heading -- the stage badge on a board section, the count on a
 * client row -- inherits the serif and reads as a title fragment rather than a
 * chip. Labels stay in the body face wherever they appear.
 */
.badge, .btn, .input, label, legend {
  font-family: var(--font-body);
  letter-spacing: normal;
}

/*
 * The intake form is the one surface a client sees, and it is not part of the
 * app chrome: it keeps the backdrop but its sheet stays a readable document.
 */
.intake__heading {
  font-family: var(--font-heading);
}

/* ------------------------------------------------------------------ badges
 * §2: Badge and Button colour schemes re-derived from the adopted palette.
 * Only the accent-carrying variants are re-pointed; the neutral badge keeps
 * css-zero's zinc so a stage badge and a "Note" badge stay distinguishable.
 */
/*
 * Round 3: these set the --badge-* CUSTOM PROPERTIES, not the properties themselves.
 * They used to set `background-color` directly and lose -- the `.badge` rule below is
 * the same specificity (0,1,0) and comes LATER in this file, so it won, and what a
 * primary badge actually painted was badge.css's own `--badge-background:
 * var(--color-primary)`. In dark that is --brand-accent, while the `color` here
 * (which DID win, being later than badge.css) was also --brand-accent: accent text on
 * an accent chip. Measured before the fix, dark mode: background rgb(79,189,214),
 * colour rgb(79,189,214) -- the same value twice, which is David's "text on
 * .badge--primary is not visible" exactly.
 *
 * Going through the variables is both the fix and the guard: one layer of
 * indirection, one place per variant, and the fallback below can no longer reach
 * past a variant's own value.
 *
 * The two greys are David's picks. Light: white on #596169 is 6.3:1, a step back
 * from the navy chip he found too intense. Dark: #272f36 on the accent is 6.2:1.
 * Both clear AA for the badge's 12px medium.
 */
.badge--primary {
  --badge-background: light-dark(#596169, var(--brand-accent));
  --badge-border-color: transparent;
  --badge-color: light-dark(white, #272f36);
}

.badge--accent {
  --badge-background: light-dark(#dff1f7, rgb(from var(--brand-accent) r g b / .15));
  --badge-border-color: light-dark(var(--brand-accent-dark), rgb(from var(--brand-accent) r g b / .45));
  --badge-color: light-dark(var(--brand-accent-dark), var(--brand-accent));
}

/*
 * Round 2, dark mode: a badge sits on navy now, so its dark half is a tint of
 * its own hue rather than css-zero's 900-shade. red-950 under a navy card is a
 * bruise (David: "badge--negative looks too dark"); an 18% wash of the 400 with
 * 200-weight text keeps the hue legible and keeps every badge in the same key.
 * The neutral badge follows the same rule with white instead of a hue.
 */
.badge {
  /* The neutral fallback only. It keeps the var() indirection, so a variant that
     sets --badge-background still wins -- but note it DOES clobber a variant that
     sets `background-color` as a property, which is how round 2 silently disabled
     the primary and accent badges above. Variants set variables, not properties. */
  background-color: var(--badge-background, light-dark(var(--color-bg), rgb(from white r g b / .08)));
}

.badge--negative {
  --badge-border-color: rgb(from var(--red-400) r g b / .35);
}

/*
 * --btn-hover-color, not --btn-background under &:hover -- see the header note.
 * In dark mode a navy button on a navy page is a rectangle of nothing, so the
 * primary flips to the accent and takes dark text.
 */
.btn--primary {
  --btn-background: light-dark(var(--brand-navy), var(--brand-accent));
  --btn-color: light-dark(white, var(--brand-navy-deep));
  --btn-hover-color: light-dark(var(--brand-navy-lift), #6fcbdf);
}

/*
 * §6.5: the required marker. A same-colour asterisk is easy to miss, so it
 * carries the accent -- and the accent-dark variant in light mode, because
 * #4FBDD6 on white is below 3:1.
 */
.required-marker {
  color: light-dark(var(--brand-accent-dark), var(--brand-accent));
  font-weight: 700;
}

/* ------------------------------------------------------------------ header
 * §3. The bar is the darkest of the three navies so it reads as chrome against
 * the backdrop, and its contents are always light -- it does not follow the
 * colour scheme, by design (§0.1: the header is constant in both treatments).
 */
#header {
  background: var(--header-bg) fixed;
  border-block-end-color: rgb(from white r g b / .08);
  color: white;
}

.app-title {
  font-family: var(--font-heading);
  font-size: var(--text-lg);
  font-weight: var(--font-bold);
  letter-spacing: -0.02em;
  /* Round 2: the title must not crowd the nav that follows it. */
  margin-inline-end: 1rem;
  white-space: nowrap;
}

.app-title__link {
  color: white;
  text-decoration: none;

  &:hover { color: var(--brand-accent); }
}

/*
 * Round 3: css-zero's `.gap` is a 0.5rem column gap, which reads as one crowded
 * word on the bar. --column-gap is the knob `.gap` already consults, so setting it
 * here widens the nav without swapping the utility out of the markup. 8px -> 18px.
 */
.app-nav { --column-gap: 1.125rem; }

/*
 * Usability spec 06: a third nav link (Guides) took the bar to 451px on a 390px
 * phone, and every signed-in page scrolled sideways. The bar is at least its old
 * 64px instead of exactly that, and the title + nav group may wrap: on a narrow
 * screen the nav drops under the title, nothing is hidden.
 */
#header {
  min-block-size: var(--size-16);
  padding-block: var(--size-2);
}

.app-header__start {
  --row-gap: var(--size-1);
  min-inline-size: 0;
}

/*
 * Nav links and the two icon buttons live on the dark bar, so they take their
 * colours from it rather than from --color-link, which is scheme-dependent.
 */
#header .text-link {
  color: var(--brand-mist);
  text-decoration: none;

  &:hover { color: white; }
}

/*
 * `:not(.menu__item)` is load-bearing, not tidiness. A popover is still a DOM
 * descendant of the header even while it is painted in the top layer, so these
 * dark-bar colours were reaching the items INSIDE the account and theme menus:
 * --brand-mist text on the popover's white surface, which David could only read
 * on hover, and barely. The menus are ordinary popovers on an ordinary surface
 * and want css-zero's own colours; the bar's own controls are what this styles.
 *
 * The hover fix is the other half. `.btn:hover` sets `background-color`
 * DIRECTLY from --btn-hover-color, so setting --btn-background under `&:hover`
 * did nothing at all and the fallback -- --color-border-light, a light grey --
 * won. White-on-light-grey is what David saw on the theme toggle, on the avatar
 * and on a completed task's circle: one bug, three sightings. Measured before
 * the fix: computed hover background oklch(0.967 0.001 286.375) on all three.
 */
#header .btn:not(.menu__item) {
  --btn-background: transparent;
  --btn-border-color: rgb(from white r g b / .18);
  --btn-color: var(--brand-mist);
  --btn-hover-color: rgb(from white r g b / .1);

  &:hover { --btn-color: white; }
}

/*
 * The avatar is a circle, and it was 30x24. Cause: `.avatar` and `.btn` both
 * declare block-size at specificity (0,1,0), the app bundle is alphabetical, and
 * button.css sorts after avatar.css -- so `block-size: auto` won and the box
 * collapsed to its text. `.avatar.btn` is (0,2,0) and settles it. Same shape of
 * bug as the completion toggle's oval (board.css, §5.1); both are fixed by
 * winning specificity honestly rather than by reordering a vendored bundle.
 */
.avatar.btn {
  aspect-ratio: 1;
  block-size: var(--avatar-size);
  inline-size: var(--avatar-size);
}

/* Round 2: breathing room between the theme toggle and the account button. */
.user-menu { margin-inline-start: 1rem; }

#header .avatar span[role="img"] {
  background-color: var(--brand-accent-dark);
  color: white;
  /* Initials at 300, not 600: two capitals in a 32px disc read as a monogram,
     not a label. Geist Variable covers 100-900, so 300 is a real instance. */
  font-weight: 300;
}

/*
 * Hovering the avatar should shade the avatar, not paint a grey pill behind it.
 * css-zero's own `filter: brightness(.9)` DARKENS; David asked for the lighter
 * direction, so this lifts the disc instead and keeps the button transparent.
 */
#header .avatar.btn {
  --btn-hover-color: transparent;

  &:hover {
    filter: none;

    span[role="img"] { background-color: var(--brand-accent); }
  }
}

/*
 * §3 + §4 replacements for the lookbook's inline `style=` attributes. A style
 * ATTRIBUTE cannot carry a CSP nonce (app CLAUDE.md, Security rules), and
 * SecurityHeadersTest scans for them, so every lookbook example that ships one
 * gets a class here instead.
 */
.icon--lg { --icon-size: 1.5rem; }

.user-menu__popover { --popover-size: 14rem; }

.user-menu__email {
  color: var(--color-text-subtle);
  font-size: var(--text-xs);
  overflow: hidden;
  padding: var(--size-2) var(--size-3);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* button_to wraps its button in a form; `contents` keeps the menu row layout. */
.user-menu__logout { inline-size: 100%; }

.auth-card { --card-padding: var(--size-6); inline-size: min(24rem, calc(100vw - 2rem)); }

.dialog--confirm { --dialog-size: 24rem; }

/* ------------------------------------------------------- §5.3 progress bar */
.client-progress {
  display: flex;
  flex-direction: column;
  margin-block-start: var(--size-4);
  row-gap: var(--size-1);
}

.client-progress__row {
  align-items: center;
  column-gap: var(--size-2);
  display: flex;
  flex-wrap: wrap;
}

.progress { --progress-value-color: light-dark(var(--brand-accent-dark), var(--brand-accent)); }

.progress--complete { --progress-value-color: light-dark(var(--emerald-600), var(--emerald-400)); }

/* ------------------------------------------------------------- §5.2 rows
 * A list row nested in a card is already inside a bordered box; a second border
 * around every row turns a readable list into a grid of boxes. Inside a card the
 * row keeps its padding and loses its border and radius. Dividers stay, one
 * hairline between rows, never after the last.
 */
.card .list-row {
  --list-border-color: transparent;

  border-radius: 0;
  border-width: 0;
  border-block-end: var(--border) solid var(--color-border-light);

  &:last-child { border-block-end-width: 0; }
}
