/* Entry point. The layer order below is the single source of truth for
   priority - later layers beat earlier ones regardless of specificity.
   Add a stylesheet by adding one @import line, in its layer's group. */

@layer reset, base, layouts, components, pages, utilities;

/* Tokens carry no layer: they only declare custom properties. */
/* Tier 1 - primitives. Raw values, named for what they ARE.
   Nothing here has an opinion about where it is used. Components must never
   reference these directly; they go through tokens/semantic.css.

   PRD 016: repainted to the Piña Digital visual system
   (prds/016-portal-redesign/docs/01-token-mapping.md has the full
   value-by-value mapping and rationale). Primary action moved from green to
   purple; green is kept, repurposed for the success status hue only -
   the source design's own --ok is green, so the name still fits its value. */

:root {
  /* ── Warm neutrals ─────────────────────────────────────────────────── */
  --p-paper-000: #FFFFFF;
  --p-paper-050: #F6F3ED;
  --p-paper-100: #FBFAF7;
  --p-paper-150: #F6F3ED;
  --p-paper-200: #EFEAE2;
  --p-paper-250: #FFFFFF;
  --p-paper-300: #E7E1D8;
  --p-paper-400: #E7E1D8;
  --p-paper-500: #D5CDC1;
  --p-paper-600: #D5CDC1;
  --p-paper-700: #B7AF9E;

  /* ── Ink ───────────────────────────────────────────────────────────── */
  --p-ink-900: #1B1725;
  --p-ink-700: #57506B;
  --p-ink-500: #8B839E;
  --p-ink-400: #A79FB5;

  /* ── Purple - actions, active state, links (was green) ───────────────── */
  --p-purple-800: #5316B8;
  --p-purple-700: #6A1FE0;
  --p-purple-tint: #F2EBFF;
  --p-purple-tint-2: #E9DBFF;

  /* ── Green - success only ─────────────────────────────────────────── */
  --p-green-700: #0F7A55;
  --p-green-150: #E3F3EB;

  /* ── Status hues ───────────────────────────────────────────────────── */
  --p-amber-700: #A96300;
  --p-amber-100: #FBEFDA;
  --p-red-700: #C4104A;
  --p-red-100: #FCE6ED;
  --p-pink-700: #E01050;
  --p-pink-100: #FCE3EC;
  --p-violet-700: #5B4A9E;
  --p-violet-100: #ECE7F8;

  /* ── Brand accents - reserved for currency/decorative use only.
        Not wired to any semantic role by this PRD (see decision #1). ──── */
  --p-brand-amber: #FCB301;
  --p-brand-orange: #FC5001;
  --p-brand-cyan: #01A8FB;
  --p-brand-teal: #01BCA4;

  /* ── A third party's brand, not ours (PRD 023 #21, #30). WhatsApp's green
        and the ink that reads on it. Neither swaps in dark mode: the green
        is the same green on any ground, and white on it is ~1.9:1, so the
        on-colour is fixed dark ink in both themes (#46). ───────────────── */
  --p-brand-whatsapp: #25D366;
  --p-brand-whatsapp-ink: #07321A;

  /* ── Type ──────────────────────────────────────────────────────────── */
  --p-family-sans: "Figtree", system-ui, -apple-system, sans-serif;
  --p-family-display: "Fredoka", "Figtree", system-ui, sans-serif;
  --p-family-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", monospace;

  --p-size-10: 10px;
  --p-size-105: 10.5px;
  --p-size-11: 11px;
  --p-size-115: 11.5px;
  --p-size-12: 12px;
  --p-size-125: 12.5px;
  --p-size-13: 13px;
  --p-size-135: 13.5px;
  --p-size-14: 14px;
  --p-size-145: 14.5px;
  --p-size-25: clamp(22px, 20px + 1vw, 28px);
  --p-size-27: clamp(24px, 21px + 1.3vw, 32px);

  --p-weight-regular: 400;
  --p-weight-medium: 500;
  --p-weight-semibold: 600;

  --p-leading-snug: 1.15;
  --p-leading-tight: 1.2;
  --p-leading-body: 1.55;

  --p-track-title: -0.02em;
  --p-track-figure: -0.028em;
  --p-track-brand: -0.01em;
  --p-track-mono: 0.02em;
  --p-track-code: 0.04em;
  --p-track-eyebrow: 0.1em;
  --p-track-label: 0.13em;
  --p-track-group: 0.14em;

  /* ── Spacing rhythm ────────────────────────────────────────────────── */
  --p-space-1: 1px;
  --p-space-2: 2px;
  --p-space-4: 4px;
  --p-space-6: 6px;
  --p-space-8: 8px;
  --p-space-10: 10px;
  --p-space-12: 12px;
  --p-space-14: 14px;
  --p-space-16: 16px;
  --p-space-18: 18px;
  --p-space-20: 20px;
  --p-space-26: 26px;
  --p-space-32: 32px;
  --p-space-72: 72px;

  /* ── Radius ────────────────────────────────────────────────────────── */
  --p-radius-3: 3px;
  --p-radius-4: 5px;
  --p-radius-5: 7px;
  --p-radius-6: 10px;
  --p-radius-7: 12px;
  --p-radius-9: 16px;
  --p-radius-pill: 999px;
  --p-radius-circle: 50%;

  /* ── Elevation - two tiers: resting surfaces, and floating/overlay
        surfaces. Overturns the prior "no elevation" rule (decision #5). ── */
  --p-shadow-thumb: 0 1px 2px rgb(27 23 37 / 8%), 0 8px 20px -14px rgb(27 23 37 / 20%);
  --p-shadow-popover: 0 2px 6px rgb(27 23 37 / 8%), 0 18px 36px -20px rgb(27 23 37 / 28%);

  /* ── Control and frame sizes ───────────────────────────────────────── */
  --p-control-26: 26px;
  --p-control-28: 28px;
  --p-control-32: 32px;
  --p-control-34: 34px;
  --p-control-38: 38px;
  --p-topbar-h: 56px;
  --p-sidebar-w: 248px;
  --p-sidebar-w-collapsed: 74px;
  --p-content-max: 1420px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --p-paper-000: #1D1826;
    --p-paper-050: #251F31;
    --p-paper-100: #141019;
    --p-paper-150: #251F31;
    --p-paper-200: #312A44;
    --p-paper-250: #1D1826;
    --p-paper-300: #312A44;
    --p-paper-400: #312A44;
    --p-paper-500: #443A5C;
    --p-paper-600: #443A5C;
    --p-paper-700: #6E6482;

    --p-ink-900: #F4F0FA;
    --p-ink-700: #ADA3C0;
    --p-ink-500: #8079A0;
    --p-ink-400: #6B6488;

    --p-purple-800: #C6ABFF;
    --p-purple-700: #A87CFF;
    --p-purple-tint: #2A1C46;
    --p-purple-tint-2: #382857;

    --p-green-700: #3FBF8B;
    --p-green-150: #122C22;

    --p-amber-700: #E0A02A;
    --p-amber-100: #33260E;
    --p-red-700: #F2547E;
    --p-red-100: #39131F;
    --p-pink-700: #FF6B9B;
    --p-pink-100: #3A1226;
    --p-violet-700: #B0A2E8;
    --p-violet-100: #2A2447;

    --p-shadow-thumb: 0 1px 2px rgb(0 0 0 / 30%), 0 8px 20px -14px rgb(0 0 0 / 55%);
    --p-shadow-popover: 0 2px 6px rgb(0 0 0 / 35%), 0 18px 36px -20px rgb(0 0 0 / 65%);
  }
}

:root[data-theme="dark"] {
  --p-paper-000: #1D1826;
  --p-paper-050: #251F31;
  --p-paper-100: #141019;
  --p-paper-150: #251F31;
  --p-paper-200: #312A44;
  --p-paper-250: #1D1826;
  --p-paper-300: #312A44;
  --p-paper-400: #312A44;
  --p-paper-500: #443A5C;
  --p-paper-600: #443A5C;
  --p-paper-700: #6E6482;

  --p-ink-900: #F4F0FA;
  --p-ink-700: #ADA3C0;
  --p-ink-500: #8079A0;
  --p-ink-400: #6B6488;

  --p-purple-800: #C6ABFF;
  --p-purple-700: #A87CFF;
  --p-purple-tint: #2A1C46;
  --p-purple-tint-2: #382857;

  --p-green-700: #3FBF8B;
  --p-green-150: #122C22;

  --p-amber-700: #E0A02A;
  --p-amber-100: #33260E;
  --p-red-700: #F2547E;
  --p-red-100: #39131F;
  --p-pink-700: #FF6B9B;
  --p-pink-100: #3A1226;
  --p-violet-700: #B0A2E8;
  --p-violet-100: #2A2447;

  --p-shadow-thumb: 0 1px 2px rgb(0 0 0 / 30%), 0 8px 20px -14px rgb(0 0 0 / 55%);
  --p-shadow-popover: 0 2px 6px rgb(0 0 0 / 35%), 0 18px 36px -20px rgb(0 0 0 / 65%);
}

/* Tier 2 - semantic. Named for what they are FOR. The only tier a component
   file may reference. Restyling the app happens here.

   PRD 016: dark mode is handled entirely at the primitive layer
   (tokens/primitives.css) - every --p-* value already swaps under
   `@media (prefers-color-scheme: dark)` (guarded by
   `:root:not([data-theme="light"])`) and `:root[data-theme="dark"]`. Since
   every token below is a `var(--p-*)` reference and never a literal, that
   single point of variation is sufficient - this file needs no dark block
   of its own. (The scheme originally sketched here - duplicating semantic
   values across three states - is unnecessary once the layer beneath it
   already resolves per-theme; kept as one indirection, not two.) */

