/* public/css/portal.css
 * EZMeeting public portal design tokens + primitives.
 * Plain CSS (NOT processed by Tailwind). Loaded last so it wins on equal
 * specificity. Light + dark token sets; dark is driven by [data-theme="dark"]
 * (toggled by theme.js), matching the rest of the portal.
 */

@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/source-sans-3-400.woff2") format("woff2");
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/source-sans-3-600.woff2") format("woff2");
}

:root {
  --font-sans: "Source Sans 3", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* Neutrals — light */
  --surface: #ffffff;
  --surface-raised: #f7f8fa;
  --text: #1a1d21;
  --text-muted: #5b6470;
  --border: #e3e6ea;
  --border-strong: #cfd4da;

  /* Accent — restrained civic blue (overridable per-org via inline --accent) */
  --accent: #1e5aa8;
  --accent-contrast: #ffffff;
  --focus-ring: #1e5aa8;

  /* 8px spacing scale */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  --radius: 8px;
  --shadow-card: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.08);
}

:root[data-theme="dark"] {
  --surface: #14171c;
  --surface-raised: #1c2027;
  --text: #e7eaee;
  --text-muted: #a2abb6;
  --border: #2a2f37;
  --border-strong: #3a414b;

  --accent: #6ea8f0;          /* lighter tint so it passes contrast on dark */
  --accent-contrast: #0c1116;
  --focus-ring: #6ea8f0;
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4);
}

/* Base typography for portal pages only (scoped to .public-portal body). */
body.public-portal {
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.6;
  color: var(--text);
}
body.public-portal h1 { font-size: 2rem; font-weight: 600; line-height: 1.25; }
body.public-portal h2 { font-size: 1.375rem; font-weight: 600; line-height: 1.3; }
body.public-portal h3 { font-size: 1.125rem; font-weight: 600; line-height: 1.35; }

/* Visible focus for keyboard users across the portal. */
body.public-portal a:focus-visible,
body.public-portal button:focus-visible,
body.public-portal summary:focus-visible,
body.public-portal [tabindex]:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* Consistent icon sizing/color for Bootstrap Icons on the portal. */
body.public-portal .bi { color: currentColor; vertical-align: -0.125em; }

@media (prefers-reduced-motion: reduce) {
  body.public-portal *,
  body.public-portal *::before,
  body.public-portal *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

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

.portal-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 60vh;
  padding: var(--space-6) var(--space-4);
  color: var(--text);
}
.portal-empty .portal-empty__icon { font-size: 3rem; color: var(--text-muted); margin-bottom: var(--space-4); }
.portal-empty h1 { margin: 0 0 var(--space-2); }
.portal-empty p { color: var(--text-muted); max-width: 32rem; margin: 0 0 var(--space-5); }
.portal-empty .portal-empty__actions { display: flex; gap: var(--space-3); flex-wrap: wrap; justify-content: center; }

