/*!
 * BBN Member Portal — SHARED COMPONENTS. bbn-portal.
 * Release Gate 1 (Structural + Visual Product Design), 2026-09-14.
 *
 * Depends on tokens.css. Every value here comes from a --bbn-* token; if you find yourself
 * typing a hex in this file, the token is missing — add it to tokens.css instead.
 *
 * ===== WHAT THIS IS FOR ====================================================================
 * The portal is WordPress pages + Elementor remnants + ~26 independent JS apps that each
 * injected their own <style> block. They drifted because there was nothing shared to reach
 * for. These classes are that shared thing.
 *
 * This file does NOT restyle existing markup by element selector. It only defines .bbn-*
 * classes that a module opts into. Nothing changes appearance until a module adopts a class,
 * which keeps the blast radius of this gate controllable and reversible.
 *
 * ===== THE FOCUS RULE ======================================================================
 * :focus-visible is defined ONCE, here, for every .bbn-* interactive class. Modules must not
 * ship their own ring. Four separate gold rings (2.17-2.67:1) existed before this file and
 * every one of them failed SC 1.4.11.
 */

/* ============================================================================================
 * FOCUS — one definition, applied to everything interactive in the system.
 * ==========================================================================================*/
/* ===== WHY EVERY SELECTOR BELOW IS DOUBLED ==================================================
 * The active theme (Go, style-shared.min.css) styles form controls by ELEMENT + ATTRIBUTE:
 * `input[type="search"]` is specificity (0,1,1), which beats a single class (0,1,0) no matter
 * which stylesheet loads last. Measured on /funding-opportunities/: the search field kept the
 * theme's 2px #05222E border and 4px radius, and the theme's `input[type="search"]:focus`
 * (0,2,1) suppressed this file's focus ring entirely.
 *
 * Doubling the class — `.bbn-input.bbn-input` (0,2,0) — clears element+attribute selectors
 * without resorting to !important, which would in turn be unoverridable by a module that
 * legitimately needs to differ. This is the same technique already proven in this codebase by
 * `body.bbn-fund-list.bbn-fund-list` (funding-list.css) and `body #bbn-db2` (dashboard.css).
 *
 * NOTE: CSS cascade layers would make this WORSE, not better — unlayered theme styles beat
 * layered ones regardless of specificity. Do not "modernise" this into @layer.
 * ==========================================================================================*/
.bbn-btn.bbn-btn:focus-visible,
.bbn-btn-icon.bbn-btn-icon:focus-visible,
.bbn-input.bbn-input:focus,
.bbn-select.bbn-select:focus,
.bbn-textarea.bbn-textarea:focus,
/* TEXT-ENTRY CONTROLS USE :focus, NOT :focus-visible — deliberately.
 * :focus-visible only matches when the browser heuristically judges focus to be keyboard-
 * driven, so clicking into a search box with a mouse would show no indicator at all. For a
 * field you are about to type into, the caret's location must always be visible. Buttons and
 * links keep :focus-visible, where a ring on every mouse click would be noise. */
.bbn-search input:focus,
.bbn-searchinput.bbn-searchinput:focus,
.bbn-filter-select.bbn-filter-select:focus,
.bbn-chip.bbn-chip:focus-visible,
.bbn-seg.bbn-seg button:focus-visible,
.bbn-card a:focus-visible,
.bbn-link:focus-visible {
  outline: 2px solid var(--bbn-focus-inner);
  outline-offset: 0;
  box-shadow: 0 0 0 5px var(--bbn-focus-ring);
  border-radius: var(--bbn-radius-sm);
}

/* ============================================================================================
 * PORTAL LINK DEFAULT — a safe floor for UNSTYLED anchors only.
 *
 * THE PROBLEM IT SOLVES
 * Elementor's kit sets the global link colour to the decorative gold:
 *     .elementor-kit-8 a { color: var(--e-global-color-d00cfb2) }   -> #C6A664, 2.32:1 on white
 * Any portal anchor that is not explicitly coloured inherits that silently. It is how the skip
 * link and every dashboard link ended up unreadable while their own CSS asked for ink — the
 * authoring was correct and the cascade overruled it.
 *
 * THE PUBLIC SITE IS NOT TOUCHED. This file is enqueued only on portal routes, and the rule is
 * a portal floor, not a change to the kit. The global gold remains a separate public-site
 * design/accessibility question and is logged as backlog, not fixed here.
 *
 * WHY `a:not([class])` AND NOT A BROAD `a`
 * An anchor inventory across five routes found 12 distinct families — nav items, popup
 * triggers, the skip link, dashboard quick actions, card titles, buttons-as-anchors. 28 of them
 * are View Details triggers styled as buttons; recolouring those would be a regression, not a
 * fix. `:not([class])` targets ONLY anchors with no class at all, which is exactly the set at
 * risk of silent inheritance, and makes it structurally impossible to reach a styled component.
 *
 * SPECIFICITY, measured rather than assumed:
 *     .elementor-kit-8 a      (0,1,1)
 *     body a:not([class])     (0,1,2)   <- wins on type count, no !important needed
 *
 * Underline on hover so the state is never signalled by colour alone.
 * ==========================================================================================*/
body a:not([class]) {
  color: var(--bbn-link);
}
body a:not([class]):hover,
body a:not([class]):focus-visible {
  color: var(--bbn-link-hover);
  text-decoration: underline;
}
/* The two-tone ring must reach these too. Measured before this rule: an unstyled portal link
 * focused with a real Tab press painted `outline: 1px rgb(5,34,46)` — the THEME's ring, a single
 * dark hairline. That is invisible on any dark surface and is not the portal's focus treatment.
 * The .bbn-* focus block above cannot cover these by definition, because these anchors have no
 * class. (0,2,2) clears the theme rule without !important. */
body a:not([class]):focus-visible {
  outline: 2px solid var(--bbn-focus-inner);
  outline-offset: 0;
  box-shadow: 0 0 0 5px var(--bbn-focus-ring);
  border-radius: var(--bbn-radius-sm);
}

/* ===== DASHBOARD CONTENT LINKS (Browser QA P2-3) ===========================================
 * "Weak sub-1px dotted focus treatment on dashboard content links."
 *
 * THE GAP, not a missing rule but an unreachable one. The dashboard's links all carry classes
 * — .db2-row-t, .db2-more, .db2-step, .db2-up, .db2-t — so:
 *   - the .bbn-* focus block above cannot match them (they are not .bbn-* classes), and
 *   - `body a:not([class])` cannot match them BY DEFINITION, because they have a class.
 * They therefore fell through to the theme's own indicator: a single dark hairline, which is
 * what Browser measured. Neither rule was wrong; the dashboard simply sat between them.
 *
 * Scoped to the dashboard container rather than widened to `body a`, so this cannot reach the
 * 28 View Details triggers or any other component with its own deliberate ring.
 * `body #bbn-db2 a:focus-visible` is (1,1,2) — clears the theme without !important.
 *
 * Same two-tone ring as the rest of the system, so the dashboard does not become a fourth
 * focus treatment: the inner ring reads on the teal cards, the outer on the cream ground and
 * on white. Hover/underline cues are untouched — this adds an indicator, it does not replace
 * one, and the state is never signalled by colour alone.
 * ==========================================================================================*/