:root {
  /* ── Surfaces ──────────────────────────────────────────────────────── */
  --s-ground: var(--p-paper-100);          /* app background */
  --s-surface: var(--p-paper-000);         /* cards, table body, top bar */
  --s-surface-sunken: var(--p-paper-050);  /* table/card headers, row hover */
  --s-surface-nav: var(--p-paper-250);     /* sidebar */
  --s-surface-inset: var(--p-paper-150);   /* input rest, ghost hover, nav-item hover */

  /* ── Lines ─────────────────────────────────────────────────────────── */
  --s-border: var(--p-paper-400);          /* card and section borders */
  --s-border-strong: var(--p-paper-600);   /* interactive: buttons, inputs */
  --s-border-subtle: var(--p-paper-300);   /* row separators */
  --s-border-faint: var(--p-paper-200);    /* field separators inside cards */

  /* ── Text ──────────────────────────────────────────────────────────── */
  --s-text: var(--p-ink-900);
  --s-text-muted: var(--p-ink-700);
  --s-text-soft: var(--p-ink-500);
  --s-text-faint: var(--p-ink-400);
  --s-mark-faint: var(--p-paper-700);      /* low-priority dots, disabled marks */

  /* ── Action ────────────────────────────────────────────────────────── */
  --s-primary: var(--p-purple-700);
  --s-primary-hover: var(--p-purple-800);
  --s-primary-tint: var(--p-purple-tint);    /* active nav background */
  --s-primary-tint-2: var(--p-purple-tint-2); /* avatar background */
  --s-on-primary: var(--p-paper-000);
  --s-focus: var(--p-purple-700);
  --s-selection: var(--p-purple-tint);

  /* ── Status - background/text pairs. The label always carries the
        meaning; colour never carries it alone. ───────────────────────── */
  --s-success: var(--p-green-700);
  --s-success-bg: var(--p-green-150);
  --s-warning: var(--p-amber-700);
  --s-warning-bg: var(--p-amber-100);
  --s-danger: var(--p-red-700);
  --s-danger-bg: var(--p-red-100);
  --s-accent: var(--p-pink-700);
  --s-accent-bg: var(--p-pink-100);
  --s-info: var(--p-violet-700);
  --s-info-bg: var(--p-violet-100);
  --s-neutral: var(--p-ink-700);
  --s-neutral-bg: var(--p-paper-300);
  --s-presented: var(--p-purple-700);
  --s-presented-bg: var(--p-purple-tint);

  /* ── The WhatsApp share button (PRD 023 #21). ─────────────────────── */
  --s-whatsapp: var(--p-brand-whatsapp);
  --s-on-whatsapp: var(--p-brand-whatsapp-ink);

  --s-scroll-thumb: var(--p-paper-500);

  /* ── Type roles ────────────────────────────────────────────────────── */
  --s-font: var(--p-family-sans);
  --s-font-display: var(--p-family-display); /* h1/h2 only - see base/elements.css */
  --s-font-mono: var(--p-family-mono);

  --s-text-body: var(--p-size-14);
  --s-text-row: var(--p-size-135);
  --s-text-meta: var(--p-size-12);
  --s-text-micro: var(--p-size-115);
  --s-text-eyebrow: var(--p-size-105);
  --s-text-title: var(--p-size-25);
  --s-text-figure: var(--p-size-27);
  --s-text-card-title: var(--p-size-14);

  /* ── Spacing roles ─────────────────────────────────────────────────── */
  --s-space-2xs: var(--p-space-4);
  --s-space-xs: var(--p-space-8);
  --s-space-sm: var(--p-space-12);
  --s-space-md: var(--p-space-16);
  --s-space-lg: var(--p-space-20);
  --s-space-xl: var(--p-space-32);

  --s-card-pad-x: var(--p-space-18);
  --s-card-pad-y: var(--p-space-14);
  --s-content-pad-x: var(--p-space-32);
  --s-content-pad-y: var(--p-space-26);

  /* ── Radius roles ──────────────────────────────────────────────────── */
  --s-radius-badge: var(--p-radius-pill);
  --s-radius-small: var(--p-radius-5);
  --s-radius-control: var(--p-radius-6);
  --s-radius-card: var(--p-radius-7);
  --s-radius-track: var(--p-radius-5);
  --s-radius-pill: var(--p-radius-pill);
  --s-radius-bar: var(--p-radius-3);
  --s-radius-avatar: var(--p-radius-circle);

  /* ── Elevation - two tiers now (decision #5): resting surfaces, and
        floating/overlay surfaces. ─────────────────────────────────────── */
  --s-shadow-raised: var(--p-shadow-thumb);
  --s-shadow-popover: var(--p-shadow-popover);

  /* ── Density. One token drives row height across every list. ───────── */
  --s-row-pad: var(--p-space-12);
}

:root[data-density="compacta"] {
  --s-row-pad: 7px;
}


/* PRD 016 moved the body face to Figtree and the display face to Fredoka,
   loaded from Google Fonts. PRD 026 #42 vendors them here instead: two
   third-party origins, a render-blocking stylesheet chain (HTML -> googleapis
   CSS -> gstatic woff2) and the one CSP exception this app carried, all for
   six files totalling ~83 KB that cache immutable like everything else.

   Each file is the variable font Google serves for the family, one per
   subset (latin, latin-ext), so a single face declaration covers every
   weight the design uses. Provenance and licence in fonts/README.md - both
   families are SIL Open Font License 1.1. IBM Plex Mono was always here.

   URLs are absolute on purpose: the production bundler inlines this file into
   css/main.bundle.css and rewrites every absolute /static/ url() to carry the
   file's content hash (PRD 026 #11), so the fonts cache `immutable` like every
   other asset instead of revalidating on each navigation. A relative path
   would resolve from the wrong directory there and fails the bundler's
   versioning check at startup. */

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/static/fonts/ibmplexmono-400.woff2?v=08949f72") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/static/fonts/ibmplexmono-500.woff2?v=01d28544") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/static/fonts/ibmplexmono-600.woff2?v=0d1f0b8d") format("woff2");
}