@media print {
  body.public-portal { color: #000; background: #fff; font-size: 12pt; }
  body.public-portal .public-header,
  body.public-portal .public-nav,
  body.public-portal .public-footer,
  body.public-portal .member-login-link,
  body.public-portal [data-theme-toggle] { display: none !important; }

  body.public-portal .public-content { padding: 0; }
  body.public-portal a { color: #000; text-decoration: underline; }

  /* Show the portal URL after external links so a printout is self-describing. */
  body.public-portal .public-content a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-size: 10pt; word-break: break-all;
  }

  /* Avoid splitting a motion/agenda item across pages where possible. */
  body.public-portal .motion-box,
  body.public-portal .agenda-item,
  body.public-portal .minutes-item { break-inside: avoid; page-break-inside: avoid; }

  body.public-portal h1, body.public-portal h2, body.public-portal h3 { break-after: avoid; }
}

/* Next-meeting hero (featured-meeting-card) */
body.public-portal .portal-hero {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  /* Elevation, NOT a left stripe. A 4px coloured left edge is Bootstrap's
     .alert grammar, and this is the next public meeting — the most important
     card on the page, not a caution. Task 7's --shadow-card carries the
     emphasis instead, and the full 1px border now closes on all four sides. */
  box-shadow: var(--shadow-card);
  padding: var(--space-5);
  margin-bottom: var(--space-6);
}
body.public-portal .portal-hero__eyebrow { color: var(--text-muted); font-size: 0.875rem; margin: 0 0 var(--space-1); }
body.public-portal .portal-hero__title { margin: 0 0 var(--space-2); font-size: 1.5rem; font-weight: 600; }
body.public-portal .portal-hero__meta { color: var(--text-muted); margin: 0 0 var(--space-1); font-size: 0.95rem; }
body.public-portal .portal-hero__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); margin-top: var(--space-4); }
body.public-portal .portal-hero__ics { font-size: 0.8rem; color: var(--text-muted); text-decoration: underline; }
body.public-portal .portal-btn {
  display: inline-flex; align-items: center; gap: var(--space-2);
  padding: var(--space-2) var(--space-4); border-radius: var(--radius-md);
  font-weight: 600; font-size: 0.9rem; text-decoration: none; border: 1px solid transparent; cursor: pointer;
}
body.public-portal .portal-btn--primary { background: var(--accent); color: var(--accent-contrast); }
body.public-portal .portal-btn--secondary { background: transparent; color: var(--accent); border-color: var(--border-strong); }
body.public-portal .portal-badge-draft {
  display: inline-block; margin-top: var(--space-2); padding: 2px var(--space-2);
  border-radius: var(--radius-sm); font-size: 0.75rem; background: #fef3c7; border: 1px solid #f59e0b; color: #8a3902;
}

/* Portal home components */
body.public-portal .portal-grid-2 { display: grid; grid-template-columns: 1fr; gap: var(--space-5); margin-bottom: var(--space-6); }
@media (min-width: 720px) { body.public-portal .portal-grid-2 { grid-template-columns: 1fr 1fr; } }
body.public-portal .portal-section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }
body.public-portal .portal-section__head { display: flex; justify-content: space-between; align-items: center; padding: var(--space-4); border-bottom: 1px solid var(--border); }
body.public-portal .portal-section__title { margin: 0; font-size: 1.125rem; font-weight: 600; }
body.public-portal .portal-list__row { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); padding: var(--space-4); text-decoration: none; color: var(--text); border-top: 1px solid var(--border); }
body.public-portal .portal-list__row:first-child { border-top: 0; }
body.public-portal .portal-list__row:hover { background: var(--surface-raised); }
body.public-portal .portal-list__meta { color: var(--text-muted); font-size: 0.875rem; }
body.public-portal .portal-stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-5); text-align: center; }
body.public-portal .portal-stat__num { font-size: 2rem; font-weight: 600; }
body.public-portal .portal-stat__label { color: var(--text-muted); font-size: 0.875rem; }
body.public-portal .portal-badge { display: inline-block; margin-top: var(--space-1); padding: 1px var(--space-2); border-radius: var(--radius-sm); font-size: 0.7rem; font-weight: 600; }
body.public-portal .portal-badge--approved { background: #dcfce7; color: #166534; }
body.public-portal .portal-badge--draft { background: #fef3c7; color: #8a3902; }
body.public-portal .portal-empty-inline { padding: var(--space-6); text-align: center; color: var(--text-muted); }
body.public-portal .portal-chip { display: inline-flex; align-items: center; gap: var(--space-1); padding: var(--space-1) var(--space-3); border: 1px solid var(--border-strong); border-radius: var(--radius-pill); text-decoration: none; color: var(--text); font-size: 0.875rem; }
body.public-portal .portal-chip:hover { background: var(--surface-raised); }

/* Agenda body — portal typographic treatment (scoped; logged-in preview unaffected).
   Scoped to the real classes present in views/partials/agenda-preview-content.ejs:
   .agenda-item and .item-number exist there; .item-title / .section-title /
   .agenda-section-title do not (sections use a plain <h2>AGENDA</h2>, and the item
   title itself is an unclassed <strong>), so those rules are omitted. .legal-notice
   exists; .legal-text does not, so only .legal-notice is scoped. */
body.public-portal .agenda-item { padding: var(--space-3) 0; border-top: 1px solid var(--border); }
body.public-portal .item-number { font-weight: 600; }
body.public-portal .legal-notice { color: var(--text-muted); font-size: 0.9rem; }

/* Minutes vote-result tables (portal only) */
body.public-portal .portal-votes { width: 100%; border-collapse: collapse; margin: var(--space-3) 0; font-size: 0.9rem; }
body.public-portal .portal-votes th, body.public-portal .portal-votes td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); vertical-align: top; }
body.public-portal .portal-votes thead th { color: var(--text-muted); font-weight: 600; border-bottom: 2px solid var(--border-strong); }
body.public-portal .portal-votes td:nth-child(2) { text-align: right; white-space: nowrap; }