body #bbn-db2 a:focus-visible,
body #bbn-db2 button:focus-visible {
  outline: 2px solid var(--bbn-focus-inner);
  outline-offset: 0;
  box-shadow: 0 0 0 5px var(--bbn-focus-ring);
  border-radius: var(--bbn-radius-sm);
}

/* ===== FEATURED-LISTING EMPTY STATE (Browser QA, minor polish) =============================
 * The Featured Funding listing is an UnlimitedElements loop grid, and its "no posts" branch
 * emits one bare, unstyled div:
 *     <div class="ue-no-posts-found">No featured opportunities right now.</div>
 * Loose grey text directly on the page ground, where every neighbouring block is a bordered
 * card — which is why it read as dead space rather than as a state.
 *
 * Brought onto the same shape the portal's own empty states use (.bbn-empty): centred, on a
 * surface, with an icon. Deliberately NOT the error treatment — surface and border, not the
 * red notice styling, because "nothing is featured this week" is a normal condition, not a
 * failure. The icon is the same star used for the Featured heading, so it names the thing that
 * is empty. Masked with currentColor rather than shipped as a coloured asset, so it follows
 * the text colour and needs no second definition.
 *
 * Portal-only: this file is enqueued on portal routes, so the public site is untouched.
 * ==========================================================================================*/
/* SPECIFICITY, measured after the first attempt shipped HALF-APPLIED (Browser spot-check).
 * The widget carries its OWN empty-message styling as Elementor control defaults, compiled
 * into post-1963.css as:
 *     .elementor-1963 .elementor-element.elementor-element-7d5e2c7 .ue-no-posts-found
 *     { color:#000; background-color:#e8e8e8; border:1px solid #c4c4c4; padding:20px }
 * That is (0,4,0). A bare `.ue-no-posts-found` is (0,1,0), so the layout, icon and radius
 * below applied (the widget sets none of those) while background, border, colour and padding
 * did not — producing a grey box with a brand icon in it. The defaults are not stored in
 * _elementor_data, so there is nothing to clear at the widget; the design system has to win.
 *
 * Tripled class + body = (0,4,1), which clears it deterministically without !important and
 * without depending on stylesheet order. Same technique as the focus block above. Written
 * against `.elementor-element` rather than a page id so it covers every listing widget on
 * every portal route, including ones added later. */
body .elementor-element .ue-no-posts-found.ue-no-posts-found.ue-no-posts-found {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 8px;
  padding: 32px 20px;
  background: var(--bbn-surface);
  border: 1px solid var(--bbn-border);
  border-radius: var(--bbn-radius);
  color: var(--bbn-text-muted);
  font-size: var(--bbn-size-support);
  line-height: 1.55;
}
.ue-no-posts-found::before {
  content: "";
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
  background-color: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.8'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpolygon%20points='12,3%2015,9.5%2022,10.3%2017,15.2%2018.2,22%2012,18.7%205.8,22%207,15.2%202,10.3%209,9.5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='%23000'%20stroke-width='1.8'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpolygon%20points='12,3%2015,9.5%2022,10.3%2017,15.2%2018.2,22%2012,18.7%205.8,22%207,15.2%202,10.3%209,9.5'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* ============================================================================================
 * PAGE FRAME — page header, intro, section grouping.
 * Gives every route the same skeleton so they read as one product.
 * ==========================================================================================*/
.bbn-page {
  max-width: var(--bbn-page-max);
  margin-inline: auto;
  padding-inline: var(--bbn-pad-page-mobile);
  font-family: var(--bbn-font);
  color: var(--bbn-text);
}
.bbn-page--narrow { max-width: var(--bbn-page-max-narrow); }

@media (min-width: 768px) {
  .bbn-page { padding-inline: var(--bbn-pad-page); }
}

/* Page header: a restrained brand-tinted band, not a slab of dark brown. */
.bbn-pagehead {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 0 14px;
  border-bottom: 1px solid var(--bbn-border);
  margin-bottom: var(--bbn-gap-section);
}
.bbn-pagehead h1,
.bbn-pagehead .bbn-pagetitle {
  margin: 0;
  font-size: var(--bbn-size-page-title);
  font-weight: var(--bbn-weight-title);
  line-height: var(--bbn-leading-tight);
  color: var(--bbn-ink);
  letter-spacing: -0.01em;
}
/* The gold flourish. Decorative only — it sits under a heading that already passes AA,
 * so it carries no meaning and its 2.32:1 is not a defect. */
.bbn-pagehead .bbn-pagetitle::after,
.bbn-pagehead h1::after {
  content: "";
  display: block;
  width: 44px;
  height: 3px;
  margin-top: 8px;
  border-radius: var(--bbn-radius-pill);
  background: var(--bbn-accent);
}
.bbn-pageintro {
  margin: 6px 0 0;
  max-width: 68ch;
  font-size: var(--bbn-size-page-intro);
  line-height: var(--bbn-leading-body);
  color: var(--bbn-text-secondary);
}
.bbn-pagehead-actions { display: flex; flex-wrap: wrap; gap: 8px; }

/* Section grouping — the thing that stops a page reading as one undifferentiated form. */
.bbn-section { margin-bottom: var(--bbn-gap-section); }
.bbn-section > h2,
.bbn-sectionhead {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  font-size: var(--bbn-size-section);
  font-weight: var(--bbn-weight-section);
  line-height: var(--bbn-leading-tight);
  color: var(--bbn-ink);
}
.bbn-sectionhead .bbn-sectionhead-note {
  margin-left: auto;
  font-size: var(--bbn-size-meta);
  font-weight: var(--bbn-weight-body);
  color: var(--bbn-text-muted);
}

/* ============================================================================================
 * BUTTONS
 * ==========================================================================================*/
/* Buttons moved from pill to the 6px product standard (owner decision 2026-09-14). Pill is kept
   only where the shape carries meaning: .bbn-btn-icon (a round icon control), .bbn-chip (a
   pill-shaped chip) and count badges. A pill button and a 6px input side by side read as two
   different design systems, which is what the standard exists to stop. */
.bbn-btn.bbn-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--bbn-target-min);
  padding: 10px 18px;
  border: 1px solid transparent;
  border-radius: var(--bbn-radius);
  font-family: var(--bbn-font);
  font-size: var(--bbn-size-support);
  font-weight: var(--bbn-weight-strong);
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--bbn-motion-fast) ease,
              border-color var(--bbn-motion-fast) ease,
              color var(--bbn-motion-fast) ease;
}
.bbn-btn > svg { width: 17px; height: 17px; flex: 0 0 auto; }

/* Variants are doubled too, and MUST stay doubled: the base above is now (0,2,0), so a
 * single-class variant at (0,1,0) would lose — the base's `border` shorthand would beat
 * --secondary's `border-color` longhand and erase its outline. */