/* Figtree, variable weight 300-900 (the design uses 400-800), upright. */
@font-face {
  font-family: "Figtree";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("/static/fonts/figtree-var.woff2?v=4ba7d3d0") format("woff2");
  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: "Figtree";
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url("/static/fonts/figtree-var-ext.woff2?v=bf7828e2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Figtree italic, variable; the design asks for 500 only. */
@font-face {
  font-family: "Figtree";
  font-style: italic;
  font-weight: 300 900;
  font-display: swap;
  src: url("/static/fonts/figtree-italic-var.woff2?v=03049837") format("woff2");
  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: "Figtree";
  font-style: italic;
  font-weight: 300 900;
  font-display: swap;
  src: url("/static/fonts/figtree-italic-var-ext.woff2?v=636f1e57") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Fredoka, variable weight 300-700 (the design uses 500-700), headings only. */
@font-face {
  font-family: "Fredoka";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("/static/fonts/fredoka-var.woff2?v=99d6c78e") format("woff2");
  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: "Fredoka";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("/static/fonts/fredoka-var-ext.woff2?v=18a1723c") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

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

  html { -webkit-text-size-adjust: 100%; }

  body,
  h1, h2, h3, h4, p, figure, blockquote, dl, dd { margin: 0; }

  ul[role="list"],
  ol[role="list"] { list-style: none; margin: 0; padding: 0; }

  img, picture, svg, video, canvas { display: block; max-inline-size: 100%; }

  input, button, textarea, select {
    font: inherit;
    color: inherit;
  }

  button { cursor: pointer; }

  [hidden] { display: none !important; }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

/* Bare-element defaults. Anything that needs a class belongs in components/. */

@layer base {
  body {
    background: var(--s-ground);
    color: var(--s-text);
    font-family: var(--s-font);
    font-size: var(--s-text-body);
    line-height: var(--p-leading-body);
    -webkit-font-smoothing: antialiased;
    text-wrap: pretty;
  }

  /* Display face on headings only (spec: "Solo en titulares... nunca en
     cifras/texto") - h3 stays in the body face, since card/section titles
     read as UI chrome, not a display headline. */
  h1, h2 {
    font-family: var(--s-font-display);
    font-weight: var(--p-weight-semibold);
    line-height: var(--p-leading-tight);
    text-wrap: balance;
  }
  h3 {
    font-weight: var(--p-weight-semibold);
    line-height: var(--p-leading-tight);
    text-wrap: balance;
  }

  h1 { font-size: var(--s-text-title); letter-spacing: var(--p-track-title); }
  h2 { font-size: var(--s-text-card-title); }

  a {
    color: var(--s-primary);
    text-decoration: none;
  }

  a:hover { color: var(--s-primary-hover); text-decoration: underline; }

  code, kbd, samp, pre, .mono {
    font-family: var(--s-font-mono);
    letter-spacing: var(--p-track-mono);
  }

  /* The prototype signals focus with border colour alone, which is not enough
     for keyboard users. Every focusable element gets a real ring here. */
  :focus-visible {
    outline: 2px solid var(--s-focus);
    outline-offset: 2px;
  }

  input, select, textarea {
    background: var(--s-surface);
    border: 1px solid var(--s-border-strong);
    border-radius: var(--s-radius-control);
    padding: 0 11px;
    block-size: var(--p-control-34);
    inline-size: 100%;
    font-size: var(--p-size-13);
  }

  textarea { block-size: auto; padding: var(--p-space-8) var(--p-space-10); }

  input:focus, select:focus, textarea:focus { border-color: var(--s-primary); }

  input[type="checkbox"] {
    block-size: 14px;
    inline-size: 14px;
    padding: 0;
    accent-color: var(--s-primary);
  }

  ::selection { background: var(--s-selection); }

  ::-webkit-scrollbar { inline-size: 11px; block-size: 11px; }
  ::-webkit-scrollbar-thumb {
    background: var(--s-scroll-thumb);
    border: 3px solid var(--s-ground);
    border-radius: var(--p-radius-9);
  }
  ::-webkit-scrollbar-track { background: transparent; }
}


/* The frame: a fixed top bar over a sidebar + main grid, each scrolling
   independently so the top bar never scrolls away.

   PRD 016: the sidebar now collapses to a 74px icon rail
   (`.app-shell.is-collapsed`, toggled + persisted by
   static/js/modules/sidebar.js) and, below 860px, becomes a slide-in
   drawer with a scrim - replacing the old "just hide it" placeholder. */

@layer layouts {
  .app-shell {
    /* dvh, not vh: on mobile browsers 100vh is the *large* viewport, measured
       with the URL bar retracted, so a shell sized in vh outgrows the visible
       area and .app-shell__main's scroll box runs off the bottom.
       pages/public.css already uses dvh; the shell never got it (PRD 017 #43). */
    block-size: 100vh;
    block-size: 100dvh;
    display: flex;
    flex-direction: column;
    /* Backstop, so the next control added to the top bar degrades into
       clipping rather than reintroducing a sideways-scrolling document.
       `clip`, not `hidden`: hidden establishes a scroll container, which would
       silently break position:sticky for any descendant. The fixed-position
       drawer is unaffected either way - its containing block is the viewport,
       not this box. */
    overflow-x: clip;
  }

  .app-shell__topbar {
    flex: none;
    block-size: var(--p-topbar-h);
    display: flex;
    align-items: center;
    gap: var(--p-space-14);
    padding: 0 var(--p-space-18) 0 var(--p-space-20);
    background: color-mix(in srgb, var(--s-ground) 88%, transparent);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border-block-end: 1px solid var(--s-border);
    position: relative;
    z-index: 30;
  }

  .app-shell__body {
    display: grid;
    grid-template-columns: var(--p-sidebar-w) minmax(0, 1fr);
    flex: 1;
    min-block-size: 0;
    transition: grid-template-columns 0.16s ease;
  }

  .app-shell.is-collapsed .app-shell__body {
    grid-template-columns: var(--p-sidebar-w-collapsed) minmax(0, 1fr);
  }

  .app-shell__sidebar {
    background: var(--s-surface-nav);
    border-inline-end: 1px solid var(--s-border);
    padding: var(--p-space-14) var(--p-space-12) var(--p-space-26);
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overflow-x: hidden;
  }

  .app-shell__main {
    overflow-y: auto;
    padding: var(--s-content-pad-y) var(--s-content-pad-x) var(--p-space-72);
  }

  .app-shell__content {
    max-inline-size: var(--p-content-max);
    margin-inline: auto;
  }

  .topbar-user {
    display: flex;
    align-items: center;
    gap: var(--p-space-10);
  }

  .topbar-user__logout { display: contents; }

  /* The compact "copy my referral link" control (PRD 016), styled after
     the source design's pill-shaped `.linkbtn`. */
  .topbar-invite {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 3px 3px 3px 11px;
    background: var(--s-surface);
    border: 1px solid var(--s-border-strong);
    border-radius: var(--s-radius-pill);
    box-shadow: var(--s-shadow-raised);
  }
  .topbar-invite__code {
    font-size: var(--s-text-meta);
    color: var(--s-text-muted);
    max-inline-size: 12ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .topbar-invite__icon {
    display: grid;
    place-items: center;
    inline-size: var(--p-control-26);
    block-size: var(--p-control-26);
    flex: none;
    border-radius: var(--s-radius-avatar);
    border: none;
    background: none;
    color: var(--s-text-soft);
    cursor: pointer;
  }
  .topbar-invite__icon:hover { background: var(--s-surface-inset); color: var(--s-text); }

  /* The copy control carries its label, so unlike the WhatsApp icon beside it
     this one sizes to content rather than to a fixed square. A bare icon told
     nobody what the control does - least of all the collaborators it exists
     for (invite-button-label). */
  .topbar-invite__btn {
    display: inline-flex;
    align-items: center;
    gap: var(--p-space-6);
    flex: none;
    block-size: var(--p-control-26);
    padding-inline: var(--p-space-8);
    border: none;
    border-radius: var(--s-radius-pill);
    background: none;
    color: var(--s-text-soft);
    cursor: pointer;
    font: inherit;
  }
  .topbar-invite__btn:hover { background: var(--s-surface-inset); color: var(--s-text); }

  /* `copy-link.js` toggles `is-copied` on whatever carries
     `data-copy-trigger` - the button, not the icon. A rule left on the icon
     would look right and silently never fire. */
  .topbar-invite__btn.is-copied { color: var(--s-success); }

  .topbar-invite__label {
    font-size: var(--s-text-meta);
    font-weight: var(--p-weight-semibold);
    white-space: nowrap;
  }

  @media (max-width: 620px) {
    .topbar-invite__code { display: none; }
    /* 38px of horizontal padding is a desktop luxury at this width. */
    .app-shell__topbar { padding-inline: var(--p-space-10); }
  }

  /* Below 620px the control is two icon buttons and nothing else - the same
     breakpoint that hides the code, because the label and the code are both
     the desktop detail. The width that buys back is what lets the WhatsApp
     share and the avatar stay on a phone, both of which earlier revisions
     dropped and neither of which needs to go. */
  @media (max-width: 620px) {
    /* Visually hidden, never `display: none`: the button is icon-only here and
       removing its only text would leave it with no accessible name. */
    .topbar-invite__label {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
    /* With the label gone the button collapses to the same square as the
       WhatsApp icon beside it, so the two read as a matched pair. */
    .topbar-invite__btn {
      inline-size: var(--p-control-26);
      padding-inline: 0;
      gap: 0;
    }
  }

  @media (max-width: 520px) {

    /* Below 520px the mark carries the brand on its own in the top bar.
       Scoped to the top bar deliberately: `.brand__name` appears in twelve
       templates, and an unscoped rule also blanked the wordmark on the login
       screen, the client portal, the public lead form and all five signing
       pages - none of which have a crowding problem.

       Visually hidden rather than `display: none`, because `.brand` is a link
       whose only text node is this span (`_topbar.html`, with `alt=""` on the
       mark and no aria-label). Removing it from the accessibility tree leaves
       the link with an empty accessible name - a WCAG 2.4.4 / 4.1.2 failure.
       Same recipe as `.u-visually-hidden`. */
    .app-shell__topbar .brand__name {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip-path: inset(50%);
      white-space: nowrap;
    }
  }

  .theme-toggle {
    display: grid;
    place-items: center;
    inline-size: var(--p-control-32);
    block-size: var(--p-control-32);
    flex: none;
    border-radius: var(--s-radius-avatar);
    border: 1px solid transparent;
    background: transparent;
    color: var(--s-text-soft);
  }
  .theme-toggle:hover { background: var(--s-surface-inset); color: var(--s-text); }
  .theme-toggle svg { grid-area: 1 / 1; }
  .theme-toggle__moon { display: none; }
  /* Dark mode shows the moon icon (what you're currently in), light mode
     shows the sun - same convention most theme toggles use. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .theme-toggle__sun { display: none; }
    :root:not([data-theme="light"]) .theme-toggle__moon { display: block; }
  }
  :root[data-theme="dark"] .theme-toggle__sun { display: none; }
  :root[data-theme="dark"] .theme-toggle__moon { display: block; }

  /* ── Collapse-to-rail (desktop ≥ 860px) ─────────────────────────────── */
  .sidebar-collapse {
    align-self: flex-end;
    display: grid;
    place-items: center;
    inline-size: 28px;
    block-size: 28px;
    margin-block-end: var(--p-space-8);
    border-radius: var(--s-radius-control);
    border: 1px solid transparent;
    background: transparent;
    color: var(--s-text-faint);
  }
  .sidebar-collapse:hover { background: var(--s-surface-inset); color: var(--s-text); }
  .sidebar-collapse svg { transition: transform 0.16s ease; }
  .app-shell.is-collapsed .sidebar-collapse { align-self: center; }
  .app-shell.is-collapsed .sidebar-collapse svg { transform: rotate(180deg); }

  .app-shell.is-collapsed .nav-group__label,
  .app-shell.is-collapsed .lbl,
  .app-shell.is-collapsed .nav-badge {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .app-shell.is-collapsed .nav-item {
    justify-content: center;
    padding-inline: 0;
  }

  /* ── Mobile drawer (< 860px) ─────────────────────────────────────────── */
  .burger { display: none; }

  .scrim {
    display: none;
    position: fixed;
    inset: 0;
    /* Pure black, not a ground-tinted value: at 45% it must read as a dim
       wash in both themes, and a dark tint over an already-dark ground
       (dark mode) would be nearly invisible. */
    background: rgb(0 0 0 / 45%);
    z-index: 55;
  }
  .scrim.is-open { display: block; }

  @media (max-width: 860px) {
    .app-shell__body,
    .app-shell.is-collapsed .app-shell__body {
      grid-template-columns: minmax(0, 1fr);
    }

    .app-shell__sidebar {
      position: fixed;
      /* A fixed element is laid out against the initial containing block, so
         `100dvh` on .app-shell cannot reach it - an earlier draft of PRD 017
         claimed one token fixed both symptoms and was wrong (caveat #61).
         `inset-block` alone resolves against the *large* viewport on mobile,
         putting the drawer's last nav item under the browser chrome. */
      inset-block: var(--p-topbar-h) auto;
      block-size: calc(100vh - var(--p-topbar-h));
      block-size: calc(100dvh - var(--p-topbar-h));
      inset-inline-start: 0;
      inline-size: 270px;
      z-index: 60;
      transform: translateX(-100%);
      transition: transform 0.2s ease;
      box-shadow: var(--s-shadow-popover);
    }
    .app-shell__sidebar.is-open { transform: none; }

    /* The rail-collapse visuals never apply on mobile - the drawer is
       either fully open or fully hidden, never a narrow rail. */
    .app-shell.is-collapsed .nav-group__label,
    .app-shell.is-collapsed .lbl,
    .app-shell.is-collapsed .nav-badge {
      position: static;
      inline-size: auto;
      block-size: auto;
      padding: revert;
      margin: revert;
      overflow: visible;
      clip-path: none;
      white-space: normal;
    }
    .app-shell.is-collapsed .nav-item { justify-content: flex-start; padding-inline: var(--p-space-10); }
    .sidebar-collapse { display: none; }

    .burger {
      display: grid;
      place-items: center;
      inline-size: 36px;
      block-size: 36px;
      flex: none;
      border-radius: var(--s-radius-control);
      border: 1px solid var(--s-border-strong);
      background: var(--s-surface);
      color: var(--s-text);
    }
    .burger:hover { background: var(--s-surface-inset); }
  }
}

/* Eyebrow → title → subtitle, with actions pushed right.
   The eyebrow states the module group, so the user always knows the lens. */

@layer layouts {
  .page-head {
    display: flex;
    align-items: flex-start;
    gap: var(--s-space-md);
    flex-wrap: wrap;
    margin-block-end: var(--p-space-20);
  }

  .page-head__eyebrow {
    font-size: var(--s-text-eyebrow);
    font-weight: var(--p-weight-semibold);
    letter-spacing: var(--p-track-group);
    text-transform: uppercase;
    color: var(--s-text-faint);
    margin-block-end: var(--p-space-6);
  }

  .page-head__subtitle {
    font-size: var(--s-text-row);
    color: var(--s-text-muted);
    max-inline-size: 70ch;
    margin-block-start: var(--p-space-6);
  }

  .page-head__actions {
    margin-inline-start: auto;
    display: flex;
    gap: var(--s-space-xs);
    flex-wrap: wrap;
  }
}


@layer components {
  .avatar {
    --avatar-size: 30px;

    display: grid;
    place-items: center;
    inline-size: var(--avatar-size);
    block-size: var(--avatar-size);
    flex: none;
    /* The brand's diagonal-split two-tone motif (design spec §3.1), the one
       place in this app it earns its keep: an "initials mark" is exactly
       what the motif was designed for. White text needs the saturated
       halves this implies, not the soft tint the rest of the app uses. */
    background: linear-gradient(135deg, var(--s-primary) 0 49.5%, var(--p-brand-cyan) 50.5% 100%);
    color: var(--s-on-primary);
    border-radius: var(--s-radius-avatar);
    font-size: var(--p-size-12);
    font-weight: var(--p-weight-semibold);
  }

  .avatar--sm { --avatar-size: 22px; font-size: var(--p-size-10); background: var(--s-neutral-bg); color: var(--s-text-muted); }
}

/* Status travels as a background/text pair plus a written label. Never colour
   alone - the label is what carries the meaning. */

@layer components {
  .badge {
    --badge-bg: var(--s-neutral-bg);
    --badge-fg: var(--s-neutral);

    display: inline-block;
    padding: var(--p-space-2) var(--p-space-8);
    background: var(--badge-bg);
    color: var(--badge-fg);
    /* A tint of the badge's own text colour, not a new token per status -
       one rule produces a border matching every variant automatically. */
    border: 1px solid color-mix(in srgb, var(--badge-fg) 30%, transparent);
    border-radius: var(--s-radius-badge);
    font-size: var(--s-text-micro);
    font-weight: var(--p-weight-semibold);
    white-space: nowrap;
  }

  .badge--success   { --badge-bg: var(--s-success-bg);   --badge-fg: var(--s-success); }
  .badge--warning   { --badge-bg: var(--s-warning-bg);   --badge-fg: var(--s-warning); }
  .badge--danger    { --badge-bg: var(--s-danger-bg);    --badge-fg: var(--s-danger); }
  .badge--info      { --badge-bg: var(--s-info-bg);      --badge-fg: var(--s-info); }
  .badge--presented { --badge-bg: var(--s-presented-bg); --badge-fg: var(--s-presented); }
  .badge--muted     { --badge-bg: var(--s-neutral-bg);   --badge-fg: var(--s-text-faint); }

  /* Sidebar alert count: white on brand pink, pill-shaped, solid - no
     border, the saturated fill already reads as a distinct mark. */
  .badge--alert {
    --badge-bg: var(--s-accent);
    --badge-fg: var(--s-on-primary);
    padding: var(--p-space-1) var(--p-space-6);
    border-color: transparent;
    border-radius: var(--s-radius-pill);
    font-family: var(--s-font-mono);
    font-size: var(--p-size-10);
  }
}

@layer components {
  .brand {
    display: flex;
    align-items: center;
    gap: var(--p-space-10);
    inline-size: 228px;
    flex: none;
    color: inherit;
    text-decoration: none;
  }

  .brand:hover { color: inherit; text-decoration: none; }

  /* The real mark (PRD 016): a transparent-background pineapple, taller
     than it is wide. Natural aspect ratio, not a cropped square - same
     treatment the source design gives it (width fixed, height auto). */
  .brand__mark {
    inline-size: 22px;
    block-size: auto;
    flex: none;
    display: block;
  }

  .brand__names { display: flex; flex-direction: column; line-height: var(--p-leading-snug); }

  .brand__name {
    font-family: var(--s-font-display);
    font-weight: var(--p-weight-semibold);
    letter-spacing: var(--p-track-brand);
  }

  .brand__tagline {
    font-size: var(--p-size-11);
    color: var(--s-text-soft);
  }

  /* PRD 017 #42. The 228px above is an alignment device, not a size: plus the
     top bar's 20px left padding it equals --p-sidebar-w (248px), so the
     brand's right edge lines up with the sidebar's. Below 860px the sidebar
     is a fixed-position drawer and there is nothing left to line up with -
     while the fixed, non-shrinkable width is 41% of a top bar that overflows
     a phone by ~194px. */
  @media (max-width: 860px) {
    .brand { inline-size: auto; }
  }
}

/* Tier-3 tokens on the block, so a variant is a token override rather than a
   second set of rules. Labels never wrap inside a fixed-height button. */

@layer components {
  .btn {
    --btn-h: var(--p-control-34);
    --btn-pad: 0 var(--p-space-14);
    --btn-bg: var(--s-surface);
    --btn-fg: var(--s-text);
    --btn-border: var(--s-border-strong);
    --btn-bg-hover: var(--s-surface-inset);
    --btn-border-hover: var(--s-border-strong);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--p-space-6);
    block-size: var(--btn-h);
    padding: var(--btn-pad);
    background: var(--btn-bg);
    color: var(--btn-fg);
    border: 1px solid var(--btn-border);
    border-radius: var(--s-radius-pill);
    font-size: var(--p-size-13);
    font-weight: var(--p-weight-medium);
    white-space: nowrap;
    flex: none;
  }

  .btn:hover {
    background: var(--btn-bg-hover);
    border-color: var(--btn-border-hover);
  }

  .btn[disabled] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  .btn--primary {
    --btn-pad: 0 var(--p-space-16);
    --btn-bg: var(--s-primary);
    --btn-fg: var(--s-on-primary);
    --btn-border: var(--s-primary);
    --btn-bg-hover: var(--s-primary-hover);
    --btn-border-hover: var(--s-primary-hover);
  }

  /* The one third-party colour in the system (PRD 023 #21): users read the
     green faster than the word. Its hover is the same green, darkened by
     the border alone - there is no second WhatsApp green to reach for. */
  .btn--whatsapp {
    --btn-pad: 0 var(--p-space-16);
    --btn-bg: var(--s-whatsapp);
    --btn-fg: var(--s-on-whatsapp);
    --btn-border: var(--s-whatsapp);
    --btn-bg-hover: var(--s-whatsapp);
    --btn-border-hover: var(--s-on-whatsapp);
    font-weight: var(--p-weight-semibold);
  }

  .btn--danger {
    --btn-fg: var(--s-danger);
    --btn-bg-hover: var(--s-danger-bg);
  }

  /* Toolbar and card-header sizes. Height is a token, so nothing else moves. */
  .btn--toolbar { --btn-h: var(--p-control-32); --btn-pad: 0 var(--p-space-12); }

  .btn--small {
    --btn-h: var(--p-control-28);
    --btn-pad: 0 var(--p-space-10);
    --btn-fg: var(--s-text-muted);
    font-size: var(--p-size-125);
  }

  .btn--block { inline-size: 100%; }

  .btn.is-copied { --btn-fg: var(--s-success); }

  /* Row actions: present, but not competing with the page's real action.
     Used by the opportunities table, which named it before it existed. */
  .btn--ghost {
    background: transparent;
    border-color: transparent;
    color: var(--s-primary);
    padding-inline: var(--p-space-6);
  }
  .btn--ghost:hover { background: var(--s-primary-tint); }
}

/* Cards separate with a 1px border plus a soft raised shadow (PRD 016
   decision #5 - overturns this file's original "no elevation" rule). */

@layer components {
  .card {
    background: var(--s-surface);
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-card);
    box-shadow: var(--s-shadow-raised);
    overflow: hidden;
  }

  .card__header {
    display: flex;
    align-items: center;
    gap: var(--p-space-10);
    padding: var(--p-space-12) var(--s-card-pad-x);
    background: var(--s-surface-sunken);
    border-block-end: 1px solid var(--s-border);
  }

  .card__title {
    font-size: var(--s-text-card-title);
    font-weight: var(--p-weight-semibold);
  }

  .card__meta {
    font-family: var(--s-font-mono);
    font-size: var(--s-text-micro);
    color: var(--s-text-faint);
  }

  .card__actions { margin-inline-start: auto; display: flex; gap: var(--p-space-8); }

  .card__body { padding: var(--s-card-pad-y) var(--s-card-pad-x); }

  /* Row lists inside a card. --s-row-pad is the density token: switching it
     on :root re-spaces every list in the app at once. */
  .card__row {
    display: flex;
    align-items: center;
    gap: var(--p-space-14);
    padding: var(--s-row-pad) var(--s-card-pad-x);
    border-block-end: 1px solid var(--s-border-subtle);
  }

  .card__row:last-child { border-block-end: 0; }
  .card__row:hover { background: var(--s-surface-sunken); }

  .card--flush .card__body { padding: 0; }

  /* A card that is itself a link (Home's quick-link tiles). */
  .home-tile {
    display: block;
    color: inherit;
    text-decoration: none;
    transition: border-color 0.12s ease;
  }
  .home-tile:hover {
    border-color: var(--s-primary);
    color: inherit;
    text-decoration: none;
  }
  .home-tile h2 { margin-block-end: var(--p-space-4); }
  .home-tile p { margin: 0; color: var(--s-text-muted); font-size: var(--s-text-row); }
}

/* Document slots - the mockup's docgrid section, transcribed. */

@layer components {
  .docgrid {
    display: grid;
    gap: var(--p-space-14);
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  }

  .doc {
    display: flex;
    flex-direction: column;
    gap: var(--p-space-8);
    padding: var(--p-space-14);
    background: var(--s-surface);
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-card);
    box-shadow: var(--s-shadow-raised);
  }

  .doc--filled { background: var(--s-surface-sunken); }
  /* Its sibling was styled and this was not, so a document that failed
     validation looked exactly like one that passed. */
  .doc--invalid { border-color: var(--s-danger); }

  .doc__head { display: flex; align-items: baseline; gap: var(--p-space-8); }
  .doc__name { font-size: var(--s-text-row); font-weight: var(--p-weight-medium); }
  .doc__head .badge { margin-inline-start: auto; flex: none; }

  .doc__where {
    font-size: var(--s-text-meta);
    color: var(--s-text-faint);
    line-height: 1.4;
  }

  .doc__file {
    display: flex;
    align-items: baseline;
    /* Two items on a document slot, five on the templates list - name,
       state, link and two metadata columns. Nothing in that row can shrink,
       so on a phone it ran past the edge of the screen. Wrapping changes
       nothing for the two-item case, which never reaches the wrap point. */
    flex-wrap: wrap;
    gap: var(--p-space-8);
    min-inline-size: 0;
    font-size: var(--s-text-meta);
    color: var(--s-text-muted);
  }
  .doc__filename { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .doc__size {
    margin-inline-start: auto;
    flex: none;
    font-family: var(--s-font-mono);
    color: var(--s-text-faint);
  }

  /* The native control, exactly as the browser ships it - dashed box, its
     own button, its own text. It is deliberately *not* hidden behind a
     styled label: doing that removes drag-and-drop, which a file input gives
     for free and which people use. */
  .doc input[type="file"] {
    inline-size: 100%;
    block-size: auto;
    padding: 7px 9px;
    font-size: var(--s-text-meta);
    background: var(--s-surface);
    border: 1px dashed var(--s-border-strong);
    border-radius: var(--s-radius-control);
  }
  .doc input[type="file"]::file-selector-button {
    margin-inline-end: 9px;
    padding: 3px 9px;
    font-size: var(--s-text-meta);
    background: var(--s-surface);
    color: var(--s-text);
    border: 1px solid var(--s-border-strong);
    border-radius: var(--s-radius-small);
    cursor: pointer;
  }
  .doc input[type="file"]::file-selector-button:hover { background: var(--s-surface-inset); }
  .doc input[type="file"]:disabled { cursor: not-allowed; opacity: 0.55; }

  /* What the slot already holds, stated above the control. The browser will
     still say "no file selected" inside the input - it always does - so this
     line is what stops that from reading as "your document is gone". */
  .doc__stored {
    display: flex;
    align-items: baseline;
    gap: var(--p-space-8);
    min-inline-size: 0;
    padding: 5px 9px;
    font-size: var(--s-text-meta);
    background: var(--s-surface);
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-control);
  }
  .doc__stored-label {
    flex: none;
    font-weight: var(--p-weight-medium);
    color: var(--s-text);
  }
  .doc__stored-name {
    min-inline-size: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--s-font-mono);
    color: var(--s-text-muted);
  }

  .doc__hint {
    font-size: var(--s-text-meta);
    color: var(--s-text-faint);
    line-height: 1.4;
  }

  .doc__date {
    display: flex;
    align-items: center;
    gap: var(--p-space-8);
    font-size: var(--s-text-meta);
    color: var(--s-text-muted);
  }
  .doc__date input { block-size: 28px; inline-size: auto; flex: none; font-size: var(--s-text-meta); }

  /* PRD 014 - the guidance disclosure. Collapsed by default; the native
     <details> marker is left alone rather than hidden, same reasoning as
     the file input above: the browser's own affordance over a reinvented
     one. No elevation - this system has exactly one shadow token and a
     disclosure that expands inline never needed it. */
  .doc__guide {
    border-top: 1px solid var(--s-border-faint);
    padding-top: var(--p-space-8);
  }
  .doc__guide-toggle {
    font-size: var(--s-text-meta);
    font-weight: var(--p-weight-medium);
    color: var(--s-primary);
    cursor: pointer;
  }
  .doc__guide-toggle:hover { color: var(--s-primary-hover); }
  .doc__guide-body {
    display: flex;
    flex-direction: column;
    gap: var(--p-space-8);
    margin-top: var(--p-space-8);
  }
  .doc__guide-link { font-size: var(--s-text-meta); }
  .doc__guide-link a { color: var(--s-primary); }
  .doc__guide-diagram {
    display: block;
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-control);
    background: var(--s-surface-inset);
  }
  .doc__guide-checklist {
    margin: 0;
    padding-inline-start: var(--p-space-16);
    font-size: var(--s-text-meta);
    color: var(--s-text-muted);
    line-height: 1.4;
    display: flex;
    flex-direction: column;
    gap: var(--p-space-4);
  }
  .doc__guide-faq { display: flex; flex-direction: column; gap: var(--p-space-4); }
  .doc__faq-item { font-size: var(--s-text-meta); }
  .doc__faq-item summary { font-weight: var(--p-weight-medium); color: var(--s-text); cursor: pointer; }
  .doc__faq-item p { margin: var(--p-space-4) 0 0; color: var(--s-text-muted); line-height: 1.4; }

  /* PRD 014 - client-side preview + retake, rendered by doc-enhance.js.
     Markup only exists once the module inserts it; no template branch. */
  .doc__preview {
    display: block;
    max-inline-size: 160px;
    max-block-size: 160px;
    object-fit: cover;
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-control);
  }
  .doc__retake {
    align-self: flex-start;
    font-size: var(--s-text-meta);
    font-weight: var(--p-weight-medium);
    color: var(--s-primary);
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
  }
  .doc__retake:hover { color: var(--s-primary-hover); }

  /* No breakpoints existed on this section before PRD 014. */
  @media (max-width: 480px) {
    .docgrid { grid-template-columns: 1fr; }
    .doc__guide-diagram { max-inline-size: 96px; }
    .doc__preview { max-inline-size: 120px; max-block-size: 120px; }
  }
}

/* Form fields - label, requirement markers, help text and validation
   messages. Meaning always travels in the text prefix, never colour alone. */

@layer components {
  .grid { display: grid; gap: var(--p-space-14); }
  .grid--2 { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
  .grid--3 { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

  .field {
    display: flex;
    flex-direction: column;
    gap: 5px;
    min-inline-size: 0;
  }

  /* `.field` is a flex column, so its default `align-items: stretch` makes
     every child fill the column - which is what an input wants and what a
     badge does not. Every other field's value is bare text, where the stretch
     is invisible; a badge has a background, so "Lead" rendered as a grey bar
     the full width of the column. Only the badge opts out. */
  .field > .badge { align-self: start; }

  .field > label {
    display: flex;
    gap: var(--p-space-6);
    align-items: baseline;
    font-size: var(--p-size-125);
    font-weight: var(--p-weight-medium);
    color: var(--s-text-muted);
  }

  .field__req { color: var(--s-danger); font-weight: var(--p-weight-semibold); }

  .field__opt {
    font-size: var(--p-size-105);
    font-weight: var(--p-weight-semibold);
    color: var(--s-text-faint);
    text-transform: uppercase;
    letter-spacing: var(--p-track-eyebrow);
  }

  .field__help { font-size: var(--s-text-meta); color: var(--s-text-faint); }

  /* Already here before field-level errors existed, and used by nothing -
     a rule waiting for this feature. Reused rather than replaced by a second
     invalid class, and widened to the controls that are not <input>. */
  .field.is-invalid input,
  .field.is-invalid select,
  .field.is-invalid textarea { border-color: var(--s-danger); }

  /* The message itself. Sits under the control, not beside the label, so the
     eye lands on it after reading what it is about. */
  .field__error {
    margin-block-start: var(--p-space-4, 4px);
    font-size: var(--s-text-meta);
    color: var(--s-danger);
  }

  .msg {
    display: flex;
    gap: var(--p-space-6);
    align-items: flex-start;
    font-size: var(--p-size-125);
    line-height: 1.4;
  }
  .msg::before {
    font-family: var(--s-font-mono);
    font-weight: var(--p-weight-semibold);
    flex: none;
  }
  .msg--ok { color: var(--s-success); }
  .msg--ok::before { content: "OK"; }
  .msg--bad { color: var(--s-danger); }
  .msg--bad::before { content: "!!"; }
  .msg--warn { color: var(--s-warning); }
  .msg--warn::before { content: "/!"; }
}

  /* The country prefix and the number read as one control (phone-validation).
     `align-items: stretch` so the select matches the input's height without
     either being told what that height is. */
  .phone-group {
    display: flex;
    align-items: stretch;
    gap: var(--p-space-6);
  }
  .phone-group__prefix {
    flex: none;
    inline-size: auto;
    font-family: var(--s-font-mono);
  }
  .phone-group__number { flex: 1 1 auto; min-inline-size: 0; 
  /* A checkbox reads as one line, not as a stacked label and control. The
     public form named this and nothing defined it, so the box and its text
     stacked apart from each other. */
  .field--check > label {
    display: flex;
    align-items: flex-start;
    gap: var(--p-space-8);
    font-weight: var(--p-weight-regular, inherit);
  }
  .field--check input[type="checkbox"] { margin-block-start: 2px; }
}

/* Numbered form blocks (the mockup's block/block-head/block-num), the
   entity segmented toggle, conditional visibility and the sticky footer.

   Conditional visibility is CSS state, not JS: the entity radios and the
   same-as-fiscal checkbox drive :has() rules, so the no-JS form is fully
   functional before any module loads. */

@layer components {
  .block { margin-block-end: var(--p-space-14); overflow: visible; }

  .block-head {
    display: flex;
    align-items: center;
    gap: var(--p-space-12);
    padding: var(--p-space-12) var(--s-card-pad-x);
    background: var(--s-surface-sunken);
    border-block-end: 1px solid var(--s-border);
    border-radius: var(--s-radius-card) var(--s-radius-card) 0 0;
  }

  .block-num {
    inline-size: 22px;
    block-size: 22px;
    flex: none;
    display: grid;
    place-items: center;
    background: var(--s-primary);
    color: var(--s-on-primary);
    border-radius: var(--s-radius-badge);
    font-family: var(--s-font-mono);
    font-size: var(--p-size-11);
    font-weight: var(--p-weight-semibold);
  }

  .block-head .block-hint {
    margin-inline-start: auto;
    font-size: var(--s-text-meta);
    color: var(--s-text-faint);
  }

  .block-body {
    padding: var(--p-space-16) var(--s-card-pad-x);
    display: flex;
    flex-direction: column;
    gap: var(--p-space-16);
  }

  /* ── Entity segmented toggle (radios visually hidden, labels styled) ── */
  .segbar {
    display: inline-flex;
    gap: 2px;
    padding: 3px;
    background: var(--s-neutral-bg);
    border-radius: var(--s-radius-track);
  }

  /* The radio is hidden but still has to be a real, focusable sibling: the
     checked state of the whole control is read off it. `position: absolute`
     alone was not enough to hide its box, because `base/elements.css` sizes
     every input at `inline-size: 100%` and corrects only the checkbox - so
     an absolutely positioned radio with no positioned ancestor resolved that
     100% against the *viewport*, and left a 1440px-wide invisible box
     hanging off the right of the client form. The page scrolled sideways
     into nothing. Give it the same 1px box the visually-hidden recipe uses. */
  .segbar input {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    opacity: 0;
    pointer-events: none;
  }

  .segbar label {
    block-size: 28px;
    display: inline-flex;
    align-items: center;
    padding: 0 var(--p-space-12);
    border-radius: var(--s-radius-small);
    color: var(--s-text-muted);
    font-size: var(--p-size-13);
    cursor: pointer;
  }

  #et-autonomo:checked ~ .segbar label[for="et-autonomo"],
  #et-sociedad:checked ~ .segbar label[for="et-sociedad"],
  .segbar input:checked + label {
    background: var(--s-surface);
    color: var(--s-text);
    font-weight: var(--p-weight-semibold);
    box-shadow: var(--s-shadow-raised);
  }

  .segbar input:focus-visible + label {
    outline: 2px solid var(--s-focus);
    outline-offset: 2px;
  }

  /* ── Conditional sections, driven from form state ───────────────────── */
  form:has(#et-sociedad:checked) .only-autonomo { display: none; }
  form:has(#et-autonomo:checked) .only-sociedad { display: none; }
  form:has(#et-sociedad:checked) .only-autonomo-inline { display: none; }
  form:has(#et-autonomo:checked) .only-sociedad-inline { display: none; }

  /* ── Sticky footer ──────────────────────────────────────────────────── */
  .formfoot {
    position: sticky;
    bottom: 0;
    margin-block-start: var(--p-space-16);
    display: flex;
    gap: var(--p-space-10);
    align-items: center;
    flex-wrap: wrap;
    padding: var(--p-space-12) var(--s-card-pad-x);
    background: var(--s-surface);
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-card);
    box-shadow: var(--s-shadow-raised);
  }

  /* The status takes the room that is left and wraps inside its own box.
     `margin-inline-end: auto` alone only holds while everything fits on one
     line: naming six documents by their Spanish names rather than by their
     enum constants made the line long enough to wrap, and the button dropped
     onto a second row at the left, which read as a different control. The
     `min-inline-size: 0` is what lets a flex item shrink below its content. */
  .formfoot__status {
    /* Basis rather than `auto`, because the container wraps: with `auto` the
       item's base size is its full text, which exceeds the row and sends the
       button to a line of its own. A 320px basis grows to fill whatever is
       left beside the button on a desktop and only wraps when the row really
       is too narrow for both, which is a phone. */
    flex: 1 1 320px;
    min-inline-size: 0;
    margin-inline-end: auto;
  }

  /* And if it does wrap - a phone - the button still belongs on the right. */
  .formfoot > .btn { margin-inline-start: auto; }
}

/* The login screen: centred card, brand above, no app chrome. */

@layer components {
  .loginpage {
    /* dvh for the same reason the app shell uses it: on mobile 100vh is the
       large viewport, so the "centred" card sits low and the page scrolls
       vertically for no reason (PRD 017 #43). No shell here, so this is the
       only rule the login screen needs. */
    min-block-size: 100vh;
    min-block-size: 100dvh;
    display: grid;
    place-items: center;
    padding: var(--p-space-26);
    background: var(--s-ground);
  }

  .loginbox { inline-size: 100%; max-inline-size: 390px; }

  .loginbox__head {
    display: flex;
    align-items: center;
    gap: var(--p-space-10);
    margin-block-end: var(--p-space-16);
  }

  .loginbox .card { padding: var(--p-space-26); }

  .loginbox__title { font-size: 21px; margin-block-end: 3px; }

  .loginbox__sub {
    margin-block-end: var(--p-space-20);
    color: var(--s-text-muted);
    font-size: var(--s-text-row);
  }

  .loginbox__fields {
    display: flex;
    flex-direction: column;
    gap: var(--p-space-14);
  }
}

/* Sidebar navigation. The active item is marked twice - tinted background and
   a 2px inset rail - so it survives being read without colour. */

@layer components {
  .nav-group { margin-block-end: var(--p-space-14); }
  .nav-group--top { margin-block-end: var(--p-space-16); }

  /* The markup doesn't carry role="list" (reset.css's list-style reset is
     scoped to that), so it falls back to default browser bullets - a
     pre-existing gap only visible now that nav items carry icons. */
  .nav-group ul { list-style: none; margin: 0; padding: 0; }

  .nav-group__label {
    padding: 0 var(--p-space-10) var(--p-space-6);
    font-size: var(--p-size-10);
    font-weight: var(--p-weight-semibold);
    letter-spacing: var(--p-track-group);
    text-transform: uppercase;
    color: var(--s-text-faint);
  }

  .nav-item {
    display: flex;
    align-items: center;
    gap: var(--p-space-8);
    inline-size: 100%;
    padding: 7px var(--p-space-10);
    margin-block-end: var(--p-space-1);
    border-radius: var(--s-radius-control);
    font-size: var(--s-text-row);
    color: var(--s-text-muted);
    text-decoration: none;
  }

  /* Was a hardcoded #E7E4DA - the one place in this layer that bypassed the
     token system (PRD 016 decision #14). --s-surface-inset is the same
     "ghost hover" role every other hover-without-a-dedicated-token uses. */
  .nav-item:hover {
    background: var(--s-surface-inset);
    color: var(--s-text);
    text-decoration: none;
  }

  .nav-item.is-active {
    background: var(--s-primary-tint);
    color: var(--s-primary);
    font-weight: var(--p-weight-semibold);
    box-shadow: inset 2px 0 0 var(--s-primary);
  }

  .nav-item__ic {
    inline-size: 16px;
    block-size: 16px;
    flex: none;
    opacity: 0.85;
  }

  .nav-item__count {
    margin-inline-start: auto;
    font-family: var(--s-font-mono);
    font-size: var(--p-size-11);
    color: var(--s-text-faint);
  }

  .nav-item .badge--alert,
  .nav-item .nav-badge { margin-inline-start: auto; }

  .nav-footer {
    margin-block-start: auto;
    padding-block-start: var(--p-space-12);
    border-block-start: 1px solid var(--s-border);
    font-size: var(--s-text-micro);
    color: var(--s-text-faint);
  }

  .nav-footer p { margin: 0; }

  .app-shell.is-collapsed .nav-footer { display: none; }
}

/* The admin notification feed: severity filter bar, rows, pager.

   The sidebar count itself needs no rules here - `.badge--alert` and
   `.nav-item .badge--alert` were already authored in badge.css and nav.css
   and had no consumer until this feature existed. */

@layer components {
  /* ── the severity filter ─────────────────────────────────────────── */
  .filterbar {
    display: flex;
    gap: var(--p-space-4);
    flex-wrap: wrap;
  }

  .filterbar__item {
    padding: var(--p-space-4) var(--p-space-10);
    border-radius: var(--s-radius-pill);
    font-size: var(--s-text-row);
    color: var(--s-text-muted);
    text-decoration: none;
  }

  .filterbar__item:hover {
    background: var(--s-surface-inset);
    color: var(--s-text);
    text-decoration: none;
  }

  /* Marked twice - tint plus weight - so the active filter survives being
     read without colour, same rule the nav follows. */
  .filterbar__item.is-active {
    background: var(--s-primary-tint);
    color: var(--s-primary);
    font-weight: var(--p-weight-semibold);
  }

  /* ── rows ────────────────────────────────────────────────────────── */
  /* An acknowledged row recedes rather than disappearing: the feed is a
     record you can walk into and read, not an inbox that empties. */
  .notif.is-acknowledged {
    color: var(--s-text-faint);
  }

  .notif.is-acknowledged .notif__message {
    color: var(--s-text-soft);
  }

  .notif__when {
    white-space: nowrap;
    color: var(--s-text-muted);
    font-size: var(--s-text-micro);
  }

  .notif__message {
    /* The longest column by far - a rubric check sentence or a provider
       failure reason. Everything else stays on one line beside it. */
    inline-size: 100%;
  }

  .notif__state {
    white-space: nowrap;
    text-align: end;
  }

  /* The sidebar badge's own wrapper. Empty when nothing is unacknowledged,
     and an empty inline element must not open a gap in the nav item. */
  .nav-badge:empty {
    display: none;
  }

  /* ── pagination ──────────────────────────────────────────────────── */
  .pager {
    display: flex;
    gap: var(--p-space-8);
    align-items: center;
    justify-content: flex-end;
    margin-block-start: var(--p-space-14);
  }

  .pager__position {
    font-size: var(--s-text-micro);
    color: var(--s-text-faint);
  }
}

/* The collaborator's sharing surfaces (PRD 023): the card on Inicio, the
   /compartir page, the QR tile and its full-screen dialog. Breakpoints are
   the shell's own - 860 (sidebar becomes a drawer) and 620 (the top-bar
   pill loses its label). */

@layer components {
  /* ── The shared controls partial ───────────────────────────────────── */
  .share-controls {
    display: flex;
    flex-direction: column;
    gap: var(--p-space-12);
    min-inline-size: 0;
  }

  .share-link {
    display: flex;
    align-items: center;
    gap: var(--p-space-8);
    padding: var(--p-space-10) var(--p-space-14);
    background: var(--s-surface-sunken);
    border: 1px dashed var(--s-border-strong);
    border-radius: var(--s-radius-control);
    font-family: var(--s-font-mono);
    font-size: var(--p-size-13);
    letter-spacing: var(--p-track-mono);
    min-inline-size: 0;
  }

  .share-link svg { inline-size: 14px; block-size: 14px; flex: none; color: var(--s-text-soft); }

  /* A 60-character URL breaks instead of pushing the page wider - the
     lesson of PRD 017 #42. */
  .share-link__text { overflow-wrap: anywhere; min-inline-size: 0; }
  .share-link__origin { color: var(--s-text-muted); }
  .share-link__code { color: var(--s-primary); font-weight: var(--p-weight-medium); }

  /* Buttons wrap, never shrink: .btn is `flex: none` + nowrap. */
  .share-actions { display: flex; flex-wrap: wrap; gap: var(--p-space-8); }
  .share-actions .btn svg { inline-size: 15px; block-size: 15px; flex: none; }

  /* ── The card on Inicio (#8) ───────────────────────────────────────── */
  .share-card { margin-block-start: var(--p-space-14); }

  .share-card__body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--p-space-14) 28px;
    padding: var(--p-space-18);
    align-items: start;
  }

  .share-card__eyebrow {
    font-size: var(--s-text-eyebrow);
    font-weight: var(--p-weight-semibold);
    letter-spacing: var(--p-track-group);
    text-transform: uppercase;
    color: var(--s-text-faint);
    margin-block-end: var(--p-space-4);
  }

  .share-card__lead {
    color: var(--s-text-muted);
    font-size: var(--s-text-row);
    margin: 0 0 var(--p-space-12);
    max-inline-size: 60ch;
  }

  .share-card__more {
    font-size: var(--p-size-13);
    font-weight: var(--p-weight-semibold);
    display: inline-flex;
    align-items: center;
    gap: var(--p-space-4);
    margin-block-start: var(--p-space-12);
  }

  .share-card__more svg { inline-size: 14px; block-size: 14px; }

  .share-card__qr { display: flex; flex-direction: column; align-items: center; gap: var(--p-space-6); }
  .share-card__qr .qr-tile { inline-size: 176px; }

  .qr-caption {
    font-size: var(--s-text-micro);
    color: var(--s-text-soft);
    text-align: center;
    margin: 0;
  }

  /* ── The QR tile ────────────────────────────────────────────────────── */
  /* Two literals on purpose (PRD 023 #16): a QR inverted for dark mode
     fails a real share of scanners and prints wrong, so the tile is white
     with dark modules in both themes. The drawer scrim in app-shell.css is
     the other deliberate non-token colour. The SVG paints with
     currentColor, so these two declarations are the whole colour story. */
  .qr-tile {
    background: #FFFFFF;
    color: #1B1725;
    padding: var(--p-space-10);
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-control);
    inline-size: min(100%, 240px);
    aspect-ratio: 1;
    display: block;
    cursor: zoom-in;
    transition: box-shadow 0.12s ease;
  }

  .qr-tile:hover { box-shadow: var(--s-shadow-raised); }
  .qr-tile[hidden] { display: none; }
  .qr-tile svg { display: block; inline-size: 100%; block-size: 100%; }

  .qr-tile__nojs {
    font-size: var(--s-text-meta);
    color: var(--s-text-soft);
    text-align: center;
    margin: 0;
  }

  .share-qr {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--p-space-10);
    inline-size: 100%;
  }

  /* ── /compartir (#9) ────────────────────────────────────────────────── */
  .share-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 280px;
    gap: var(--p-space-14) var(--p-space-26);
    align-items: start;
  }

  .share-layout__controls { grid-column: 1; }
  .share-layout__aside { grid-column: 2; grid-row: 1 / span 3; }
  .share-layout__steps { grid-column: 1; }
  .share-layout__note { grid-column: 1; }

  .share-layout__steps h2 { margin-block: var(--p-space-8) 0; }

  .share-steps {
    list-style: none;
    padding: 0;
    margin: var(--p-space-6) 0 0;
    counter-reset: step;
    display: flex;
    flex-direction: column;
    gap: var(--p-space-12);
  }

  .share-steps li {
    counter-increment: step;
    display: grid;
    grid-template-columns: 24px 1fr;
    gap: var(--p-space-12);
    align-items: start;
    font-size: var(--s-text-row);
    color: var(--s-text-muted);
    max-inline-size: 66ch;
  }

  .share-steps li::before {
    content: counter(step);
    inline-size: 24px;
    block-size: 24px;
    border-radius: var(--s-radius-avatar);
    background: var(--s-surface);
    border: 1px solid var(--s-border-strong);
    color: var(--s-text);
    font-size: var(--s-text-meta);
    font-weight: var(--p-weight-semibold);
    display: grid;
    place-items: center;
    margin-block-start: var(--p-space-1);
  }

  .share-steps b { color: var(--s-text); font-weight: var(--p-weight-semibold); }

  /* The one dashed card in the app, which is the point: it reads as an
     aside, not as content. */
  .share-note { border-style: dashed; box-shadow: none; }
  .share-note .card__body {
    font-size: var(--s-text-meta);
    color: var(--s-text-muted);
    line-height: 1.6;
    max-inline-size: 66ch;
  }
  .share-note b { color: var(--s-text); }

  .share-qr-card .card__body {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--p-space-10);
    padding: var(--p-space-18);
  }
  .share-qr-card .qr-caption { max-inline-size: 26ch; }

  /* ── The full-screen view (#18) ─────────────────────────────────────── */
  /* A native <dialog>. The UA gives it a max size, a border and padding;
     all four are reset so it is the whole screen. Literal white and ink
     rather than surface tokens: the surface token is dark in dark mode and
     `::backdrop` inherits custom properties only in recent browsers (PRD
     023 #48). With zero padding, a click whose target is the dialog itself
     can only be the backdrop. */
  .qr-dialog {
    inline-size: 100dvw;
    block-size: 100dvh;
    max-inline-size: none;
    max-block-size: none;
    border: 0;
    padding: var(--p-space-26);
    margin: 0;
    background: #FFFFFF;
    color: #1B1725;
  }

  .qr-dialog::backdrop { background: #FFFFFF; }

  .qr-dialog__inner {
    block-size: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--p-space-14);
  }

  .qr-dialog__tile { inline-size: min(88vw, 60dvh); aspect-ratio: 1; }
  .qr-dialog__tile svg { display: block; inline-size: 100%; block-size: 100%; }

  .qr-dialog__code {
    font-family: var(--s-font-mono);
    font-size: var(--p-size-13);
    letter-spacing: var(--p-track-mono);
    color: #57506B;
    text-align: center;
    overflow-wrap: anywhere;
    max-inline-size: 90%;
    margin: 0;
  }

  .qr-dialog__code b { color: #6A1FE0; font-weight: var(--p-weight-medium); }

  .qr-dialog__hint { font-size: var(--s-text-meta); color: #8B839E; text-align: center; margin: 0; }

  /* The dialog is white whatever the theme, so its button is too. */
  .qr-dialog .btn {
    --btn-bg: #FFFFFF;
    --btn-fg: #1B1725;
    --btn-border: #D5CDC1;
    --btn-bg-hover: #F6F3ED;
  }

  /* ── Folding ────────────────────────────────────────────────────────── */
  @media (max-width: 860px) {
    .share-layout { grid-template-columns: minmax(0, 1fr); }
    .share-layout__controls,
    .share-layout__aside,
    .share-layout__steps,
    .share-layout__note { grid-column: 1; }
    .share-layout__aside { grid-row: 2; }
    .share-layout__steps { grid-row: 3; }
    .share-layout__note { grid-row: 4; }
  }

  @media (max-width: 620px) {
    .share-card__body { grid-template-columns: minmax(0, 1fr); }
    .share-card__qr .qr-tile { inline-size: min(100%, 220px); }
    .share-qr-card .qr-tile { inline-size: min(100%, 280px); }
  }
}

/* Data tables in a card frame, per the design handoff: sunken header row,
   1px separators, hover wash. Row density rides the --s-row-pad token. */

@layer components {
  .tablewrap {
    overflow-x: auto;
    /* The scroller has to be a containing block, not just a clipper. A
       `.u-visually-hidden` label sits in the last header cell of the
       opportunities table, and `position: absolute` with nothing positioned
       above it resolves against the *initial* containing block - so the
       invisible 1px box landed at document x=791 instead of inside the
       scroller, and the phone layout scrolled sideways into 400px of empty
       background. Clipping alone does not stop that; only owning the
       coordinate space does. */
    position: relative;
    background: var(--s-surface);
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-card);
    box-shadow: var(--s-shadow-raised);
  }

  .tablewrap table {
    border-collapse: collapse;
    inline-size: 100%;
    min-inline-size: 700px;
    font-size: var(--s-text-row);
  }

  .tablewrap th {
    padding: 9px var(--p-space-14);
    text-align: start;
    white-space: nowrap;
    background: var(--s-surface-sunken);
    border-block-end: 1px solid var(--s-border);
    font-size: var(--s-text-eyebrow);
    font-weight: var(--p-weight-semibold);
    letter-spacing: var(--p-track-eyebrow);
    text-transform: uppercase;
    color: var(--s-text-soft);
  }

  .tablewrap td {
    padding: var(--s-row-pad) var(--p-space-14);
    border-block-end: 1px solid var(--s-border-subtle);
    vertical-align: top;
  }

  .tablewrap tbody tr:last-child td { border-block-end: none; }
  .tablewrap tbody tr:hover { background: var(--s-surface-sunken); }

  .rowlink {
    border: none;
    background: none;
    padding: 0;
    text-align: start;
    color: var(--s-primary);
    font-weight: var(--p-weight-medium);
    font-size: var(--s-text-row);
    text-decoration: none;
  }
  .rowlink:hover { text-decoration: underline; }

  .table-empty {
    padding: 48px var(--p-space-20);
    text-align: center;
    color: var(--s-text-faint);
    font-size: var(--s-text-row);
  }

  /* The sentinel row at the end of a paginated list: it fetches the next
     page as it scrolls into view and swaps itself out, so it only ever
     shows for a moment. Quieter than an empty-state message on purpose. */
  .table-more .table-empty {
    padding: var(--p-space-16) var(--p-space-20);
    font-size: var(--p-size-12);
  }

  .table-actions {
    display: flex;
    gap: var(--p-space-6);
    justify-content: flex-end;
  }
  .table-actions form { display: contents; }

  .list-attribution { margin-block-start: var(--p-space-10); font-size: var(--s-text-micro); }
  .list-attribution a { color: inherit; }
}

/* List toolbars: search + filters above a table card. */

@layer components {
  .toolbar {
    display: flex;
    gap: var(--p-space-8);
    align-items: center;
    flex-wrap: wrap;
    margin-block-end: var(--p-space-14);
  }

  /* Both filters, not just the search box. A `select` inherits
     `inline-size: 100%` from `base/elements.css`, so as a flex item it took
     the whole remaining row and wrapped onto its own line - the status filter
     spanned the page while the search box beside it was 240px, at a different
     height. They are a pair and have to look like one. */
  .toolbar input[type="search"],
  .toolbar select {
    max-inline-size: 240px;
    block-size: var(--p-control-32);
  }

  .toolbar__check {
    display: flex;
    align-items: center;
    gap: var(--p-space-8);
    font-size: var(--s-text-row);
    color: var(--s-text-muted);
  }
}

/* Typeahead dropdown - shared by the IAE selector now and the address
   typeahead in phase 4, so both fields behave and look identically. */

@layer components {
  .ta { position: relative; }

  .ta-list {
    position: absolute;
    z-index: 20;
    inset-inline: 0;
    top: calc(100% + 3px);
    max-block-size: 330px;
    overflow-y: auto;
    background: var(--s-surface);
    border: 1px solid var(--s-border-strong);
    border-radius: var(--s-radius-control);
    box-shadow: var(--s-shadow-popover);
  }

  .ta-item {
    padding: var(--p-space-8) 11px;
    border-block-end: 1px solid var(--s-border-subtle);
    font-size: var(--s-text-row);
    cursor: pointer;
  }
  .ta-item:last-child { border-block-end: none; }
  .ta-item:hover,
  .ta-item.is-active { background: var(--s-primary-tint); }

  .ta-item mark {
    background: var(--s-warning-bg);
    color: inherit;
    font-weight: var(--p-weight-semibold);
    border-radius: 1px;
    padding: 0 1px;
  }

  .ta-code {
    font-family: var(--s-font-mono);
    font-weight: var(--p-weight-semibold);
    color: var(--s-primary);
    letter-spacing: var(--p-track-code);
    margin-inline-end: 7px;
  }

  .ta-badge {
    font-size: var(--p-size-10);
    font-weight: var(--p-weight-semibold);
    letter-spacing: var(--p-track-eyebrow);
    text-transform: uppercase;
    background: var(--s-neutral-bg);
    color: var(--s-text-muted);
    border-radius: var(--s-radius-badge);
    padding: 1px 5px;
    margin-inline-end: 7px;
  }

  .ta-via {
    margin-block-start: 2px;
    padding-inline-start: 7px;
    border-inline-start: 2px solid var(--s-border-strong);
    font-size: var(--s-text-meta);
    color: var(--s-text-faint);
  }

  .ta-foot {
    padding: var(--p-space-6) 11px;
    background: var(--s-surface-sunken);
    border-block-start: 1px solid var(--s-border-subtle);
    font-size: var(--p-size-11);
    color: var(--s-text-faint);
  }
}


/* The public submission form: its own page shell, no app chrome.

   Reuses every component the backoffice already has - card, field, block,
   docgrid, formfoot - so a restyle of those reaches this page for free. What
   lives here is only what is genuinely particular to a page with no sidebar
   and no logged-in user. */

@layer pages {
  .publicpage {
    min-block-size: 100dvh;
    background: var(--s-surface-sunken);
  }

  .publicpage__head {
    display: flex;
    align-items: center;
    padding: var(--s-space-md) var(--s-space-lg);
    background: var(--s-surface);
    border-block-end: 1px solid var(--s-border);
  }

  .publicpage__body {
    max-inline-size: 46rem;
    margin-inline: auto;
    padding: var(--s-space-lg) var(--s-space-md) var(--s-space-xl);
  }

  .publicpage--narrow .publicpage__body {
    max-inline-size: 32rem;
  }

  /* The honeypot. Off-screen rather than `display: none` or
     `visibility: hidden`: both of those are the first thing a form-filling
     bot checks for, and moving the field out of the viewport costs it a
     little more work to notice. The template also carries tabindex="-1", so
     someone browsing without this stylesheet is not trapped by it either. */
  .hp {
    position: absolute;
    inset-inline-start: -9999px;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
  }
}

/* The client portal, inside the application shell since PRD 024. What is
   here is the read-only data list, the home's signing notice, the contracts
   rows, and the document grid's settled state. Everything else the portal
   renders is a shell or card component styled in its own file. */

@layer pages {
  .portal__stack {
    display: grid;
    gap: var(--p-space-14);
    max-inline-size: 760px;
  }

  .portal__owner {
    font-weight: var(--p-weight-semibold);
    margin-block-end: var(--s-space-2xs);
  }

  .portal__data {
    display: grid;
    gap: var(--s-space-sm);
    margin: 0;
  }

  /* The home's compact card: the four facts side by side where there is
     room, stacked on a phone. */
  .portal__data--compact {
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  }

  .portal__data dt {
    font-size: var(--s-text-micro);
    color: var(--s-text-faint);
    margin-block-end: var(--s-space-2xs);
  }

  .portal__data dd {
    margin: 0;
    color: var(--s-text);
  }

  .portal__readonly-note {
    margin-block-start: var(--s-space-md);
    font-size: var(--s-text-micro);
  }

  .portal__more {
    display: inline-flex;
    align-items: center;
    gap: var(--p-space-4);
    margin-block-start: var(--s-space-md);
    font-size: var(--s-text-row);
    font-weight: var(--p-weight-semibold);
  }
  .portal__more svg { inline-size: 14px; block-size: 14px; }

  .portal__empty,
  .portal__note {
    font-size: var(--s-text-meta);
  }
  .portal__empty { color: var(--s-text-soft); }

  .portal__doclist {
    margin: 0 0 var(--s-space-md);
    padding-inline-start: var(--p-space-14);
    color: var(--s-text-muted);
  }

  /* The one alert on the home (PRD 024 #7): a warning-tinted card, built
     from the same tokens the warning badge and message use. */
  .portal-notice {
    background: var(--s-warning-bg);
    border-color: color-mix(in srgb, var(--s-warning) 45%, var(--s-border));
    box-shadow: none;
  }

  .portal-notice__body {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--p-space-14) var(--p-space-14);
  }

  .portal-notice__ic {
    display: grid;
    place-items: center;
    flex: none;
    inline-size: 34px;
    block-size: 34px;
    border-radius: 50%;
    background: color-mix(in srgb, var(--s-warning) 16%, transparent);
    color: var(--s-warning);
  }
  .portal-notice__ic svg { inline-size: 18px; block-size: 18px; }

  .portal-notice__text {
    flex: 1 1 260px;
    min-inline-size: 0;
  }

  .portal-notice__title {
    font-size: var(--s-text-card-title);
    font-weight: var(--p-weight-semibold);
    color: var(--s-text);
  }

  .portal-notice__lead {
    margin-block-start: var(--s-space-2xs);
    font-size: var(--s-text-row);
    color: var(--s-text-muted);
  }

  .portal-notice .btn--primary { margin-inline-start: auto; }

  /* A contracts row: title and date, the status badge, one action. */
  .portal-contract { flex-wrap: wrap; }

  .portal-contract__main {
    flex: 1 1 240px;
    min-inline-size: 0;
  }

  .portal-contract__title { font-weight: var(--p-weight-semibold); }

  .portal-contract__meta {
    font-size: var(--s-text-meta);
    color: var(--s-text-soft);
  }

  .portal-contract__action {
    margin-inline-start: auto;
    display: flex;
    gap: var(--p-space-8);
    flex-wrap: wrap;
  }

  /* A document that has already passed: shown, but not offered for
     replacement, so it should not compete for attention with the ones that
     still need something. */
  .doc--settled {
    opacity: 0.72;
  }

  @media (max-width: 860px) {
    .portal-notice .btn--primary {
      margin-inline-start: 0;
      inline-size: 100%;
    }
  }

  @media (max-width: 620px) {
    .portal-contract__action {
      margin-inline-start: 0;
      inline-size: 100%;
    }
    .portal-contract__action .btn { flex: 1; }
  }
}