/* Draft agenda watermark (portal only) */
body.public-portal .agenda-draft-watermark { position: relative; overflow: hidden; }
body.public-portal .agenda-draft-watermark::before {
  content: "DRAFT"; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-sans); font-weight: 700; letter-spacing: 0.1em;
  font-size: 22vw; color: var(--text); opacity: 0.05; transform: rotate(-28deg);
}
body.public-portal .agenda-draft-watermark > * { position: relative; z-index: 1; }
@media print { body.public-portal .agenda-draft-watermark::before { opacity: 0.08; } }

/* Committee page (Phase 3 token restyle) */
body.public-portal .portal-committee { max-width: 64rem; margin: 0 auto; }
body.public-portal .portal-committee__header { margin: 0 0 var(--space-5); }
body.public-portal .portal-committee__header h1 { margin: 0; }
body.public-portal .portal-breadcrumb { color: var(--text-muted); font-size: 0.875rem; margin: 0 0 var(--space-4); }
body.public-portal .portal-breadcrumb a { color: var(--accent); text-decoration: none; }
body.public-portal .portal-breadcrumb a:hover { text-decoration: underline; }
body.public-portal .portal-section--pad { padding: var(--space-5); margin-bottom: var(--space-6); }
body.public-portal .portal-notice { border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-6); font-size: 0.9rem; }
body.public-portal .portal-notice--warn { background: var(--state-inactive-bg); border: 1px solid var(--state-inactive-text); color: var(--state-inactive-text); }
body.public-portal .portal-badge--type { background: var(--surface-raised); border: 1px solid var(--border-strong); color: var(--accent); }
body.public-portal .portal-members { list-style: none; margin: 0; padding: 0; }
body.public-portal .portal-members__row { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) 0; border-top: 1px solid var(--border); }
body.public-portal .portal-members__row:first-child { border-top: 0; padding-top: 0; }
body.public-portal .portal-members__name { color: var(--text); }
body.public-portal .portal-members__title { color: var(--text-muted); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; }
body.public-portal .portal-email-reveal {
  background: none; border: 0; padding: 0; margin-top: var(--space-1);
  font: inherit; font-size: 0.75rem; color: var(--accent); text-decoration: underline; cursor: pointer;
}
body.public-portal .portal-email-link { color: var(--accent); font-size: 0.75rem; word-break: break-all; }
body.public-portal .portal-links { list-style: none; margin: 0; padding: 0; }
body.public-portal .portal-links li { padding: var(--space-2) 0; border-top: 1px solid var(--border); }
body.public-portal .portal-links li:first-child { border-top: 0; padding-top: 0; }
body.public-portal .portal-links a { color: var(--accent); text-decoration: none; }
body.public-portal .portal-links a:hover { text-decoration: underline; }
body.public-portal .portal-pagination { display: flex; align-items: center; justify-content: space-between; margin-top: var(--space-4); font-size: 0.875rem; }
body.public-portal .portal-pagination a { color: var(--accent); text-decoration: none; }
body.public-portal .portal-pagination a:hover { text-decoration: underline; }

/* Standardized aristocratic error page (404/500) */
body.public-portal .portal-error { max-width: 40rem; margin: var(--space-8) auto; padding: var(--space-6) var(--space-4); text-align: center;
  font-family: Georgia, "Iowan Old Style", "Palatino Linotype", "Times New Roman", serif; }