/* Primary — white on teal, 6.20:1. Hover deepens to 9.40:1. */
.bbn-btn--primary.bbn-btn--primary {
  background: var(--bbn-brand);
  color: var(--bbn-text-on-brand);
}
.bbn-btn--primary.bbn-btn--primary:hover { background: var(--bbn-brand-deep); }
.bbn-btn--primary.bbn-btn--primary:active { background: var(--bbn-brand-deep); transform: translateY(1px); }

/* Secondary — outlined. Border is meaningful here (it is the button's only edge), so it uses
 * the teal brand colour at 5.81:1 rather than the decorative hairline. */
.bbn-btn--secondary.bbn-btn--secondary {
  background: var(--bbn-surface);
  color: var(--bbn-brand-deep);
  border-color: var(--bbn-brand);
}
.bbn-btn--secondary.bbn-btn--secondary:hover { background: var(--bbn-brand-tint); }

/* Tertiary — text button. Underline on hover so it is not colour-only. */
.bbn-btn--tertiary.bbn-btn--tertiary {
  background: transparent;
  color: var(--bbn-link);
  padding-inline: 8px;
}
.bbn-btn--tertiary.bbn-btn--tertiary:hover { color: var(--bbn-link-hover); text-decoration: underline; }

/* Destructive — used by admin surfaces. */
.bbn-btn--danger.bbn-btn--danger { background: var(--bbn-error); color: #FFFFFF; }
.bbn-btn--danger.bbn-btn--danger:hover { filter: brightness(0.9); }

.bbn-btn[disabled],
.bbn-btn[aria-disabled="true"] {
  background: var(--bbn-disabled-surface);
  color: var(--bbn-disabled-text);
  border-color: var(--bbn-border);
  cursor: not-allowed;
  opacity: var(--bbn-disabled-opacity);
}

/* Loading — the label stays put and a spinner joins it. aria-busy carries it to AT. */
.bbn-btn[aria-busy="true"] { cursor: progress; }
.bbn-btn[aria-busy="true"]::before {
  content: "";
  width: 14px; height: 14px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: bbn-spin 700ms linear infinite;
}
@keyframes bbn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .bbn-btn[aria-busy="true"]::before { animation-duration: 0s; border-right-color: currentColor; opacity: .5; }
}

/* Icon-only button — MUST still carry an accessible name via aria-label. */
.bbn-btn-icon.bbn-btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--bbn-target-min);
  height: var(--bbn-target-min);
  padding: 0;
  border: 1px solid var(--bbn-border);
  border-radius: var(--bbn-radius-pill);
  background: var(--bbn-surface);
  color: var(--bbn-text);
  cursor: pointer;
}
.bbn-btn-icon.bbn-btn-icon:hover { background: var(--bbn-hover-tint); }

/* ============================================================================================
 * CARDS
 * ==========================================================================================*/
.bbn-card {
  position: relative;
  background: var(--bbn-surface);
  border: 1px solid var(--bbn-border);
  border-radius: var(--bbn-radius);
  padding: var(--bbn-pad-card);
  box-shadow: var(--bbn-shadow-sm);
  transition: box-shadow var(--bbn-motion-fast) ease,
              border-color var(--bbn-motion-fast) ease;
}
.bbn-card--interactive:hover {
  box-shadow: var(--bbn-shadow);
  border-color: var(--bbn-border-strong);
}
/* Hierarchy inside a card: title > meta > body. Not every field equally weighted. */
.bbn-card-title {
  margin: 0 0 4px;
  font-size: var(--bbn-size-card-title);
  font-weight: var(--bbn-weight-strong);
  line-height: var(--bbn-leading-tight);
  color: var(--bbn-ink);
}
.bbn-card-sub {
  margin: 0 0 10px;
  font-size: var(--bbn-size-support);
  color: var(--bbn-text-secondary);
}
.bbn-card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  font-size: var(--bbn-size-meta);
  color: var(--bbn-text-muted);
}
.bbn-card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}
/* Featured card — a tinted ground plus a brand edge. Two cues, not colour alone. */
.bbn-card--featured {
  background: var(--bbn-surface-tint);
  border-left: 4px solid var(--bbn-brand);
}

.bbn-cardgrid {
  display: grid;
  gap: var(--bbn-gap-card);
  grid-template-columns: 1fr;
}
@media (min-width: 640px)  { .bbn-cardgrid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .bbn-cardgrid { grid-template-columns: repeat(3, 1fr); } }
.bbn-cardgrid--wide { grid-template-columns: 1fr; }
@media (min-width: 900px) { .bbn-cardgrid--wide { grid-template-columns: repeat(2, 1fr); } }

/* ============================================================================================
 * BADGES — status is never colour-only; each variant pairs tint + text + (caller-supplied) icon.
 * ==========================================================================================*/
/* ===== BADGE BASE — STRUCTURE ONLY, NO SKIN ================================================
 * This base deliberately sets NO background, colour or border.
 *
 * Why: a page-level <style id="bbn-mcp-custom-css"> (18.6KB, injected on the Funding route)
 * already defines a complete badge system — .bbn-badge-type (solid teal, cream text, 5.81:1),
 * .bbn-badge-feat (gold), .bbn-badge-closed, .bbn-badge-new (mint), .bbn-tag. Those are
 * on-brand, they pass contrast, and they are what the portal actually looks like today.
 *
 * An earlier revision of this file gave the base a background and colour at (0,2,0). That
 * outranked every one of those single-class skins and silently repainted all 201 badges on
 * /funding-opportunities/ a uniform neutral tint — flattening exactly the colour this gate
 * exists to protect. Structure here, skin in the variant, is the correct split and leaves the
 * existing treatments intact.
 *
 * `gap` is the one addition that matters: it spaces the new inline SVG icon from its label
 * without the modules having to emit a spacer, and it is inert on badges that have no icon. */
.bbn-badge.bbn-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
}
.bbn-badge.bbn-badge > svg { width: 13px; height: 13px; flex: 0 0 auto; }
/* Variants are written `.bbn-badge.bbn-badge--x` — badges always carry both classes, and the
 * pairing gives the same (0,2,0) needed to clear theme element rules. */
.bbn-badge.bbn-badge--success { background: var(--bbn-success-surface); border-color: var(--bbn-success-border); color: var(--bbn-success); }
.bbn-badge.bbn-badge--warning { background: var(--bbn-warning-surface); border-color: var(--bbn-warning-border); color: var(--bbn-warning); }
.bbn-badge.bbn-badge--error   { background: var(--bbn-error-surface);   border-color: var(--bbn-error-border);   color: var(--bbn-error); }
.bbn-badge.bbn-badge--info    { background: var(--bbn-info-surface);    border-color: var(--bbn-info-border);    color: var(--bbn-info); }
.bbn-badge.bbn-badge--neutral { background: var(--bbn-surface-sunken);  border-color: var(--bbn-border);         color: var(--bbn-text-muted); }
/* Category badge — quieter than status, so status always wins the eye. */
.bbn-badge.bbn-badge--category { background: var(--bbn-surface); border-color: var(--bbn-border); color: var(--bbn-text-muted); font-weight: var(--bbn-weight-body); }