/* The collaborator's send page (pages/signing/sent.html, PRD 024 #74): the
   lead, and two cards side by side - "por email" and "tú mismo" - that
   stack on a phone. The link box, the button row and the QR tile are the
   share components (components/share.css); what is here is the layout
   around them and the address box. */

@layer pages {
  .send-lead {
    color: var(--s-text-muted);
    font-size: var(--s-text-row);
    max-inline-size: 78ch;
    margin-block-end: var(--p-space-14);
  }
  .send-lead strong { color: var(--s-text); }

  .send-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--p-space-14);
    align-items: start;
    max-inline-size: 1040px;
  }

  .send-card__body {
    display: flex;
    flex-direction: column;
    gap: var(--p-space-12);
  }

  .send-card__eyebrow {
    font-size: var(--s-text-eyebrow);
    font-weight: var(--p-weight-semibold);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--s-text-faint);
    margin-block-end: var(--p-space-4);
  }

  .send-card__lead {
    color: var(--s-text-muted);
    font-size: var(--s-text-row);
    max-inline-size: 52ch;
  }

  /* The address the email goes to: shown, never edited here. */
  .send-to {
    display: flex;
    align-items: center;
    gap: var(--p-space-10);
    padding: var(--p-space-10) var(--p-space-14);
    background: var(--s-surface-sunken);
    border: 1px solid var(--s-border);
    border-radius: var(--s-radius-control);
  }
  .send-to svg { inline-size: 16px; block-size: 16px; color: var(--s-text-soft); flex: none; }
  .send-to__addr { font-family: var(--s-font-mono); font-size: var(--s-text-row); overflow-wrap: anywhere; }
  .send-to__who { font-size: var(--s-text-meta); color: var(--s-text-soft); }

  .send-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--p-space-8);
    align-items: center;
  }

  /* The QR beside its caption; the tile keeps the share component's
     literal white and ink (PRD 023 #16). */
  .send-qr {
    display: grid;
    grid-template-columns: 148px minmax(0, 1fr);
    gap: var(--p-space-14);
    align-items: center;
  }
  .send-qr .qr-tile { inline-size: 148px; }
  .send-qr .qr-caption { text-align: start; }

  @media (max-width: 860px) {
    .send-layout { grid-template-columns: minmax(0, 1fr); }
  }

  @media (max-width: 620px) {
    .send-qr {
      grid-template-columns: minmax(0, 1fr);
      justify-items: center;
    }
    .send-qr .qr-caption { text-align: center; }
    .send-actions .btn { flex: 1 1 auto; }
  }
}


/* Highest layer: single-purpose overrides that beat any component rule without
   !important. Keep this file short - a utility used once should have been a
   component rule. */

@layer utilities {
  /* htmx's indicator classes, self-hosted (includeIndicatorStyles is off
     because the injected <style> violates our CSP). */
  .htmx-indicator { opacity: 0; }
  .htmx-request .htmx-indicator,
  .htmx-request.htmx-indicator { opacity: 1; }

  .u-visually-hidden {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .u-mono { font-family: var(--s-font-mono); letter-spacing: var(--p-track-mono); }
  .u-nums { font-variant-numeric: tabular-nums; }
  .u-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .u-push-right { margin-inline-start: auto; }
  .u-text-soft { color: var(--s-text-soft); }
  .u-text-faint { color: var(--s-text-faint); }

  /* Both asked for by the opportunities table and defined nowhere, so a
     wrapping cell wrapped and an inline form broke the row it sat in. */
  .u-nowrap { white-space: nowrap; }
  .u-inline { display: inline; }
}