body.public-portal .portal-error__code { margin: 0; font-size: clamp(4rem, 14vw, 7rem); font-weight: 400; line-height: 1; color: var(--accent); letter-spacing: 0.04em; }
body.public-portal .portal-error__ornament { display: block; font-size: 1.4rem; color: var(--text-muted); margin: var(--space-2) 0 var(--space-4); }
body.public-portal .portal-error__heading { margin: 0 0 var(--space-3); font-size: 1.6rem; font-weight: 400; font-style: italic; color: var(--text); }
body.public-portal .portal-error__message { margin: 0 auto var(--space-5); max-width: 34rem; font-family: var(--font-sans); color: var(--text-muted); line-height: 1.6; }

/* --- Public document reader (B7) ---------------------------------------- */
/* `pre-wrap` because section text is plain text extracted from DOCX: its
   paragraph breaks are real newlines, and escaping is what keeps a stray
   angle bracket in a bylaw from becoming markup. */
.portal-document__text { white-space: pre-wrap; }
.portal-document__text--empty { color: var(--text-muted); font-style: italic; }
.portal-document__cite { color: var(--text-muted); font-weight: 400; }
.portal-document__toc ol { list-style: none; padding-left: 0; }
.portal-document__toc-item--d1 { padding-left: 1rem; }
.portal-document__toc-item--d2 { padding-left: 2rem; }
.portal-document__toc-item--d3 { padding-left: 3rem; }
.portal-document__toc-item--d4 { padding-left: 4rem; }
.portal-document__toc-item--d5 { padding-left: 5rem; }
/* 2.5.8 Target Size: TOC entries sit in a dense list, so give them a floor. */
.portal-document__toc a { display: inline-block; min-height: 24px; padding: 2px 0; }
.portal-document__section { margin-bottom: 1.5rem; }

/* --- Target size floors on the small portal links (B8) -------------------
   WCAG 2.5.8 Target Size (Minimum), 24x24 CSS px. The compiled style.css puts a
   44px floor on .portal-btn and friends but deliberately opts plain links out,
   so these small standalone links carry their own floor. Measured before the
   fix in headless Chromium at a 1024px viewport:

     .portal-hero__ics       ("Other (.ics)")     71.7 x 20.5
     .portal-pagination a    ("Next ->")          48.8 x 22.4
     a.portal-list__meta     ("Subscribe")        68.8 x 22.4
     a.portal-list__meta     (".ics")             23.3 x 22.4   <- also too narrow
     .portal-email-link / .portal-email-reveal    dense members list

   `inline-block` rather than `inline-flex` on purpose: these links are
   underlined, and an underline on a flex container does not reliably paint over
   its anonymous text item. `padding-block: 2px` splits the extra height evenly
   so the label stays optically centred in the taller box, which an
   `inline-block` with only `min-height` would not do (the text would hug the
   top). Every one of these sits alone in a flex row or a list row whose
   neighbours are >=24px away centre-to-centre, so there is no target-crowding
   to resolve alongside the size floor. */
body.public-portal .portal-hero__ics,
body.public-portal .portal-pagination a,
body.public-portal a.portal-list__meta,
body.public-portal .portal-email-link,
body.public-portal .portal-email-reveal {
  display: inline-block;
  min-height: 24px;
  min-width: 24px;
  padding-block: 2px;
}