/* ============================================================================================
 * FORM ELEMENTS
 * Borders here are MEANINGFUL (they are the control's only boundary), so they use
 * --bbn-border-strong at 3.31:1, never the decorative hairline and never the 2.17:1 gold.
 * ==========================================================================================*/
.bbn-field { margin-bottom: 16px; }
.bbn-label {
  display: block;
  margin-bottom: 5px;
  font-size: var(--bbn-size-label);
  font-weight: var(--bbn-weight-strong);
  color: var(--bbn-text);
}
.bbn-hint {
  margin-top: 4px;
  font-size: var(--bbn-size-meta);
  color: var(--bbn-text-muted);
}
.bbn-input.bbn-input,
.bbn-select.bbn-select,
.bbn-textarea.bbn-textarea {
  width: 100%;
  min-height: var(--bbn-target-min);
  padding: 10px 14px;
  box-sizing: border-box;
  border: 1px solid var(--bbn-border-strong);
  border-radius: var(--bbn-radius-sm);
  background: var(--bbn-surface);
  font-family: var(--bbn-font);
  font-size: var(--bbn-size-body);
  color: var(--bbn-text);
}
.bbn-textarea { min-height: 110px; resize: vertical; line-height: var(--bbn-leading-body); }
.bbn-input::placeholder,
.bbn-textarea::placeholder { color: var(--bbn-text-muted); opacity: 1; }
.bbn-input[aria-invalid="true"],
.bbn-textarea[aria-invalid="true"],
.bbn-select[aria-invalid="true"] { border-color: var(--bbn-error); border-width: 2px; }
.bbn-fielderr {
  display: flex;
  align-items: flex-start;
  gap: 5px;
  margin-top: 5px;
  font-size: var(--bbn-size-meta);
  font-weight: var(--bbn-weight-strong);
  color: var(--bbn-error);
}
.bbn-input[disabled],
.bbn-select[disabled],
.bbn-textarea[disabled] { background: var(--bbn-disabled-surface); color: var(--bbn-disabled-text); cursor: not-allowed; }

/* Search — pill-shaped, with a real focus ring. The two portal search inputs previously set
 * `outline:none` with NO replacement, leaving them invisible to keyboard focus. */
.bbn-search { position: relative; display: flex; align-items: center; }
.bbn-search input {
  width: 100%;
  min-height: var(--bbn-target-min);
  padding: 10px 16px 10px 40px;
  box-sizing: border-box;
  border: 1px solid var(--bbn-border-strong);
  border-radius: var(--bbn-radius);
  background: var(--bbn-surface);
  font-family: var(--bbn-font);
  font-size: var(--bbn-size-body);
  color: var(--bbn-text);
}
.bbn-search > svg {
  position: absolute;
  left: 14px;
  width: 16px; height: 16px;
  color: var(--bbn-text-muted);
  pointer-events: none;
}

/* Standalone pill search input — for the Funding and Visibility search fields, which are
 * emitted by their modules WITHOUT a .bbn-search wrapper. Both previously carried
 * `border:2px solid #C6A664; outline:none` in an INLINE style, which is why no stylesheet
 * could fix them: the gold border measured 2.17:1 against the cream field (SC 1.4.11 needs
 * 3:1) and `outline:none` removed the focus indicator outright (SC 2.4.7). An inline style
 * cannot express :focus-visible, so the modules now drop those two declarations and adopt
 * this class instead. */
.bbn-searchinput.bbn-searchinput {
  min-height: var(--bbn-target-min);
  border: 1px solid var(--bbn-border-strong);
  border-radius: var(--bbn-radius);
  background: var(--bbn-surface);
  font-family: var(--bbn-font);
  color: var(--bbn-text);
}
.bbn-searchinput::placeholder { color: var(--bbn-text-muted); opacity: 1; }

/* Segmented control — Cards/List. Selected carries tint + weight + edge, never colour alone. */
.bbn-seg.bbn-seg {
  display: inline-flex;
  padding: 3px;
  gap: 2px;
  border: 1px solid var(--bbn-border-strong);
  border-radius: var(--bbn-radius);
  background: var(--bbn-surface-sunken);
}
/* The inner buttons take the smaller radius so they nest inside the 6px track rather than
   fighting it — a child at the same radius as its parent reads as a misalignment. */
.bbn-seg.bbn-seg button {
  min-height: 36px;
  padding: 6px 16px;
  border: 0;
  border-radius: var(--bbn-radius-sm);
  background: transparent;
  font-family: var(--bbn-font);
  font-size: var(--bbn-size-support);
  font-weight: var(--bbn-weight-body);
  color: var(--bbn-text-secondary);
  cursor: pointer;
}
.bbn-seg.bbn-seg button[aria-pressed="true"] {
  background: var(--bbn-surface);
  color: var(--bbn-brand-deep);
  font-weight: var(--bbn-weight-strong);
  box-shadow: var(--bbn-shadow-sm);
}

/* Filter chip */
.bbn-chip.bbn-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  padding: 6px 14px;
  border: 1px solid var(--bbn-border-strong);
  border-radius: var(--bbn-radius-pill);
  background: var(--bbn-surface);
  font-family: var(--bbn-font);
  font-size: var(--bbn-size-support);
  color: var(--bbn-text);
  cursor: pointer;
}
.bbn-chip.bbn-chip[aria-pressed="true"] {
  background: var(--bbn-selected-surface);
  border-color: var(--bbn-selected-edge);
  font-weight: var(--bbn-weight-strong);
  color: var(--bbn-brand-deep);
}

/* ============================================================================================
 * FEEDBACK — callouts, empty states, skeletons.
 * ==========================================================================================*/
.bbn-note {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border: 1px solid var(--bbn-border);
  border-left-width: 4px;
  border-radius: var(--bbn-radius-sm);
  background: var(--bbn-surface-tint);
  font-size: var(--bbn-size-support);
  line-height: var(--bbn-leading-body);
  color: var(--bbn-text);
}
.bbn-note > svg { width: 18px; height: 18px; flex: 0 0 auto; margin-top: 1px; }
.bbn-note--success { background: var(--bbn-success-surface); border-color: var(--bbn-success-border); border-left-color: var(--bbn-success); color: var(--bbn-success); }
.bbn-note--warning { background: var(--bbn-warning-surface); border-color: var(--bbn-warning-border); border-left-color: var(--bbn-warning); color: var(--bbn-warning); }
.bbn-note--error   { background: var(--bbn-error-surface);   border-color: var(--bbn-error-border);   border-left-color: var(--bbn-error);   color: var(--bbn-error); }
.bbn-note--info    { background: var(--bbn-info-surface);    border-color: var(--bbn-info-border);    border-left-color: var(--bbn-info);    color: var(--bbn-info); }

/* Empty state — icon + heading + one sentence + a real next action. Never a blank panel. */
.bbn-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 8px;
  padding: 40px 20px;
  border: 1px dashed var(--bbn-border-strong);
  border-radius: var(--bbn-radius-lg);
  background: var(--bbn-surface-tint);
}
.bbn-empty-icon {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  border-radius: 50%;
  background: var(--bbn-surface);
  color: var(--bbn-brand);
  box-shadow: var(--bbn-shadow-sm);
}
.bbn-empty-icon > svg { width: 22px; height: 22px; }
.bbn-empty h3 {
  margin: 4px 0 0;
  font-size: var(--bbn-size-section);
  font-weight: var(--bbn-weight-section);
  color: var(--bbn-ink);
}
.bbn-empty p {
  margin: 0;
  max-width: 46ch;
  font-size: var(--bbn-size-support);
  line-height: var(--bbn-leading-body);
  color: var(--bbn-text-secondary);
}
.bbn-empty .bbn-btn { margin-top: 6px; }

/* Skeleton — shape-only placeholder so async surfaces do not jump from blank to full.
 * aria-hidden on the element; the live region announces the real state. */
.bbn-skel {
  background: linear-gradient(90deg, var(--bbn-surface-sunken) 25%, var(--bbn-surface-tint) 37%, var(--bbn-surface-sunken) 63%);
  background-size: 400% 100%;
  border-radius: var(--bbn-radius-sm);
  animation: bbn-shimmer 1.4s ease-in-out infinite;
}
@keyframes bbn-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) {
  .bbn-skel { animation: none; background: var(--bbn-surface-sunken); }
}
.bbn-skel-line { height: 12px; margin-bottom: 8px; }
.bbn-skel-line--short { width: 55%; }
.bbn-skel-title { height: 18px; width: 70%; margin-bottom: 12px; }

/* ============================================================================================
 * ROUTE CORRECTIONS — /saved/ (page 3406)
 *
 * These are NOT restyling. The Saved page ships its own ~3.8KB inline <style> (an Elementor
 * widget, no id, so it cannot be grepped from the plugin) which is a perfectly reasonable skin
 * — EXCEPT that three rules use #8a7c6e on white, which measures 4.05:1. All three are text
 * below 18.66px (13px, 11px and 10.5px), so AA requires 4.5:1 and all three FAIL SC 1.4.3.
 *
 * This is the same colour dashboard.css already documented and replaced with #6b5b4d; the
 * Saved page simply never got that fix. Only the colour is overridden here — background,
 * border, radius, padding and layout all stay with the page's own rules, so the page still
 * looks like itself.
 *
 * SPECIFICITY: the page's rules are written `#bbn-saved .sv-empty` — that is (1,1,0), and an
 * ID outranks ANY number of classes, so a doubled class (0,2,0) loses outright. Measured: the
 * colour stayed rgb(138,124,110) until these selectors carried the id too. `#bbn-saved` +
 * doubled class = (1,2,0), which clears the page rule without !important.
 * ==========================================================================================*/
#bbn-saved .sv-empty.sv-empty       { color: var(--bbn-text-muted); }   /* 4.05:1 -> 6.50:1 */
#bbn-saved .sv-gt.sv-gt             { color: var(--bbn-text-muted); }   /* 4.05:1 -> 6.50:1 */
#bbn-saved .sv-pill.sv-pill.passed  { color: var(--bbn-text-muted); }   /* 4.05:1 -> 6.50:1 */

/* ---- /funding-opportunities/ (page 1963) ------------------------------------------------
 * Measured live across 191 visible text elements: two colour values fail, both #9A8F86 —
 * another of the retired muted greys (3.16:1 on white, 2.96:1 on the cream card).
 *
 *   #bbn-fund-count      "28 of 28 open funding opportunities"  13px   3.16:1
 *   widget 0ff71b9       the "Funder:"   label on every card    10px   2.96:1
 *   widget df4855d       the "Deadline:" label on every card    10px   2.96:1
 *
 * Those two widget ids share ONE Elementor rule and cover 188 elements (94 cards x 2 labels).
 * The count was fixed in funding-grid.js instead, because it is written as an INLINE style —
 * no stylesheet, at any specificity or importance, can reach an inline declaration.
 *
 * The count is the page's answer to "did my filter work", and Funder is the single most-read
 * field on a funding card — neither is decorative. The Funder label is Elementor-authored, so
 * the override is scoped to that one widget id rather than to headings generally.
 *
 * `body #…` and a doubled class supply the specificity; both original rules are authored at
 * id / element-id strength and would otherwise win on source order.
 */
body #bbn-fund-count { color: var(--bbn-text-muted); }                    /* 3.16:1 -> 6.50:1 */
/* !important is used HERE and nowhere else in this file, and only because the rule being
 * corrected is itself !important:
 *     .elementor-element-0ff71b9 .elementor-heading-title{color:rgb(154,143,134)!important}
 * emitted by Elementor into post-1851.css (the loop-item template) and repeated inline once
 * per card. An !important declaration cannot be beaten by specificity alone, so a normal
 * declaration — even at (0,4,1) — was measured doing nothing at all. Matching the flag is the
 * only way to correct it from a stylesheet; the alternative is editing the widget's colour in
 * Elementor, which is the better long-term fix and is noted for the owner. */
body .elementor-element-0ff71b9 .elementor-heading-title.elementor-heading-title,
body .elementor-element-df4855d .elementor-heading-title.elementor-heading-title {
  color: var(--bbn-text-muted) !important;                               /* 2.96:1 -> 6.09:1 */
}

/* ---- /portal/ dashboard (page 2277) ------------------------------------------------------
 * The dashboard's opportunity title links render GOLD at 2.32:1. The page's own CSS asks for
 * `.db2-row-t { color:#3F291B }` (13.60:1) — it simply loses. `.db2-row-t` is (0,1,0); the
 * Elementor kit's global link colour `.elementor-kit-8 a` is (0,2,0) and wins.
 *
 * This is the same root cause as the skip link, and it is the clearest illustration of why the
 * global gold link colour is a portal-wide problem rather than a page-level one: every <a> that
 * is not explicitly coloured inherits a 2.32:1 value. These are the names of the funding
 * opportunities on the member's home screen — the most-read links in the product.
 *
 * `body #bbn-db2` is (1,2,0), which clears the kit rule without !important. Hover already
 * resolves to the brand teal in the page CSS and is left alone.
 */
body #bbn-db2 .db2-row-t { color: var(--bbn-ink); }        /* 2.32:1 -> 13.60:1 */
body #bbn-db2 a.db2-more { color: var(--bbn-link); }       /* "See all funding →" 2.32 -> 6.20 */
/* .db2-up covers EIGHT more dashboard links — quick actions ("Update your business listing",
 * "Email preferences"), the updates list, and the Explore card. The contrast sweep dedupes by
 * colour+size, so all eight surfaced as a single finding; fixing only the sampled one would
 * have left seven unreadable links and a sweep that reported clean. */