/* --- Legacy token overrides, portal scope only (B5, WCAG 1.4.3) ----------
   HISTORICAL PREMISE -- SUPERSEDED. The two figures below are no longer live.
   They are kept because the override they justify is still here and would
   otherwise read as unmotivated.

   When this was written, themes.css declared --text-secondary #6c757d and
   --text-muted #adb5bd, and views/public/ used both inline and via themes.css's
   own `.text-muted { color: var(--text-muted) !important }` utility. Measured
   then with src/utils/embed-contrast.js:

     --text-muted   #adb5bd on #ffffff  2.0748:1   FAIL
     --text-secondary #6c757d on #f8f9fa (.public-nav background)  4.4486:1  FAIL

   NEITHER HEX EXISTS IN themes.css ANY MORE -- grep returns zero of each -- and
   the citation this replaces pointed at themes.css:18-19, which is now prose
   about the shared vocabulary rather than a declaration. Visual Overhaul
   Phase 1 replaced the palette with A3: --text-muted is #554e42 at
   themes.css:33 (light) and #b7ab97 at :377 (dark), with --text-secondary
   aliased onto it at :222 and :416. Re-measured on the same two surfaces:

     --text-muted     #554e42 on #ffffff  8.2202:1  passes the 7:1 bodyText bar
     --text-secondary #554e42 on #f8f9fa  7.7982:1  passes

   So the defect this block exists to fix is gone at the source, and the
   "--text-secondary is the live fix" sentence further down is stale the same
   way: the override is now belt-and-braces for BOTH names.

   THE DECLARATIONS ARE LEFT EXACTLY AS THEY ARE. portal.css's values (#5b6470 /
   #545c64) are the portal's own, not a repair of themes.css, and removing them
   would move pixels on a public surface for no accessibility gain. Retiring
   them is a portal decision, not a line-number one.

   Deliberately NOT fixed in themes.css: `text-muted` appears 147 times in
   class="" context across 33 .ejs views, that token is owned by
   feat/visual-overhaul Phase 1 (spec 6.1), and the global fix belongs to
   sub-project E. Scoped to body.public-portal, which portal.css (loaded last,
   layout-public.ejs:28) can override without touching the shared sheet.

   These are the minimum that passes today, NOT a design choice —
   feat/visual-overhaul owns the final palette and raises body text to AAA.

   --text-muted is belt-and-braces: portal.css's own :root already re-declares
   it as #5b6470 and wins over themes.css on load order, so this changes no
   pixel today; it pins the portal's value against a future higher-specificity
   declaration in themes.css. --text-secondary is the live fix.

   DARK THEME COMPANION IS LOAD-BEARING. Custom properties inherit, so a bare
   `body.public-portal { --text-muted: ... }` sets the token ON BODY and beats
   the value inherited from :root[data-theme="dark"] — it would push the light
   greys into dark mode. The dark rule below restates the existing dark values
   (portal.css's own --text-muted, themes.css's --text-secondary, both already
   AA on the dark surfaces: 6.08:1 and 7.14:1) so dark renders unchanged. */
body.public-portal {
  --text-muted: #5b6470;      /* 5.9971:1 on #ffffff, matches portal.css --text-muted */
  --text-secondary: #545c64;  /* 6.7921:1 on #ffffff, 6.4434:1 on #f8f9fa */
}
:root[data-theme="dark"] body.public-portal {
  --text-muted: #a2abb6;      /* unchanged: portal.css's dark --text-muted */
  --text-secondary: #9ea7b8;  /* unchanged: themes.css's dark --text-secondary */
}

/* --- Focus appearance on the active nav pill (B9, WCAG 2.4.13 / 1.4.11) ---
   .public-nav .nav-link.active is filled with --primary-color (#2563eb via
   style.css) and the portal's focus outline is --focus-ring (#1e5aa8): those
   two measure 1.3178:1 against each other. The generic rule's
   `outline-offset: 2px` does currently save it — the outline lands on the nav
   background (#1e5aa8 on #f8f9fa = 6.4617:1), so the indicator is not actually
   adjacent to the pill — but the whole indicator then hangs on an offset that
   reads as cosmetic and a per-org --focus-ring can move too.

   The white inner ring makes the indicator contrast against BOTH sides
   explicitly: #ffffff on the #2563eb pill is 5.1686:1 and #ffffff against the
   #1e5aa8 outline needs nothing (they are two halves of one indicator; only
   its outer boundaries have to carry contrast). In dark theme the ring is
   white against a dark pill and a #6ea8f0 outline against a dark nav — both
   boundaries stay well clear of 3:1. */
body.public-portal .nav-link:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  /* DELIBERATELY NOT var(--focus-ring-shadow). This band is opaque white on
   * purpose, and opaque white is theme-INVARIANT: it clears the active nav
   * pill at 5.17:1 in BOTH themes. --surface-raised is theme-varying, so
   * routing this to the shared ring paints the band in the portal's raised
   * surface -- 4.86:1 in light, fine, but 3.16:1 in dark, a 5.4 per cent
   * margin over the 1.4.11 bar where there is currently a 72 per cent one.
   *
   * ADA owns this rule and guards it in tests/unit/portal/portal-contrast.test.js
   * ("active nav pill focus indicator (B9)"), whose assertion matches the
   * literal and therefore covers both themes with one check. Tokenising it
   * requires ADA's sign-off and a dark-theme row in that gate, not a rewrite
   * of the gate to follow the token. Task 9 tried the rewrite; it silently
   * dropped dark-theme coverage and was reverted.
   *
   * Enumerated as a documented survivor in primitives.test.js, so the
   * conformance scan sees it rather than being weakened for it.
   */
  box-shadow: 0 0 0 2px #ffffff;
}

/* --- Accessibility statement + barrier-report form (D1) ------------------- */

body.public-portal .portal-statement { max-width: 44rem; }
body.public-portal .portal-statement h3 { margin-top: var(--space-6); }
body.public-portal .portal-statement p,
body.public-portal .portal-statement li { line-height: 1.6; }

/* Honeypot. Off-screen rather than display:none, and deliberately so: bots
   that read computed style detect and skip a display:none trap, which defeats
   the only reason the field exists. Nothing here is what keeps assistive
   technology out — aria-hidden="true" on the wrapper removes it from the
   accessibility tree and tabindex="-1" removes it from the tab order, both in
   the markup (views/public/accessibility.ejs), so the trap stays invisible to
   a screen-reader user even with this stylesheet unloaded. Do not "simplify"
   this to display:none, and do not drop the aria-hidden/tabindex pair. */
.portal-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

body.public-portal .portal-form > div { margin-bottom: var(--space-4); }
body.public-portal .portal-form label { display: block; font-weight: 600; margin-bottom: var(--space-1); }
body.public-portal .portal-form input,
body.public-portal .portal-form textarea {
  width: 100%;
  max-width: 40rem;
  /* 24px floor is SC 2.5.8 Target Size (Minimum); padding takes the real
     control well past it, but the floor holds if the padding is ever tuned. */
  min-height: 24px;
  padding: var(--space-2);
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}
body.public-portal .portal-form__help { font-size: 0.875rem; color: var(--text-muted); margin-top: var(--space-1); }

/* SC 3.3.1: the error is TEXT and is announced (role="alert" in the markup).
   Colour is a second channel, never the only one — an empty message renders
   nothing at all, so a red border can never be the sole indication. */
body.public-portal .portal-form__error:empty { display: none; }

/* The role="status" region is NEVER display:none, even when empty. A live
   region that is absent from the accessibility tree until content is inserted
   in the same tick is announced unreliably, and often not at all for a polite
   status — and that message is the only feedback a screen-reader user gets on
   the JS path. It is emptied of its BOX (border, padding, background) instead,
   so it is visually absent while staying in the tree the whole time. Do not
   "tidy" this into the :empty rule above. */
body.public-portal .portal-form__status:empty {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
}
body.public-portal .portal-form__error {
  margin-top: var(--space-1);
  font-size: 0.9375rem;
  font-weight: 600;
  /* #b3261e: 5.9247:1 on #ffffff. */
  color: #b3261e;
}
:root[data-theme="dark"] body.public-portal .portal-form__error {
  /* #f2b8b5: 9.5747:1 on #14171c. */
  color: #f2b8b5;
}
body.public-portal .portal-form input[aria-invalid="true"],
body.public-portal .portal-form textarea[aria-invalid="true"] { border-color: currentColor; border-width: 2px; }
/* The script moves focus here on success, so the landing point has to be
   visible to a sighted keyboard user. :focus, not :focus-visible — the focus
   is programmatic, and browsers do not reliably apply :focus-visible to a
   tabindex="-1" element focused from script. */
body.public-portal .portal-form__status:focus {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
body.public-portal .portal-form__status {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
  color: var(--text);
}