body #bbn-db2 a.db2-up   { color: var(--bbn-link); }       /* 2.32:1 -> 8.20:1 */
body #bbn-db2 a.db2-up:hover { color: var(--bbn-link-hover); }

/* ---- /directory/ (page 121) --------------------------------------------------------------
 * Measured live across 191 visible text elements. SIX looked like failures; FOUR were false
 * positives and are deliberately left alone — the cover and promo banner paint their ground
 * with a `linear-gradient`, i.e. a background-IMAGE, which `backgroundColor` reports as
 * transparent. Composited against the real gradient they pass comfortably:
 *     #E7E1D5 banner text  7.64:1   #C6A664 cover eyebrow  5.85:1
 *     #C6A664 "BBN" 48px   5.85:1   #F3ECE0 cover subtitle 11.58:1
 * Recolouring those would have made the cover worse for no accessibility gain.
 *
 * TWO are real:
 *   .bbn-promo-banner strong  #C6A664 14px on the gradient's lighter end (#5A3C28) = 4.28:1.
 *       Just under AA. #D2B87C is the same gold two steps lighter and measures 5.16:1, so the
 *       banner keeps its accent rather than falling back to cream.
 *   #bbn-dir-count            #9A8F86 13px on white = 3.16:1 — the result count, the same
 *       defect as #bbn-fund-count on Funding. It is read from the page (not JS-generated), so
 *       unlike the Funding one it can be corrected from here.
 */
body .bbn-promo-banner strong { color: #D2B87C; }            /* 4.28:1 -> 5.16:1 */
body #bbn-dir-count           { color: var(--bbn-text-muted); } /* 3.16:1 -> 6.50:1 */

/* ---- /updates/ (page 1970) ---------------------------------------------------------------
 * Four measured failures, all of them the gold used where it cannot carry text.
 *
 *   17 archive links ("February", "March", …) gold on white           2.32:1
 *   "Featured" badges, white on a GOLD ground                          2.32:1
 *   "Search" button label, white on a GOLD ground                      2.32:1
 *   the "2025" year heading, gold on white at 44px (large: needs 3)    2.32:1
 *
 * White-on-gold is the worst of these — it is a button and a badge nobody can read. Gold is
 * not the problem; white on it is. Flipping to the ink token gives 5.85:1 and keeps the gold
 * exactly as designed.
 *
 * The archive links are scoped to their own list widget rather than recoloured globally: 18
 * gold links are visible on this route and ONE of them (the nav brand mark) sits on the teal
 * block, where teal-on-teal would be worse than what it replaces. A blanket link rule would
 * have broken it.
 *
 * Content, ordering and the Mailchimp embed are untouched — colour only.
 *
 * The !important flags below match flags already present on the Elementor/UE rules being
 * corrected (same reason as the Funder label above). Where a plain declaration was enough —
 * the archive list and the Search label — none is used.
 */
body .elementor-element-c3b9918 .elementor-icon-list-item a { color: var(--bbn-link); }  /* 2.32 -> 6.20 */
body .elementor-element-c3b9918 .elementor-icon-list-item a:hover { color: var(--bbn-link-hover); text-decoration: underline; }
body .ue-btn-text { color: var(--bbn-ink); }         /* white on gold 2.32 -> ink on gold 5.85 */

/* Beats `.elementor-2024 .elementor-element.elementor-element-cd8959c .ue-badge-title`. */
body .ue-badge-title.ue-badge-title { color: var(--bbn-ink) !important; }   /* 2.32 -> 5.85 */

/* The year heading resisted every plain override; the winning declaration could not be
 * identified reliably from the CSSOM, so this matches importance rather than guessing again.
 *
 * The declaration that actually wins is `h3 { color: var(--wp--preset--color--contrast-3)
 * !important }` — WordPress global styles, injected inline, specificity (0,0,1). An !important
 * theme declaration is why every plain override here did nothing.
 *
 * TWO year headings exist — 2025 (65158a5) and 2026 (4e0b614) — and fixing only the first
 * left the second failing. A new heading is added each year, so this will need the new widget
 * id when the 2027 section appears; the archive is already a known manual maintenance step. */
body .elementor-1970 .elementor-element-65158a5 .elementor-heading-title,
body .elementor-1970 .elementor-element-4e0b614 .elementor-heading-title {
  color: var(--bbn-accent-ink) !important;                                  /* 2.32 -> 5.36 */
}

/* Elementor's GLOBAL link colour is the gold: `.elementor-kit-8 a { color: <gold> }`. On this
 * route that leaves body links at 2.32:1. Scoped to this page's Elementor wrapper, which the
 * portal nav is NOT inside (nav.js appends #bbn-nav to <body>), so the one gold-on-dark link
 * on the page — the nav brand mark — keeps its treatment.
 *
 * NOTE FOR THE OWNER: the same global rule applies site-wide, so plain links on other pages
 * are gold at 2.32:1 too. Changing it globally is a visual decision across the public site as
 * well as the portal, so it is reported rather than done here. */
body .elementor-1970 a { color: var(--bbn-link); }                          /* 2.32 -> 6.20 */
body .elementor-1970 a:hover { color: var(--bbn-link-hover); }

/* ---- /messages/ (page 3415) -------------------------------------------------------------
 * Same #8a7c6e-on-white family. Measured live at 4.05:1 across the signed-out gate and every
 * secondary line in the thread list. The page's rules are `#bbn-msg .mg-*` (1,1,0), so these
 * carry the id for the same reason the Saved ones do.
 *
 * .mg-when / .mg-stamp are timestamps and .mg-meta is sender metadata — small, frequently
 * read, and exactly the text a 4.05:1 value hurts most.
 */
#bbn-msg .mg-gate.mg-gate,
#bbn-msg .mg-sub.mg-sub,
#bbn-msg .mg-when.mg-when,
#bbn-msg .mg-empty.mg-empty,
#bbn-msg .mg-meta.mg-meta,
#bbn-msg .mg-stamp.mg-stamp,
#bbn-msg .mg-note.mg-note,
#bbn-msg .mg-pick.mg-pick span,
#bbn-msg .mg-mh.mg-mh button { color: var(--bbn-text-muted); }   /* 4.05:1 -> 6.50:1 */

/* Empty/loading scaffolding the module now emits. .bbn-empty supplies the shared shape; these
 * only reconcile it with the page's existing .sv-empty box so the two do not fight. */
#bbn-saved .sv-empty.bbn-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 8px;
  padding: 36px 20px;
}
#bbn-saved .sv-empty-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 4px; }
#bbn-saved .sv-loading { display: grid; gap: var(--bbn-gap-card); }
#bbn-saved .sv-loading .sv-card { box-shadow: none; }
/* The page-level `#bbn-saved .sv-btn` skin was REMOVED in the 2026-09-14 pass (it was half of
 * the teal-on-teal empty-state CTA defect), and the markup no longer carries .sv-btn at all —
 * "Save note" is now a plain .bbn-btn--secondary. This rule therefore matches on .sv-btn-sm
 * alone. It keeps the id so it still outranks the (0,2,0) button base for size only. */
#bbn-saved .sv-btn-sm { font-size: var(--bbn-size-meta); padding: 8px 16px; margin-top: 8px; }

/* Group headings are now real <h2>s. The page's .sv-gt rule already supplies size, weight and
 * letter-spacing; these add the icon alignment and reset the heading's default margin so the
 * change from <div> to <h2> is invisible visually. text-transform does the shouting that the
 * data no longer does. */
#bbn-saved .sv-gt.sv-gt {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 8px;
  text-transform: uppercase;
}
#bbn-saved .sv-gt.sv-gt .sv-gt-n {
  display: inline-grid;
  place-items: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: var(--bbn-radius-pill);
  background: var(--bbn-surface-sunken);
  color: var(--bbn-text-muted);
  font-size: var(--bbn-size-badge);
  letter-spacing: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════════════════════
   DIRECTORY (page 121) — HORIZONTAL OVERFLOW AT 375px
   ═══════════════════════════════════════════════════════════════════════════════════════════

   MEASURED: document scrollWidth 598 against a 375px viewport — the page scrolled sideways by
   223px on every phone.

   The cause is NOT in the directory's own CSS, which is why it survived several passes. Walking
   the ancestor chain from the widest overflowing element (a social link at right:541) showed the
   width being introduced three levels above the app:

       .elementor-element-b084e91   343px   <- correctly constrained
       .elementor-element-b6008dd   582px   <- BREAKS HERE:  flex: 0 0 auto
       .bbn-dir-grid                558px   <- grid-cols computed to a single 558px track
       .bbn-dir-card                558px
       .bbn-dir-socs                516px   <- had flex-wrap:wrap and still could not wrap

   `flex-shrink: 0` on the Elementor container means it sizes to MAX-CONTENT and refuses to
   shrink to its 343px parent. Everything inside then has 582px to fill, so nothing ever needs
   to wrap — which is why `.bbn-dir-socs` already having `flex-wrap: wrap` fixed nothing, and
   why adding `overflow-wrap` to the links alone could not have worked either: word-breaking
   lowers MIN-content, and an unshrinkable box is sized by MAX-content.

   Fixed from the portal layer rather than by editing the Elementor container, so the change is
   reversible, version-controlled, and cannot be lost by a builder edit. The `:has()` selector is
   the durable half — it finds the wrapper by what it CONTAINS, so a re-saved page that changes
   the hashed element id does not silently reintroduce the defect. The explicit id is kept as a
   belt-and-braces fallback for browsers without :has().

   VERIFIED before/after at three viewports:
       375px   scrollWidth 598 -> 375   card 558 -> 319   1 column
       768px   unchanged                2 columns, 346px each
      1280px   unchanged                3 columns, 358px each
   Mobile is fixed and desktop is byte-identical. */
body .elementor-element.e-con:has(#bbn-dir-root),
body .elementor-element-b6008dd {
  min-width: 0;
  flex-shrink: 1;
  max-width: 100%;
}
/* Grid items default to min-width:auto, which keeps a track from shrinking below its content. */
body .bbn-dir-grid { min-width: 0; }
/* Long unbroken URLs ("Instagram.com/brownbabiespuzzles") are the widest atoms in a card. This
   does not fix the overflow on its own — see above — but it stops a single link defining the
   card's minimum width once the container can shrink. */
body .bbn-dir-socs a,
body .bbn-dir-links a { min-width: 0; overflow-wrap: anywhere; }

/* ═══════════════════════════════════════════════════════════════════════════════════════════
   CORNER LANGUAGE — bringing the card families onto the 6px standard
   ═══════════════════════════════════════════════════════════════════════════════════════════

   MEASURED 2026-09-14, computed at runtime: three corner languages for the same component type.

       .bbn-dir-card   (Directory)          16px
       .db2-card       (Dashboard)          10px
       .bt-card        (Resource Library)   10px

   None of those was a decision — they are what the portal drifted into. The product standard is
   6px for ordinary UI.

   WHY THIS LIVES HERE RATHER THAN AT THE SOURCE: both families are defined in page-level <style>
   blocks emitted by Elementor widgets — the Directory's is an 8.8KB unnamed block on page 121 —
   not in any plugin file. Editing them means a builder edit that is invisible to version
   control, ungreppable from the plugin, and silently reverted by the next page save. A
   portal-layer override is reversible, reviewable, and loses nothing: `body .bbn-dir-card` is
   (0,1,1) against the page block's (0,1,0), and components.css loads later regardless.

   This is the same class of problem as the brand teal and it has the same shape: the token
   system does not own the pixels it is supposed to own. Tracked, not rewritten.

   ⚠️ `.bt-card` needs the id. Its page block authors it as `#bbn-tools .bt-card` = (1,1,0), and
   an ID beats any number of classes, so `body .bt-card` at (0,1,1) silently did nothing — the
   rule shipped and the cards stayed at 10px. Same trap as `#bbn-saved .sv-*`, already documented
   in DESIGN-ARCHITECTURE.md. Verify a corrected radius by MEASURING it, not by reading the rule. */
body .bbn-dir-card,
body .db2-card,
body #bbn-tools .bt-card { border-radius: var(--bbn-radius); }

/* ═══════════════════════════════════════════════════════════════════════════════════════════
   THE FOUR STATES — loading · empty · error · ready
   ═══════════════════════════════════════════════════════════════════════════════════════════

   A member must be able to tell these apart WITHOUT knowing what they did to cause them. Today
   they often cannot: several dashboard blocks catch a failure and render the same neutral copy
   they show when there is genuinely nothing — so "this did not load" and "you have not saved
   anything yet" are the same screen. One of those is the product working and the other is the
   product broken, and a member who cannot tell them apart cannot report the broken one.

   Distinction is carried by THREE cues, never colour alone: an icon, a border treatment, and the
   wording. That is what keeps it working in greyscale and for colour-blind members.

       --loading   neutral, animated, no icon      "Loading saved opportunities…"
       --empty     neutral, calm, outline icon     "No saved opportunities yet"
       --error     error tint + left rule + alert  "We couldn't load your saved opportunities."

   ERROR also needs role="alert" IN THE MARKUP — an error paragraph without it is invisible to a
   screen reader, which is the whole failure this distinction exists to prevent. The CSS cannot
   supply that; see saved-app.js for the correct pattern. */
.bbn-state {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 14px 16px;
  border: 1px solid var(--bbn-border);
  border-radius: var(--bbn-radius);
  background: var(--bbn-surface-tint);
  color: var(--bbn-text-secondary);
  font-size: var(--bbn-size-support);
  line-height: var(--bbn-leading-body);
}
.bbn-state > svg { width: 18px; height: 18px; flex: 0 0 auto; margin-top: 1px; }
.bbn-state-title { display: block; font-weight: var(--bbn-weight-strong); color: var(--bbn-text); }

.bbn-state--empty { background: var(--bbn-surface-sunken); }

.bbn-state--error {
  background: var(--bbn-error-surface);
  border-color: var(--bbn-error-border);
  border-left: 3px solid var(--bbn-error);
  color: var(--bbn-error);
}

.bbn-state--loading { color: var(--bbn-text-muted); }
/* The pulse is the cue that something is still happening. Honour reduced-motion: a member who
   has asked for less movement still needs to know the difference between loading and empty, so
   the text carries it and only the animation stops. */
.bbn-state--loading::before {
  content: "";
  width: 14px; height: 14px; flex: 0 0 auto; margin-top: 2px;
  border: 2px solid var(--bbn-border-strong);
  border-top-color: var(--bbn-brand);
  border-radius: var(--bbn-radius-pill);
  animation: bbn-spin 0.8s linear infinite;
}
@keyframes bbn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .bbn-state--loading::before { animation: none; }
}

/* ── Dashboard blocks: .db2-empty is page-level CSS and paints a RETIRED colour ───────────────
   #8a7c6e measures 4.05:1 and was retired for exactly that reason; --bbn-text-muted replaced it.
   It survived every contrast sweep because the element only renders when a block is empty or has
   failed — the two moments nobody screenshots. A state message that appears only when something
   has gone wrong is the LAST place that should be hard to read. */
body .db2-empty { color: var(--bbn-text-muted); }
body .db2-empty--error {
  color: var(--bbn-error);
  border-left: 3px solid var(--bbn-error);
  padding-left: 10px;
}

/* ── .db2-chip — the monthly theme label, classified BY MEANING (2026-09-15) ─────────────────
 * Rendered content: "BACK TO BUSINESS" — the month's theme, sitting beside the dashboard
 * greeting. So it is an EDITORIAL / BRAND label. It is not a status, not success, and not a
 * state indicator, which matters because of how it was going to be fixed.
 *
 * Measured before the change: color rgb(8,80,65) = #085041 on #E1F5EE, 8.28:1 — it PASSED
 * contrast, so this was never an accessibility defect. #085041 is simply the SUPERSEDED
 * brand-deep, left behind by the brand darkening.
 *
 * WHY IT WAS DEFERRED, AND WHY IT IS SAFE NOW: #085041 doubled as the OLD success colour, and a
 * previous blanket replace of that hex silently converted an `ok ? success : error` branch in
 * login-reset.js into a brand colour. The lesson was not "never touch this hex" — it was
 * "decide what each instance MEANS before changing it". This instance means brand, so it takes
 * the brand token. A bulk hex replace would still be wrong.
 *
 * After: --bbn-brand-deep (#07493B) on the same tint, ~9.3:1 — slightly better, same identity.
 * The 999px pill radius is deliberate and stays: this is a label, one of the documented
 * round-on-purpose exceptions.
 *
 * Page-level rule, so overridden from here: `.db2-chip` is (0,1,0), `body .db2-chip` is (0,1,1). */
body .db2-chip { color: var(--bbn-brand-deep); }

/* ── MY ACCOUNT · SECURITY section (2026-09-15) ───────────────────────────────────────────────
 * The password control was functional but hidden inside a collapsed <details> at the bottom of
 * the form. account-app.js now promotes it into a real section; this gives it a visual identity.
 *
 * DELIBERATELY SECONDARY, NOT HIDDEN. Profile editing is what members come here to do, and
 * "Save changes" must stay the primary action — so Security sits below it, separated by a rule,
 * on the sunken surface rather than as another card competing for attention. The distinction is
 * position and ground, not a disclosure the member has to discover.
 *
 * The button keeps the ghost treatment it already had: a second filled teal button here would
 * read as a second primary action on the page, which is the emphasis-inflation the design system
 * measures for. */
body .bp-security {
  margin-top: var(--bbn-gap-section);
  padding: 18px;
  border-top: 1px solid var(--bbn-border);
  border-radius: 0 0 var(--bbn-radius) var(--bbn-radius);
  background: var(--bbn-surface-sunken);
}
body .bp-security-h {
  margin: 0 0 10px;
  font-size: var(--bbn-size-section);
  font-weight: var(--bbn-weight-section);
  color: var(--bbn-text);
  letter-spacing: .01em;
}
body .bp-security-sub {
  margin: 0 0 2px;
  font-weight: var(--bbn-weight-strong);
  color: var(--bbn-text);
  font-size: var(--bbn-size-body);
}
body .bp-security-note {
  margin: 0 0 12px;
  color: var(--bbn-text-secondary);
  font-size: var(--bbn-size-support);
  line-height: var(--bbn-leading-body);
}
/* The label wraps its input, so it needs to stack rather than run the field onto the same line. */
body .bp-security label {
  display: block;
  margin-bottom: 12px;
  font-size: var(--bbn-size-support);
  color: var(--bbn-text-secondary);
}
body .bp-security input[type="password"] {
  display: block;
  width: 100%;
  max-width: 340px;
  margin-top: 6px;
  min-height: var(--bbn-target-min);
  box-sizing: border-box;
  padding: 10px 14px;
  border: 1px solid var(--bbn-border-strong);
  border-radius: var(--bbn-radius);
  background: var(--bbn-surface);
  color: var(--bbn-text);
  font-family: var(--bbn-font);
  font-size: var(--bbn-size-body);
}
/* ── FOCUS INDICATOR for every My Account field ───────────────────────────────────────────────
 * Surfaced while promoting the Security section, and it is not limited to the password field.
 * The page styles its own form at:
 *
 *     #bbn-profile input:focus, #bbn-profile select:focus, #bbn-profile textarea:focus
 *     { outline:none; border-color:#1E8A78; box-shadow:0 0 0 3px rgba(30,138,120,.18) }
 *
 * `outline:none` removes the browser's fallback, and what replaces it is an 18%-alpha glow
 * measuring roughly 1.2:1 against white — far short of the 3:1 SC 1.4.11 requires of a focus
 * indicator. So EVERY field on this route, not just the new one, was effectively unfocusable
 * for a keyboard user.
 *
 * ⚠️ SPECIFICITY: that rule is (1,1,1). The first attempt here was
 * `body .bp-security input[type="password"]:focus` = (0,3,2), which LOST — an ID beats any number
 * of classes. Third time this exact trap has appeared on this product (#bbn-saved, #bbn-tools,
 * now #bbn-profile), and every time it shipped looking correct and did nothing. Carry the id.
 *
 * Measured after: outline rgb(255,255,255) + box-shadow rgb(11,68,55) — the portal two-tone ring,
 * 11.07:1 on white. */
body #bbn-profile input:focus,
body #bbn-profile select:focus,
body #bbn-profile textarea:focus,
body #bbn-profile .bp-security input[type="password"]:focus {
  outline: 2px solid var(--bbn-focus-inner);
  outline-offset: 0;
  box-shadow: 0 0 0 5px var(--bbn-focus-ring);
  border-color: var(--bbn-brand);
}

/* Screen-reader-only utility — used for live regions and icon-button names. */
.bbn-sr {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
