/* palette.css */
/* GENERATED by scripts/gen-palette.mjs from private/config/palette.json — DO NOT EDIT.
   Edit the JSON and re-run `npm run ast`. Contrast is asserted in tests/contrast.tests.js.

   This is the ONLY file that may declare a theme token. Templates and stylesheets reference
   them; they never redeclare them. */

/* Theme-independent: geometry, type and the --color-* aliases are the same in both themes. */
:root{
  --radius: 16px;
  --radius-lg: 18px;
  --radius-sm: 8px;
  --maxw: 1200px;
  --spacing-xs: .25rem;
  --spacing-sm: .5rem;
  --spacing-md: .9rem;
  --spacing-lg: 1.4rem;
  --spacing-xl: 2rem;
  --spacing-2xl: 3rem;
  --safe-top: env(safe-area-inset-top,0px);
  --safe-bottom: env(safe-area-inset-bottom,0px);
  --transition-fast: .12s ease;
  --transition-normal: .2s ease;
  --transition-slow: .35s ease;
  --sans: Inter,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --display: Montserrat,var(--sans);
  --mono: ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
  --color-background: var(--bg);
  --color-card: var(--card);
  --color-foreground: var(--fg);
  --color-muted: var(--muted);
  --color-border: var(--border);
  --color-primary: var(--blue-dark);
  --color-primary-hover: var(--blue);
  --color-link: var(--blue);

  /* Third-party BRAND colours. Emitted once, outside the themes, because an external brand
     colour must NOT follow ours — that is the point of a brand colour. They were written as
     literals three times per rule (background, border-color) across .share-*, .social-* and
     .asw-*, so #1877f2 appeared five times. */
  --brand-facebook: #1877f2;
  --brand-linkedin: #0a66c2;
  --brand-reddit: #ff4500;
  --brand-whatsapp: #25d366;
  --brand-instagram: #e1306c;
  --brand-github: #24292f;
  --brand-youtube: #ff0000;
  --brand-mastodon: #6364ff;
  --brand-telegram: #229ed9;
  --brand-bluesky: #0085ff;
  --brand-x: #000000;
}

/* DARK is canonical — the templates were designed against it — so it is the bare :root value. */
:root{
  color-scheme: dark;
  --bg: #0f1117;
  --surface: #0d1219;
  --card: #141B21;
  --card2: #0f151b;
  --fg: #cdd9f0;
  --headline: #eaf1ff;
  --muted: #8093b0;
  --subtle: #76849e;
  --border: #1F2830;
  --blue: #3b82f6;
  --blue2: #3b82f6;
  --blue-dark: #2563EB;
  --blue-light: #60A5FA;
  --teal: #0D9488;
  --teal2: #22c9b8;
  --teal-light: #14B8A6;
  --yellow: #F4C430;
  --red: #E74C3C;
  --green: #4ADE80;
  --accent: #3b82f6;
  --join: #1f7a4d;
  --members: #2563EB;
  --ring: rgba(59,130,246,.35);
  --chip: rgba(59,130,246,.10);
  --glass: rgba(20,27,33,.72);
  --scrim: rgba(8,11,17,.66);
  --grid-line: rgba(59,130,246,.06);
  --tint-blue: rgba(59,130,246,.16);
  --tint-teal: rgba(13,148,136,.18);
  --tint-yellow: rgba(244,196,48,.16);
  --tint-red: rgba(231,76,60,.16);
  --yellow-surface: #F4C430;
  --teal-surface: #0D9488;
  --on-yellow: #2a2205;
  --on-teal: #04201d;
  --on-light: #12203b;
  --surface-modal: #05070b;
  --surface-deep: #0a0d12;
  --banner-border: #1F2830;
  --blue-hover: #1d4fd8;
  --on-brand: #ffffff;
  --surface-invert: #ffffff;
  --letterbox: #000000;
  --eyebrow: #9fb2d6;
  --shadow: 0 18px 50px rgba(0,0,0,.45);
  --shadow-card: 0 6px 20px -12px rgba(0,0,0,.55);
  --shadow-card-hover: 0 10px 28px -14px rgba(0,0,0,.65);
  --grad: linear-gradient(120deg,#3b82f6,#0D9488);
  --hero-overlay: linear-gradient(102deg, rgba(12,14,20,.96) 0%, rgba(13,16,23,.82) 42%, rgba(13,18,25,.5) 78%, rgba(13,18,25,.35) 100%);
  --workshop-bg: repeating-linear-gradient(45deg,#171e26,#171e26 12px,#141a22 12px,#141a22 24px);
  --banner-bg: repeating-linear-gradient(135deg,#191c12,#191c12 12px,#16180f 12px,#16180f 24px);
  --request-wash: linear-gradient(120deg,#12233f,var(--bg) 70%);
  --hero-financial: linear-gradient(90deg,#111826,#0d1524);
  --yellow-grad: linear-gradient(180deg,#f7d05a,var(--yellow-surface));
  --teal-grad: linear-gradient(180deg,#22c9b8,var(--teal-surface));
  --text: var(--fg);
}

/* The OS preference, but ONLY while no explicit choice is stored — otherwise choosing dark on a
   light-set machine would have no effect, because the media query would keep winning. */
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]):not([data-theme="light"]){
    color-scheme: light;
    --bg: #eceff4;
    --surface: #e3e8ef;
    --card: #f6f8fa;
    --card2: #eef2f7;
    --fg: #111827;
    --headline: #0b1220;
    --muted: #3f4a5a;
    --subtle: #5b6676;
    --border: #cbd5e1;
    --blue: #2562e9;
    --blue2: #2562e9;
    --blue-dark: #1d4ed8;
    --blue-light: #2562e9;
    --teal: #0b7970;
    --teal2: #0b7970;
    --teal-light: #0b7970;
    --yellow: #816819;
    --red: #c03f32;
    --green: #297a46;
    --accent: #1d4ed8;
    --join: #1b6b43;
    --members: #1d4ed8;
    --ring: rgba(29,78,216,.35);
    --chip: rgba(29,78,216,.08);
    --glass: rgba(246,248,250,.78);
    --scrim: rgba(15,23,42,.45);
    --grid-line: rgba(29,78,216,.06);
    --tint-blue: rgba(29,78,216,.10);
    --tint-teal: rgba(11,121,112,.12);
    --tint-yellow: rgba(129,104,25,.12);
    --tint-red: rgba(192,63,50,.10);
    --yellow-surface: #F4C430;
    --teal-surface: #0D9488;
    --on-yellow: #2a2205;
    --on-teal: #04201d;
    --on-light: #12203b;
    --surface-modal: #f6f8fa;
    --surface-deep: #e3e8ef;
    --banner-border: #ecdca6;
    --blue-hover: #1a45c0;
    --on-brand: #ffffff;
    --surface-invert: #ffffff;
    --letterbox: #000000;
    --eyebrow: #9fb2d6;
    --shadow: 0 18px 40px -26px rgba(15,42,90,.35);
    --shadow-card: 0 1px 3px rgba(15,42,90,.08);
    --shadow-card-hover: 0 10px 22px -12px rgba(15,42,90,.18);
    --grad: linear-gradient(120deg,#1d4ed8,#0b7970);
    --hero-overlay: linear-gradient(102deg, rgba(236,239,244,.96) 0%, rgba(236,239,244,.8) 44%, rgba(227,232,239,.45) 80%, rgba(227,232,239,.25) 100%);
    --workshop-bg: repeating-linear-gradient(45deg,#e6ecf5,#e6ecf5 12px,#eef2f8 12px,#eef2f8 24px);
    --banner-bg: #fff7e0;
    --request-wash: linear-gradient(120deg,#e3edfb,#ffffff 72%);
    --hero-financial: linear-gradient(90deg,#e3e8ef,#eef2f7);
    --yellow-grad: linear-gradient(180deg,#f7d05a,var(--yellow-surface));
    --teal-grad: linear-gradient(180deg,#22c9b8,var(--teal-surface));
    --text: var(--fg);
  }
}

/* An explicit choice always wins. Dark is restated here so a stylesheet keying on the attribute
   sees the same values as one keying on the bare root. */
:root[data-theme="light"]{
  color-scheme: light;
  --bg: #eceff4;
  --surface: #e3e8ef;
  --card: #f6f8fa;
  --card2: #eef2f7;
  --fg: #111827;
  --headline: #0b1220;
  --muted: #3f4a5a;
  --subtle: #5b6676;
  --border: #cbd5e1;
  --blue: #2562e9;
  --blue2: #2562e9;
  --blue-dark: #1d4ed8;
  --blue-light: #2562e9;
  --teal: #0b7970;
  --teal2: #0b7970;
  --teal-light: #0b7970;
  --yellow: #816819;
  --red: #c03f32;
  --green: #297a46;
  --accent: #1d4ed8;
  --join: #1b6b43;
  --members: #1d4ed8;
  --ring: rgba(29,78,216,.35);
  --chip: rgba(29,78,216,.08);
  --glass: rgba(246,248,250,.78);
  --scrim: rgba(15,23,42,.45);
  --grid-line: rgba(29,78,216,.06);
  --tint-blue: rgba(29,78,216,.10);
  --tint-teal: rgba(11,121,112,.12);
  --tint-yellow: rgba(129,104,25,.12);
  --tint-red: rgba(192,63,50,.10);
  --yellow-surface: #F4C430;
  --teal-surface: #0D9488;
  --on-yellow: #2a2205;
  --on-teal: #04201d;
  --on-light: #12203b;
  --surface-modal: #f6f8fa;
  --surface-deep: #e3e8ef;
  --banner-border: #ecdca6;
  --blue-hover: #1a45c0;
  --on-brand: #ffffff;
  --surface-invert: #ffffff;
  --letterbox: #000000;
  --eyebrow: #9fb2d6;
  --shadow: 0 18px 40px -26px rgba(15,42,90,.35);
  --shadow-card: 0 1px 3px rgba(15,42,90,.08);
  --shadow-card-hover: 0 10px 22px -12px rgba(15,42,90,.18);
  --grad: linear-gradient(120deg,#1d4ed8,#0b7970);
  --hero-overlay: linear-gradient(102deg, rgba(236,239,244,.96) 0%, rgba(236,239,244,.8) 44%, rgba(227,232,239,.45) 80%, rgba(227,232,239,.25) 100%);
  --workshop-bg: repeating-linear-gradient(45deg,#e6ecf5,#e6ecf5 12px,#eef2f8 12px,#eef2f8 24px);
  --banner-bg: #fff7e0;
  --request-wash: linear-gradient(120deg,#e3edfb,#ffffff 72%);
  --hero-financial: linear-gradient(90deg,#e3e8ef,#eef2f7);
  --yellow-grad: linear-gradient(180deg,#f7d05a,var(--yellow-surface));
  --teal-grad: linear-gradient(180deg,#22c9b8,var(--teal-surface));
  --text: var(--fg);
}
:root[data-theme="dark"]{
  color-scheme: dark;
  --bg: #0f1117;
  --surface: #0d1219;
  --card: #141B21;
  --card2: #0f151b;
  --fg: #cdd9f0;
  --headline: #eaf1ff;
  --muted: #8093b0;
  --subtle: #76849e;
  --border: #1F2830;
  --blue: #3b82f6;
  --blue2: #3b82f6;
  --blue-dark: #2563EB;
  --blue-light: #60A5FA;
  --teal: #0D9488;
  --teal2: #22c9b8;
  --teal-light: #14B8A6;
  --yellow: #F4C430;
  --red: #E74C3C;
  --green: #4ADE80;
  --accent: #3b82f6;
  --join: #1f7a4d;
  --members: #2563EB;
  --ring: rgba(59,130,246,.35);
  --chip: rgba(59,130,246,.10);
  --glass: rgba(20,27,33,.72);
  --scrim: rgba(8,11,17,.66);
  --grid-line: rgba(59,130,246,.06);
  --tint-blue: rgba(59,130,246,.16);
  --tint-teal: rgba(13,148,136,.18);
  --tint-yellow: rgba(244,196,48,.16);
  --tint-red: rgba(231,76,60,.16);
  --yellow-surface: #F4C430;
  --teal-surface: #0D9488;
  --on-yellow: #2a2205;
  --on-teal: #04201d;
  --on-light: #12203b;
  --surface-modal: #05070b;
  --surface-deep: #0a0d12;
  --banner-border: #1F2830;
  --blue-hover: #1d4fd8;
  --on-brand: #ffffff;
  --surface-invert: #ffffff;
  --letterbox: #000000;
  --eyebrow: #9fb2d6;
  --shadow: 0 18px 50px rgba(0,0,0,.45);
  --shadow-card: 0 6px 20px -12px rgba(0,0,0,.55);
  --shadow-card-hover: 0 10px 28px -14px rgba(0,0,0,.65);
  --grad: linear-gradient(120deg,#3b82f6,#0D9488);
  --hero-overlay: linear-gradient(102deg, rgba(12,14,20,.96) 0%, rgba(13,16,23,.82) 42%, rgba(13,18,25,.5) 78%, rgba(13,18,25,.35) 100%);
  --workshop-bg: repeating-linear-gradient(45deg,#171e26,#171e26 12px,#141a22 12px,#141a22 24px);
  --banner-bg: repeating-linear-gradient(135deg,#191c12,#191c12 12px,#16180f 12px,#16180f 24px);
  --request-wash: linear-gradient(120deg,#12233f,var(--bg) 70%);
  --hero-financial: linear-gradient(90deg,#111826,#0d1524);
  --yellow-grad: linear-gradient(180deg,#f7d05a,var(--yellow-surface));
  --teal-grad: linear-gradient(180deg,#22c9b8,var(--teal-surface));
  --text: var(--fg);
}


/* base.css */
/* Brand fonts are loaded by a <link> in private/templates/seo-head.html, NOT by an @import
   here. A CSS @import is only valid before any other rule IN THE STYLESHEET, and Meteor
   concatenates every sheet into merged-stylesheets.css — so the import works only while
   this file happens to be concatenated first, and adding a stylesheet that sorts earlier
   would silently stop the brand fonts loading with nothing to explain it. */
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.55;-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}img{max-width:100%;display:block}
h1,h2,h3,.brandname{font-family:var(--display)}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 1.25rem}
.muted{color:var(--muted)}.small{font-size:.82rem}.row{display:flex;gap:.6rem;align-items:center;flex-wrap:wrap;margin-top:.8rem}

.btn{display:inline-flex;align-items:center;gap:.4rem;font-weight:700;font-size:.9rem;padding:.55rem 1rem;border-radius:10px;cursor:pointer;border:1px solid transparent;font-family:var(--sans)}
.btn-primary{background:var(--blue-dark);color:var(--on-brand)}.btn-primary:hover{filter:brightness(1.06)}
.btn-ghost{background:transparent;color:var(--fg);border-color:var(--border)}
.btn.small{padding:.35rem .7rem;font-size:.8rem}
input,select,textarea{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:9px;color:var(--fg);padding:.55rem .7rem;font-family:var(--sans);font-size:.95rem}
label{display:block;font-size:.8rem;font-weight:600;color:var(--muted);margin:.6rem 0 .2rem}
/* Was a hardcoded #fca5a5 — a light-pink error text that measures 1.7:1 on the light surface.
   Uses the themed token, which is AA-compliant in both modes. */
.mm-err{color:var(--red);font-size:.85rem;margin:.5rem 0 0}
.lang{width:auto;padding:.35rem .5rem;font-size:.85rem}
.badge{display:inline-block;margin-left:.4rem;font-size:.66rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;background:rgba(59,130,246,.18);color:var(--blue-light);border:1px solid rgba(59,130,246,.35);padding:.1rem .4rem;border-radius:999px;vertical-align:middle}
.badge.ok{background:rgba(74,222,128,.15);color:var(--green);border-color:rgba(74,222,128,.35)}

/* Public site */
.site-head{position:sticky;top:0;z-index:20;backdrop-filter:blur(10px);background:color-mix(in srgb,var(--bg) 82%,transparent);border-bottom:1px solid var(--border)}
.nav{display:flex;align-items:center;gap:1rem;height:60px}
.brand{display:flex;align-items:center;gap:.5rem;font-weight:800}
.oapp-mark{height:34px;width:34px;border-radius:8px}
.links{display:flex;gap:1rem;align-items:center;margin-left:auto;font-weight:600;color:var(--muted);font-size:.9rem}
.links a:hover{color:var(--fg)}.links .ext{color:var(--teal-light)}
.hero{padding:3.4rem 0;background:radial-gradient(1000px 460px at 78% -10%,rgba(59,130,246,.18),transparent 60%)}
.eyebrow{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:var(--teal-light)}
.hero h1{font-size:clamp(2.4rem,6vw,3.8rem);letter-spacing:-1.4px;margin:.8rem 0 .4rem}
.lede{color:var(--muted);max-width:96rem;font-size:1.1rem}
.cta-row{display:flex;gap:.6rem;flex-wrap:wrap;margin-top:1.3rem}
.frontiers{padding:2rem 0}.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem}
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.2rem}
.card h3{margin:.1rem 0 .3rem}.card p{margin:0;color:var(--muted);font-size:.92rem}
.membership{padding:2.6rem 0;text-align:center}.membership h2{font-size:clamp(1.6rem,3.5vw,2.2rem)}
.site-foot{border-top:1px solid var(--border);background:var(--surface);padding:1.4rem 0;color:var(--subtle);font-size:.85rem}
.site-foot .wrap{display:flex;justify-content:space-between;gap:.5rem;flex-wrap:wrap}
.admin-link{color:var(--muted)}.admin-link:hover{color:var(--fg)}
@media(max-width:820px){.cards{grid-template-columns:1fr}.links a:not(.btn):not(.ext){display:none}}

/* Membership modal */
.mm-backdrop{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:rgba(4,7,14,.82);backdrop-filter:blur(6px);padding:1.2rem}
.mm-panel{width:min(460px,100%);max-height:92vh;overflow:auto;background:var(--card);border:1px solid var(--border);border-radius:18px;padding:1.6rem;position:relative}
.mm-panel h3{margin:.1rem 0 .2rem;font-size:1.3rem}.mm-sub{margin:0 0 1rem;color:var(--muted);font-size:.9rem}
.mm-x{position:absolute;top:.7rem;right:.85rem;background:transparent;border:0;color:var(--muted);font-size:1.05rem;cursor:pointer}
.mm-toggle{display:flex;gap:.4rem;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:.3rem;margin-bottom:.4rem}
.mm-toggle button{flex:1;padding:.5rem;border:0;border-radius:9px;background:transparent;color:var(--muted);font-weight:700;font-size:.85rem;cursor:pointer;font-family:var(--sans)}
.mm-toggle button.on{background:var(--blue-dark);color:var(--on-brand)}
.mm-check{display:flex;gap:.5rem;align-items:flex-start;margin-top:.8rem;font-weight:500;color:var(--muted);font-size:.82rem}
.mm-check input{width:auto;margin-top:.15rem}
.mm-panel .btn{width:100%;justify-content:center;margin-top:1rem}
.mm-done{text-align:center;padding:.6rem 0}.mm-done .btn{max-width:200px;margin:1rem auto 0}

/* Cookie consent banner */
.cc-banner{position:fixed;left:0;right:0;bottom:0;z-index:70;background:var(--card);border-top:1px solid var(--border);box-shadow:0 -8px 24px rgba(4,7,14,.35)}
.cc-inner{max-width:var(--maxw);margin:0 auto;padding:1rem 1.25rem;display:flex;flex-wrap:wrap;gap:.8rem;align-items:center;justify-content:space-between}
.cc-text{margin:0;color:var(--muted);font-size:.85rem;flex:1 1 320px}
.cc-actions{display:flex;gap:.5rem;flex-wrap:wrap}
.cc-pref{flex:1 1 100%;margin:0}
.cc-actions .btn{width:auto}
@media(max-width:600px){.cc-inner{flex-direction:column;align-items:stretch}.cc-actions{justify-content:flex-end}}

/* Admin */
.admin-center{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1.5rem}
.auth-card{width:min(420px,100%);background:var(--card);border:1px solid var(--border);border-radius:18px;padding:1.8rem;text-align:center}
.auth-card.wide{width:min(480px,100%)}
.auth-card .oapp-mark{margin:0 auto .4rem}.auth-card .btn{width:100%;justify-content:center;margin-top:1rem}
.auth-card.wide .row .btn{width:auto}
.qr{background:var(--surface-invert);border-radius:12px;padding:.8rem;width:220px;margin:1rem auto;display:flex;justify-content:center}
.qr svg{width:190px;height:190px}
.secret{font-size:.8rem;color:var(--muted)}.secret code{color:var(--fg)}
.admin-shell{display:grid;grid-template-columns:220px 1fr;min-height:100vh}
.admin-nav{background:var(--surface);border-right:1px solid var(--border);padding:1rem;display:flex;flex-direction:column;gap:.4rem}
.admin-brand{display:flex;align-items:center;gap:.5rem;font-family:var(--display);margin-bottom:.8rem}
.admin-nav nav{display:flex;flex-direction:column;gap:.2rem}
.admin-nav a{padding:.5rem .7rem;border-radius:9px;color:var(--muted);font-weight:600}
.admin-nav a:hover{background:var(--card);color:var(--fg)}
.admin-nav a.active{background:var(--blue-dark);color:var(--on-brand)}
.admin-nav-group{display:flex;flex-direction:column;gap:.15rem;margin-bottom:.7rem}
.admin-nav-heading{padding:.2rem .7rem;text-transform:uppercase;letter-spacing:.06em;font-size:.68rem;font-weight:700;opacity:.65}
.admin-nav nav{overflow-y:auto}
.admin-foot{margin-top:auto;padding-top:1rem;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:.35rem}
.admin-main{padding:1.6rem 2rem;overflow:auto}
.admin-main h1{margin-top:0}
.tbl{width:100%;border-collapse:collapse;font-size:.9rem}
.tbl th,.tbl td{text-align:left;padding:.5rem .6rem;border-bottom:1px solid var(--border);vertical-align:middle}
.tbl th{color:var(--muted);font-size:.75rem;text-transform:uppercase;letter-spacing:.05em}
.tbl select{width:auto;padding:.3rem .5rem}
.form-narrow{max-width:520px}
.rolecells{display:flex;gap:.7rem;flex-wrap:wrap}
.rolechk{display:inline-flex;align-items:center;gap:.3rem;font-size:.85rem;font-weight:600;color:var(--fg);margin:0}
.rolechk input{width:auto}
.mail-wrap{display:grid;grid-template-columns:340px 1fr;gap:1rem;height:calc(100vh - 3.2rem)}
.mail-list{overflow:auto;border-right:1px solid var(--border);padding-right:.6rem}
.mail-toolbar{display:flex;align-items:center;justify-content:space-between}
.mail-item{display:block;width:100%;text-align:left;background:transparent;border:0;border-bottom:1px solid var(--border);padding:.6rem .4rem;cursor:pointer;color:var(--fg)}
.mail-item:hover{background:var(--card)}.mail-item.on{background:var(--card)}
.mail-item.unseen .mail-from{font-weight:800}
.mail-subj{font-size:.9rem}.mail-snip{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mail-read{overflow:auto}.mail-body{white-space:pre-wrap;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:.8rem;font-family:var(--mono);font-size:.82rem;max-height:36vh;overflow:auto}
.reply{min-height:9rem;font-family:var(--sans)}
.inline-label{display:flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:600;color:var(--muted);margin:0}
.inline-label input{width:auto}
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.8rem;margin:1rem 0}
.kpi-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:1rem 1.1rem}
.kpi-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}
.kpi-value{font-size:1.6rem;font-weight:800;color:var(--fg);margin-top:.2rem}
.kpi-sub{margin-top:.15rem}
.chart-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:1rem;margin:1rem 0}
.chart-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1rem 1.2rem}
.chart-card h3{margin:.1rem 0 .6rem;font-size:1rem}
.tbl-scroll{overflow-x:auto}
.tbl th.sortable{cursor:pointer;user-select:none}
.tbl th.sortable:hover{color:var(--fg)}
/* CMS */
.panel-head{display:flex;align-items:center;justify-content:space-between;gap:.6rem}
.cms-tabs{display:flex;gap:.4rem;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:.3rem;margin-bottom:1rem;width:fit-content}
.cms-tabs a{padding:.45rem 1rem;border-radius:9px;color:var(--muted);font-weight:700;font-size:.85rem}
.cms-tabs a.active{background:var(--blue-dark);color:var(--on-brand)}
.tbl-actions{display:flex;gap:.4rem;flex-wrap:wrap}
.badge.warn{background:rgba(244,196,48,.18);color:var(--yellow);border-color:rgba(244,196,48,.35)}
.badge.off{background:rgba(90,107,138,.18);color:var(--subtle);border-color:rgba(90,107,138,.35)}
.cms-editor{margin:.4rem 0 1rem}
.cms-editor .ql-toolbar{background:var(--surface);border-color:var(--border);border-radius:9px 9px 0 0}
.cms-editor .ql-container{background:var(--surface);border-color:var(--border);border-radius:0 0 9px 9px;font-family:var(--sans);font-size:.95rem}
.cms-editor .ql-editor{color:var(--fg);min-height:220px}
.cms-editor .ql-picker-label,.cms-editor .ql-picker-options{color:var(--fg)}
.cms-editor .ql-stroke{stroke:var(--muted)}
.cms-editor .ql-fill{fill:var(--muted)}
.job-status{margin-top:1.2rem;padding-top:.8rem;border-top:1px solid var(--border)}
.job-status h2{margin:0 0 .4rem;font-size:.95rem}

[dir="rtl"] .admin-shell{grid-template-columns:1fr 220px}
[dir="rtl"] .admin-nav{border-right:0;border-left:1px solid var(--border)}
@media(max-width:820px){.admin-shell{grid-template-columns:1fr}.mail-wrap{grid-template-columns:1fr;height:auto}}

/* THE TOKEN BRIDGE MOVED.

   private/branding/styles.css and mobile.css came from the upstream template and reference a
   different token vocabulary (--color-*, --spacing-*, --shadow-*, --transition-*) than this
   app's palette (--bg, --fg, --blue, ...). That bridge now lives in private/config/palette.json
   under `aliases` and is emitted into private/branding/palette.css, so there is ONE declaration site for
   every token rather than one here and one per page template. Do not re-add tokens to this
   file — scripts/gen-palette.mjs fails the gate if any file but palette.css declares one. */

/* ---------------------------------------------------------------------------------------
   BRAND TYPOGRAPHY WINS OVER THE VENDOR SHEET.

   PublicLayout appends Bootstrap at RUNTIME (useVendorCss injects a <link>), so the vendor
   stylesheet always lands after this bundled one and wins every tie. Bootstrap 3 sets
   `body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif }`, which silently
   replaced Inter on every page inside the shared layout — `/` looked right only because the
   home design sits outside it and never loads Bootstrap.

   Fixed with SPECIFICITY rather than order, because load order is not ours to control:
   `html body` (0,0,2) beats `body` (0,0,1) whenever the vendor sheet arrives. Do not "simplify"
   these back to bare selectors.
   --------------------------------------------------------------------------------------- */
html body { font-family: var(--sans); }
html h1, html h2, html h3, html h4, html .brandname { font-family: var(--display); }


/* styles.css */
/* Imported by client/main.jsx. It genuinely is now — it was not for a long time, and 203
   rules including the whole Lauren widget and the menu primitives were dead. */

/* The TOKENS for this sheet live in private/branding/base.css (see the token bridge there), NOT here.
   This file arrived from the upstream template with its own :root palette — a light background
   and a red --color-primary — and because it is imported after main.css that palette WON,
   repainting the brand red the moment this stylesheet was first loaded. Site identity has one
   source; a second one here is the same class of mistake as a second settings store.
   Only --spacing-xs remains, because the bridge does not define it. */


/* ============ Reset & Base Styles ============ */
/* `box-sizing` only. The template's reset also zeroed margin and padding on EVERY element,
   which had never been in effect in this app — importing it silently restyled every page that
   relies on default spacing. main.css already sets box-sizing globally, so this is a no-op kept
   for clarity rather than a second declaration of intent. */
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    sans-serif;
  background-color: var(--color-background);
  color: var(--color-foreground);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  text-decoration: none;
  color: inherit;
}

/* ============ Layout ============ */
.page {
  min-height: 100vh;
  background-color: var(--color-background);
}

.container {
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  padding-left: var(--spacing-md);
  padding-right: var(--spacing-md);
}

@media (min-width: 768px) {
  .container {
    padding-left: var(--spacing-lg);
    padding-right: var(--spacing-lg);
  }
}

/* ============ Header / Navigation ============ */
.header {
  border-bottom: 1px solid var(--color-border);
  background-color: var(--color-card);
  border-radius: var(--radius);
}

.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 4rem;
}

.logo-container {
  display: flex;
  align-items: center;
  gap: var(--spacing-sm);
}

.logo {
  width: 4rem;
  height: 4rem;
}

.logo-text {
  font-weight: 600;
  color: var(--color-foreground);
  display: none;
}

@media (min-width: 640px) {
  .logo-text {
    display: inline;
  }
}

.page-title {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--color-foreground);
  letter-spacing: -0.025em;
}

@media (min-width: 768px) {
  .page-title {
    font-size: 1.5rem;
  }
}

.nav-link {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-link);
  transition: opacity var(--transition-fast);
}

.nav-link:hover {
  opacity: 0.8;
}

/* ============ Main Content ============ */
.main {
  padding-top: var(--spacing-xl);
  padding-bottom: var(--spacing-xl);
}

@media (min-width: 768px) {
  .main {
    padding-top: var(--spacing-2xl);
    padding-bottom: var(--spacing-2xl);
  }
}

/* ============ Card Component ============ */
.card {
  background-color: var(--color-card);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  border: 1px solid var(--color-border);
}

/* ============ Counter Section ============ */
.counter-card {
  padding: var(--spacing-lg);
  margin-bottom: 2.5rem;
}

@media (min-width: 768px) {
  .counter-card {
    padding: var(--spacing-xl);
    margin-bottom: var(--spacing-2xl);
  }
}

.counter-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-md);
}

@media (min-width: 640px) {
  .counter-content {
    flex-direction: row;
  }
}

.counter-text {
  color: var(--color-muted);
  text-align: center;
}

@media (min-width: 640px) {
  .counter-text {
    text-align: left;
  }
}

.counter-value {
  font-weight: 600;
  color: var(--color-foreground);
}

/* ============ Button Component ============ */
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 120px;
  padding: 0.625rem 1.5rem;
  font-size: 0.875rem;
  font-weight: 500;
  font-family: inherit;
  color: white;
  background-color: var(--color-primary);
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color var(--transition-normal),
    transform var(--transition-fast);
}

.button:hover {
  background-color: var(--color-primary-hover);
}

.button:active {
  transform: scale(0.98);
}

.button:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

/* ============ Resources Section ============ */
.section-title {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--color-foreground);
  margin-bottom: var(--spacing-lg);
  letter-spacing: -0.025em;
}

.resources-grid {
  list-style-type: none;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--spacing-md);
}

@media (min-width: 640px) {
  .resources-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* ============ Resource Card ============ */
.resource-link {
  display: block;
}

.resource-card {
  padding: 1.25rem;
  transition: box-shadow var(--transition-slow),
    transform var(--transition-slow);
}

.resource-card:hover {
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-2px);
}

.resource-content {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.resource-title {
  font-weight: 500;
  color: var(--color-foreground);
  transition: color var(--transition-fast);
}

.resource-link:hover .resource-title {
  color: var(--color-link);
}

.resource-icon {
  width: 1rem;
  height: 1rem;
  color: var(--color-muted);
  opacity: 0;
  transition: opacity var(--transition-fast), color var(--transition-fast);
}

.resource-link:hover .resource-icon {
  opacity: 1;
  color: var(--color-link);
}

/* ============ Generic site shell (config-driven) ============ */
.nav-links {
  display: flex;
  align-items: center;
  gap: var(--spacing-lg);
  flex-wrap: wrap;
}

.hero {
  padding: var(--spacing-2xl) 0;
  text-align: center;
}

.hero-title {
  font-size: 2.5rem;
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--color-foreground);
  margin-bottom: var(--spacing-md);
}

.hero-subtitle {
  font-size: 1.125rem;
  color: var(--color-muted);
  max-width: 42rem;
  margin: 0 auto var(--spacing-xl);
}

.content-page {
  max-width: 48rem;
  margin: 0 auto;
  padding: var(--spacing-xl) 0;
}

.content-page h1 {
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: -0.025em;
  margin-bottom: var(--spacing-md);
}

.content-page .lead {
  font-size: 1.125rem;
  color: var(--color-muted);
  margin-bottom: var(--spacing-lg);
}

.content-page p {
  margin-bottom: var(--spacing-md);
}

.content-page .feature-list {
  list-style: disc;
  padding-left: var(--spacing-lg);
  display: grid;
  gap: var(--spacing-sm);
}

.content-page code {
  background: var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0.1rem 0.35rem;
  font-size: 0.875em;
}

.footer {
  border-top: 1px solid var(--color-border);
  padding: var(--spacing-xl) var(--spacing-md);
  color: var(--color-muted);
  font-size: 0.875rem;
  text-align: center;
}

.loading {
  padding: var(--spacing-2xl);
  text-align: center;
  color: var(--color-muted);
}

/* ============ Public contact form ============ */
.contact-form { display: grid; gap: var(--spacing-md); max-width: 36rem; }
.contact-form .contact-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); }
.contact-form input, .contact-form textarea {
  width: 100%; padding: 0.6rem 0.75rem; font: inherit;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-card);
}
.contact-form .contact-error { color: var(--color-link); }

/* ============ Consent banner ============ */
.consent-banner {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-md);
  flex-wrap: wrap; padding: var(--spacing-md) var(--spacing-lg);
  background: var(--color-card); border-top: 1px solid var(--color-border);
  box-shadow: 0 -4px 12px hsl(220 20% 15% / 0.06); font-size: 0.9rem;
}
.consent-banner p { margin: 0; color: var(--color-muted); }
.consent-actions { display: flex; gap: var(--spacing-sm); }
.button-ghost { background: transparent; color: var(--color-foreground); border: 1px solid var(--color-border); }
.button-ghost:hover { background: var(--color-background); }

/* ============ Lauren LiveChat ============ */
.livechat-fab { position: fixed; right: 20px; bottom: 20px; z-index: 1001; width: 52px; height: 52px; border-radius: 50%; border: none; background: var(--color-primary); color: var(--on-brand); font-size: 1.4rem; cursor: pointer; box-shadow: var(--shadow-card-hover); transition: transform 0.15s ease; }
.livechat-fab:hover { transform: scale(1.06); }
/* The OAPP robot mark as Lauren's face. `display:block` + auto margins centre it inside the
   round button; without them the img sits on the text baseline and reads as off-centre. */
.livechat-fab .livechat-robot { display: block; margin: 0 auto; width: 30px; height: 30px; }
.livechat-fab { display: grid; place-items: center; }
.livechat-fab.pulse { animation: livechat-pulse 2.4s ease-in-out infinite; }
@keyframes livechat-pulse {
  0%, 100% { box-shadow: var(--shadow-card-hover), 0 0 0 0 color-mix(in srgb, var(--color-primary) 45%, transparent); }
  50% { box-shadow: var(--shadow-card-hover), 0 0 0 9px color-mix(in srgb, var(--color-primary) 0%, transparent); }
}
.livechat-panel { position: fixed; right: 20px; bottom: 84px; z-index: 1001; width: 340px; max-width: calc(100vw - 40px); height: 460px; max-height: 70vh; display: flex; flex-direction: column; background: var(--color-card); border: 1px solid var(--color-border); border-radius: var(--radius); box-shadow: var(--shadow-card-hover); overflow: hidden; animation: livechat-panel-in 0.18s ease-out; }
@keyframes livechat-panel-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.livechat-head { padding: var(--spacing-md); font-weight: 600; border-bottom: 1px solid var(--color-border); }
.livechat-body { flex: 1; overflow-y: auto; padding: var(--spacing-md); display: grid; gap: var(--spacing-sm); align-content: start; }
.livechat-msg { padding: 0.5rem 0.7rem; border-radius: var(--radius-sm); font-size: 0.9rem; max-width: 90%; }
.livechat-msg.you { background: var(--color-primary); color: var(--on-brand); justify-self: end; }
.livechat-msg.lauren { background: var(--color-background); }
.livechat-src { margin-top: 0.3rem; display: flex; gap: 0.5rem; flex-wrap: wrap; font-size: 0.75rem; }
.livechat-src a { color: var(--color-link); }
.livechat-input { display: flex; gap: 0.4rem; padding: var(--spacing-sm); border-top: 1px solid var(--color-border); }
.livechat-input input { flex: 1; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); font: inherit; }

/* Typing indicator — three staggered dots instead of a plain "…" string. */
.livechat-typing { display: flex; gap: 4px; align-items: center; padding-block: 0.6rem; }
.livechat-typing span { width: 6px; height: 6px; border-radius: 50%; background: var(--color-muted); animation: livechat-typing-bounce 1.1s infinite ease-in-out; }
.livechat-typing span:nth-child(2) { animation-delay: 0.15s; }
.livechat-typing span:nth-child(3) { animation-delay: 0.3s; }
@keyframes livechat-typing-bounce { 0%, 60%, 100% { opacity: 0.35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-3px); } }

/* Proactive nudge — a small greeting callout that appears once per tab session. */
.livechat-nudge { position: fixed; right: 20px; bottom: 82px; z-index: 1000; max-width: min(260px, calc(100vw - 40px)); display: flex; align-items: flex-start; gap: 6px; background: var(--color-card); border: 1px solid var(--color-border); border-radius: var(--radius); box-shadow: var(--shadow-card-hover); padding: 10px 12px; animation: livechat-panel-in 0.2s ease-out; }
.livechat-nudge-text { background: none; border: none; padding: 0; text-align: left; font: inherit; font-size: 0.85rem; color: var(--color-foreground); cursor: pointer; line-height: 1.35; }
.livechat-nudge-close { flex-shrink: 0; background: none; border: none; padding: 2px; margin-inline-start: auto; color: var(--color-muted); cursor: pointer; font-size: 0.75rem; line-height: 1; }
.livechat-nudge-close:hover { color: var(--color-foreground); }

/* Position variant — mirrors FAB/panel/nudge to the bottom-left. */
.livechat-root.pos-left .livechat-fab,
.livechat-root.pos-left .livechat-panel,
.livechat-root.pos-left .livechat-nudge { right: auto; left: 20px; }

@media (prefers-reduced-motion: reduce) {
  .livechat-fab.pulse, .livechat-panel, .livechat-nudge, .livechat-typing span { animation: none; }
}

/* ============ Visual editor — block rendering (public + preview) ============ */
.block { margin: 0 0 var(--spacing-lg); }
.block:last-child { margin-bottom: 0; }
.block-hero { text-align: left; padding: var(--spacing-lg) 0; }
.block-hero.align-center { text-align: center; }
.block-hero.align-right { text-align: right; }
.block-hero-title { font-size: 2rem; margin: 0 0 0.5rem; }
.block-hero-sub { font-size: 1.15rem; color: var(--color-muted); margin: 0 0 1rem; }
.block-richtext :is(h1, h2, h3) { margin-top: 1.2rem; }
/* Columns: a responsive flex row of column blocks (each holds nested children) */
.block-columns { display: flex; gap: var(--spacing-md); }
.block-columns.gap-none { gap: 0; }
.block-columns.gap-sm { gap: var(--spacing-sm); }
.block-columns.gap-lg { gap: var(--spacing-lg); }
.block-columns.valign-top { align-items: flex-start; }
.block-columns.valign-center { align-items: center; }
.block-columns.valign-stretch { align-items: stretch; }
.block-column { flex: 1 1 0; min-width: 0; }
.block-column.span-2 { flex-grow: 2; }
.block-column.span-3 { flex-grow: 3; }
.block-column.span-auto { flex: 0 1 auto; }
@media (max-width: 700px) { .block-columns { flex-direction: column; } .block-column { flex: 1 1 auto; } }
.block-image { margin: 0 0 1.5rem; }
.block-image img { max-width: 100%; height: auto; border-radius: var(--radius-sm); }
.block-image.width-full img { width: 100%; }
.block-image.width-wide img { width: 100%; max-width: 900px; }
.block-image figcaption { font-size: 0.85rem; color: var(--color-muted); margin-top: 0.4rem; }
.block-cta { text-align: center; padding: var(--spacing-lg); border: 1px solid var(--color-border); border-radius: var(--radius); background: var(--color-background); }
.block-cta-title { margin: 0 0 0.5rem; }
.block-embed { margin: var(--spacing-md) 0; }
.block-embed iframe { width: 100%; border: 0; border-radius: var(--radius-sm); }
.block-embed.embed-video iframe { aspect-ratio: 16 / 9; }
.block-embed.embed-audio iframe { height: 166px; }
.block-embed.embed-spotify iframe { height: 352px; }
.block-embed.embed-link a { display: inline-block; padding: 0.6rem 1rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); word-break: break-all; }
/* Social share — privacy-friendly share-intent buttons */
.block-share { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: var(--spacing-sm) 0; }
.share-heading { margin: 0 0 0.5rem; font-weight: 600; }
.share-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 40px; min-height: 40px; padding: 0 0.7rem; border-radius: var(--radius-sm); border: 1px solid var(--color-border); background: var(--color-background); color: inherit; font-weight: 700; text-decoration: none; cursor: pointer; transition: transform 0.08s ease, filter 0.15s ease; }
.share-btn:hover { transform: translateY(-1px); filter: brightness(1.05); }
.share-x { background: var(--brand-x); color: var(--on-brand); border-color: var(--brand-x); }
.share-facebook { background: var(--brand-facebook); color: var(--on-brand); border-color: var(--brand-facebook); }
.share-linkedin { background: var(--brand-linkedin); color: var(--on-brand); border-color: var(--brand-linkedin); }
.share-reddit { background: var(--brand-reddit); color: var(--on-light); border-color: var(--brand-reddit); }
/* A DARK label here, not white. WhatsApp green is light: white on it measures 1.98:1,
   below even the 3:1 floor WCAG 1.4.11 sets for a UI component, so the label was
   effectively unreadable. The literals hid that — naming the colour is what made it
   measurable. The brand green is preserved, which is what the recognition depends on;
   only the label flips. tests/contrast.tests.js now holds every brand pill to 3:1. */
.share-whatsapp { background: var(--brand-whatsapp); color: var(--on-light); border-color: var(--brand-whatsapp); }
/* Social profile follow-links */
.block-social { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: var(--spacing-sm) 0; }
.social-heading { margin: 0 0 0.5rem; font-weight: 600; }
.social-btn { display: inline-flex; align-items: center; justify-content: center; min-width: 40px; min-height: 40px; padding: 0 0.7rem; border-radius: var(--radius-sm); border: 1px solid var(--color-border); background: var(--color-background); color: inherit; font-weight: 700; text-decoration: none; transition: transform 0.08s ease, filter 0.15s ease; }
.social-btn:hover { transform: translateY(-1px); filter: brightness(1.05); }
.social-x, .social-tiktok { background: var(--brand-x); color: var(--on-brand); border-color: var(--brand-x); }
.social-instagram { background: var(--brand-instagram); color: var(--on-brand); border-color: var(--brand-instagram); }
.social-github { background: var(--brand-github); color: var(--on-brand); border-color: var(--brand-github); }
.social-linkedin { background: var(--brand-linkedin); color: var(--on-brand); border-color: var(--brand-linkedin); }
.social-youtube { background: var(--brand-youtube); color: var(--on-light); border-color: var(--brand-youtube); }
.social-facebook { background: var(--brand-facebook); color: var(--on-brand); border-color: var(--brand-facebook); }
.social-bluesky { background: var(--brand-bluesky); color: var(--on-light); border-color: var(--brand-bluesky); }
.social-mastodon { background: var(--brand-mastodon); color: var(--on-brand); border-color: var(--brand-mastodon); }
/* Reusable customizable container */
.block-container { margin: var(--spacing-md) 0; }
.block-container.width-wide { max-width: 900px; margin-left: auto; margin-right: auto; }
.block-container.width-full { max-width: none; }
.block-container.pad-none { padding: 0; }
.block-container.pad-sm { padding: var(--spacing-sm); }
.block-container.pad-md { padding: var(--spacing-md); }
.block-container.pad-lg { padding: var(--spacing-lg); }
.block-container.pad-xl { padding: var(--spacing-xl); }
.block-container.radius-sm { border-radius: var(--radius-sm); }
.block-container.radius-md { border-radius: var(--radius); }
.block-container.radius-lg { border-radius: calc(var(--radius) * 2); }
.block-container.radius-full { border-radius: 999px; }
.block-container.border-thin { border: 1px solid var(--color-border); }
.block-container.border-thick { border: 2px solid var(--color-border); }
.block-container.shadow-sm { box-shadow: 0 1px 3px hsl(220 20% 10% / 0.1); }
.block-container.shadow-md { box-shadow: 0 4px 12px hsl(220 20% 10% / 0.12); }
.block-container.shadow-lg { box-shadow: 0 12px 32px hsl(220 20% 10% / 0.16); }
.block-container.align-center { text-align: center; }
.block-container.align-right { text-align: right; }
.block-form { padding: var(--spacing-md) 0; }
/* Component instance — transparent wrapper; the saved master's blocks render in place */
.block-component { display: contents; }

/* CMS web-facing form blocks + event ticket (P2) */
.block-form-el { display: flex; flex-direction: column; gap: 10px; max-width: 480px; }
.block-form-el input, .block-form-el textarea { padding: 10px 12px; border: 1px solid var(--color-border); border-radius: 8px; font: inherit; }
.block-form-el textarea { min-height: 100px; resize: vertical; }
.block-form-el button { align-self: flex-start; padding: 10px 18px; border: none; border-radius: 8px; background: var(--color-primary); color: var(--on-brand); font: inherit; cursor: pointer; }
.block-form-done { color: var(--color-primary); font-weight: 600; }
.block-form-err { color: var(--red); }
.block-ticket { display: flex; justify-content: center; padding: 16px; }
.ticket-card { border: 2px dashed var(--color-primary); border-radius: 14px; padding: 24px 32px; text-align: center; max-width: 360px; background: var(--color-card); }
.ticket-title { font-size: 1.4rem; font-weight: 700; }
.ticket-sub { color: var(--color-muted); margin-top: 4px; }
.ticket-perf { font-size: 2.5rem; margin: 12px 0; }
.ticket-note { font-size: 0.85rem; color: var(--color-muted); margin-top: 6px; }

/* ---------------------------------------------------------------------------
   Menu primitives — {{menu-top}} / {{menu-sub}} and the per-page CMS option.

   The RENDERER emits only semantic class tags and never inline styles, so the
   format lives here and a site can restyle or replace these rules wholesale.
   Orientation is a class, not a different markup shape, so the same menu can be
   flipped horizontal/vertical without re-rendering.

   Structure:
     nav.be-menu.be-menu--{top|sub}.be-menu--{horizontal|vertical}[.custom-class]
       ul.be-menu__list
         li.be-menu__item.be-menu__item--depth-N[.is-active][.is-ancestor]
           a.be-menu__link[aria-current=page]
   --------------------------------------------------------------------------- */
.be-menu__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: 0.25rem;
}
.be-menu--horizontal .be-menu__list { flex-direction: row; flex-wrap: wrap; align-items: center; }
.be-menu--vertical .be-menu__list { flex-direction: column; align-items: stretch; }

.be-menu__link {
  display: block;
  padding: 0.45rem 0.75rem;
  text-decoration: none;
  border-radius: 4px;
  color: inherit;
}
.be-menu__link:hover,
.be-menu__link:focus-visible { background: rgba(127, 127, 127, 0.14); }

/* Current page, and an ancestor of it, are distinguished — a subnav is much harder
   to orient in when the active trail is not visible. */
.be-menu__item.is-active > .be-menu__link { font-weight: 600; background: rgba(37, 99, 235, 0.12); }
.be-menu__item.is-ancestor > .be-menu__link { font-weight: 600; }

/* Depth indent applies only when stacked; indenting a horizontal bar is meaningless. */
.be-menu--vertical .be-menu__item--depth-1 > .be-menu__link { padding-left: 1.6rem; }
.be-menu--vertical .be-menu__item--depth-2 > .be-menu__link { padding-left: 2.5rem; }
.be-menu--vertical .be-menu__item--depth-3 > .be-menu__link { padding-left: 3.4rem; }

/* A top bar should scroll rather than wrap into a tall block on a narrow screen. */
@media (max-width: 640px) {
  .be-menu--top.be-menu--horizontal .be-menu__list {
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
}

/* ============ Theme toggle ============ */
/* Fixed top-right, above page chrome but BELOW the chat panel (z 1001) so it can never cover
   an open conversation. */
.theme-toggle {
  position: fixed; top: 14px; right: 16px; z-index: 900;
  width: 36px; height: 36px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--color-card); color: var(--color-foreground);
  border: 1px solid var(--color-border); cursor: pointer;
  font-size: 1rem; line-height: 1;
  box-shadow: var(--shadow-card);
  transition: transform var(--transition-fast), background var(--transition-fast);
}
.theme-toggle:hover { transform: scale(1.08); }
.theme-toggle:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
/* The home design has its own fixed header; nudge clear of it on wide screens. */
@media (min-width: 900px) { .theme-toggle { top: 18px; right: 20px; } }

/* INLINE in the header bar — the normal case. `position: static` undoes the fixed fallback
   above, which otherwise floated the switch over the nav bar's own content. */
.theme-toggle--inline { position: static; top: auto; right: auto; width: 34px; height: 34px;
  box-shadow: none; background: transparent; }
.theme-toggle--inline:hover { background: var(--color-card); transform: none; }
/* ORDER, not DOM order. The theme switch and the hamburger sit EARLIER in the markup than the
   nav panel, because on a phone they must be in the bar while the panel is off-canvas — but on
   desktop they read better after the links. `order` resolves that without a second markup
   variant: brand, panel (links + actions), switch, burger.

   `margin-left` is NOT used to push them right any more; the panel claims the free space (see
   `.hdr-panel`), so adding or removing the switch cannot change where the nav sits. */
.hdr-theme { display: inline-flex; align-items: center; order: 3; margin-left: .35rem; }

/* ============ Shared site chrome (blocks/site-header.html, blocks/site-footer.html) ============
   Lifted out of the home template's own <style> so the SAME header and footer render correctly on
   every page. While that CSS lived only in the home page's own template, any other page
   including those blocks got the right markup and none of the styling.

   This is also where the {{menu-top}} / {{menu-sub}} primitives finally get styled: their rules
   were written into this stylesheet when the feature was built, and this stylesheet was imported
   nowhere, so the menus have never actually been styled. */

header.site { position: sticky; top: 0; z-index: 40; background: var(--color-background);
  border-bottom: 1px solid var(--color-border); }
@supports (backdrop-filter: blur(10px)) {
  /* Translucent only where it is supported; otherwise the solid background above stands, rather
     than text scrolling under a transparent bar.

     THE BLUR IS ON A PSEUDO-ELEMENT, NOT ON THE HEADER. `backdrop-filter` on an element makes
     it a containing block for `position: fixed` descendants — so with it on `header.site`, the
     drawer and its scrim were laid out inside the 70px bar instead of the viewport. The scrim
     looked right (the bar starts at the top of the screen) and was only ~70px tall, so a tap
     below the header hit the page, and "tap outside to close" silently did nothing.

     Putting the filter on `::before` keeps the frosted bar and leaves the header an ordinary
     containing block. `isolation` pins the pseudo-element behind the bar's own content without
     it escaping behind the page. */
  header.site { background: transparent; isolation: isolate; }
  header.site::before {
    content: ''; position: absolute; inset: 0; z-index: -1;
    background: color-mix(in srgb, var(--color-background) 88%, transparent);
    backdrop-filter: blur(10px);
  }
}
/* THE HEADER USES THE WHOLE VIEWPORT WIDTH; the 1180px measure is for READING, not for chrome.
   It used to share the article max-width, which threw away ~260px of usable bar on a 1440px
   screen — and because `.be-menu__list` wraps, that missing width did not overflow, it silently
   pushed "About" onto a second row and grew the header from 66px to 154px with the caret sitting
   UNDER its own link. A navigation bar has no reason to be measured for prose. */
header.site .wrap { display: flex; align-items: center; gap: 1rem;
  padding: .8rem clamp(1.1rem, 3vw, 2.2rem); max-width: none; margin: 0; width: 100%; }
.brand { display: flex; align-items: center; gap: .7rem; text-decoration: none; flex: 0 0 auto; }
.brand img.mark { height: 38px; width: 38px; }
/* Larger now that the second mark is gone — the wordmark IS the identity in the header. */
.brand img.wordmark { height: 42px; width: auto; }
/* Exactly ONE wordmark is visible. Both are in the markup because a <picture> media query
   follows the OPERATING SYSTEM and would ignore an explicit light/dark choice, and srcset cannot
   be driven from CSS — so visibility is. These rules were originally written inside the home
   template's own <style>, which meant the shared header showed BOTH marks on every other page. */
.wordmark--light { display: none; }
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]):not([data-theme="light"]) .wordmark--dark { display: none; }
  :root:not([data-theme="dark"]):not([data-theme="light"]) .wordmark--light { display: block; }
}
:root[data-theme="light"] .wordmark--dark { display: none; }
:root[data-theme="light"] .wordmark--light { display: block; }
.hdr-cta { display: flex; align-items: center; gap: .5rem; }
.hdr-cta small { color: var(--color-muted); font-size: .68rem; letter-spacing: .03em; }

/* The top menu primitive, styled to match the header it sits in. It no longer claims the free
   space itself — `.hdr-panel` does, so the links and the buttons move as one group. */
.be-menu--top { display: flex; align-items: center; gap: 1.5rem; }
.be-menu--top ul { list-style: none; margin: 0; padding: 0; display: flex; align-items: center; gap: 1.5rem; }
/* NEVER WRAP IN THE BAR. `.be-menu--horizontal .be-menu__list` sets `flex-wrap: wrap` for the
   generic primitive, which is right for a menu in a page body and wrong for a fixed-height bar:
   a wrapped item does not look like an overflow, it looks like a broken header, and a `has-children`
   item wraps its own caret onto a second line underneath the link. Above the drawer breakpoint the
   bar is a single row; below it, the hamburger owns the navigation. Those are the only two states. */
header.site .be-menu--top .be-menu__list { flex-wrap: nowrap; }
header.site .be-menu--top .be-menu__item { white-space: nowrap; }
/* A `has-children` item is ONE line: the link and its caret side by side. Without this the caret
   is a separate flow item that can break under the link, which is exactly what was reported. */
header.site .be-menu__item.has-children { display: inline-flex; align-items: center; gap: .15rem; }
.be-menu--top a { color: var(--color-muted); text-decoration: none; font-size: .9rem; font-weight: 600; }
.be-menu--top a:hover, .be-menu--top .is-active > a { color: var(--color-foreground); }
/* The chrome's vertical menu. `align-items` is NOT restated here: this rule and the primitive's
   own (.be-menu--vertical .be-menu__list, further up) disagreed — stretch there, flex-start here,
   and this one won by source order. So the primitive and the chrome held different opinions about
   the same element, and which applied depended on which stylesheet loaded last. */
.be-menu--vertical, .be-menu--vertical ul { flex-direction: column; gap: .6rem; }

footer.site { border-top: 1px solid var(--color-border); background: var(--surface, var(--color-card)); }
footer.site .wrap { max-width: 1180px; margin: 0 auto; padding: 0 clamp(1.1rem, 4vw, 2.6rem); }
footer.site .grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 2rem;
  padding-top: 3rem; padding-bottom: 2rem; }
footer.site .about img { height: 30px; margin-bottom: 1rem; }
footer.site .about p { color: var(--color-muted); font-size: .88rem; max-width: 38ch; }
footer.site .about .tag { color: var(--yellow, var(--color-primary)); font-weight: 600; font-size: .82rem; margin-top: .8rem; }
footer.site h4 { font-family: var(--display); font-size: .72rem; letter-spacing: .14em;
  text-transform: uppercase; color: var(--color-muted); margin: 0 0 1rem; }
footer.site ul { list-style: none; padding: 0; margin: 0; display: grid; gap: .6rem; }
footer.site a { color: var(--color-foreground); text-decoration: none; font-size: .9rem; }
footer.site a:hover { color: var(--color-primary-hover); }
footer.site .eco span { display: block; font-size: .72rem; color: var(--color-muted); }
footer.site .bottom { border-top: 1px solid var(--color-border); padding: 1.2rem 0; display: flex;
  flex-wrap: wrap; gap: .6rem; justify-content: space-between; color: var(--color-muted); font-size: .82rem; }

/* The footer's Explore column renders {{menu-top-vertical}}, which emits the same .be-menu
   markup as the header. Reset the header's horizontal rules for it. */
footer.site .explore .be-menu, footer.site .explore .be-menu ul {
  display: grid; gap: .6rem; margin: 0; padding: 0; list-style: none;
}
footer.site .explore .be-menu a { color: var(--color-foreground); font-size: .9rem; font-weight: 400; }
footer.site .explore .be-menu a:hover { color: var(--color-primary-hover); }

@media (max-width: 860px) {
  footer.site .grid { grid-template-columns: 1fr; }
}

/* ============ Attestation receipt (one line, details on demand) ============ */
.attest { margin: .6rem 0 0; font-size: .74rem; color: var(--color-muted); }
.attest-summary { display: flex; align-items: center; gap: .45rem; cursor: pointer;
  list-style: none; padding: .15rem 0; }
/* The default disclosure triangle is removed in both engines; the dot carries the meaning. */
.attest-summary::-webkit-details-marker { display: none; }
.attest-summary::marker { content: ''; }
.attest-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--color-primary);
  flex: 0 0 auto; }
.attest-dot--off { background: var(--color-muted); }
.attest-fp { font-family: var(--mono); font-size: .72rem; opacity: .8; }
.attest-detail { padding: .5rem 0 .2rem; }
.attest-detail dl { display: grid; grid-template-columns: auto 1fr; gap: .2rem .7rem; margin: 0; }
.attest-detail dt { color: var(--color-muted); }
.attest-detail dd { margin: 0; }
/* `break-all`, because a signature has no spaces to wrap at and would otherwise force the whole
   page to scroll sideways on a phone. */
.attest-detail code { font-family: var(--mono); font-size: .72rem;
  word-break: break-all; }
.attest-summary:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

/* Rollover on pointer devices, in ADDITION to click and keyboard. `any-hover` rather than
   `hover` so a hybrid laptop with a touchscreen still gets it. Touch-only devices keep the tap. */
@media (any-hover: hover) {
  .attest:hover > .attest-detail { display: block; }
  .attest:not([open]):hover { /* the detail is revealed by the rule above */ }
}

/* ============ Clearance for the fixed chrome ============
   The chat launcher is fixed bottom-right and the theme toggle fixed top-right, so both sit OVER
   page content at the extremes of the scroll. At the bottom of a page that meant the launcher
   covered the last footer line — the attestation receipt and the privacy controls — which is not
   just untidy: those elements became unclickable, and a test trying to open the receipt timed out
   waiting for an element that was visible but obscured.

   Reserve the space instead of raising z-index: putting footer content above a floating action
   button only moves the collision. */
footer.site, .footer { padding-bottom: 76px; }
@media (min-width: 900px) { footer.site, .footer { padding-bottom: 88px; } }

/* ============ Footer legal controls ============
   Privacy Policy / Cookie settings / Do Not Sell are a link and two buttons, so they inherited
   three different sets of defaults — different colour, weight, underline and baseline. They are
   one row of equivalent controls and now look like it. The <button>s stay buttons because they
   dispatch events into React state rather than navigating. */
.footer-legal { display: flex; flex-wrap: wrap; align-items: center; gap: .35rem 1rem;
  margin: .5rem 0 0; font-size: .74rem; }
.footer-legal > *, .footer-legal .link-btn {
  color: var(--color-muted); font: inherit; background: none; border: 0; padding: 0;
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
}
.footer-legal > *:hover, .footer-legal .link-btn:hover { color: var(--color-foreground); }
.footer-legal .link-btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

/* ============ Public forms, mobile-first ============
   Membership, member sign-in, the sponsorship enquiry and "get in touch" are the only forms a
   visitor ever sees, and they were inheriting whatever the surrounding page happened to set.
   One set of rules so they behave the same from a phone up.

   `max-width` in ch rather than px: a form field wider than about 60 characters is harder to
   read, independent of device pixel ratio. */
.form-narrow { display: grid; gap: .85rem; max-width: 60ch; }
.form-narrow label { display: grid; gap: .3rem; font-size: .88rem; color: var(--color-muted); }
.form-narrow input, .form-narrow select, .form-narrow textarea {
  width: 100%; padding: .6rem .7rem; border-radius: var(--radius-sm);
  border: 1px solid var(--color-border); background: var(--color-card);
  color: var(--color-foreground);
  /* 16px minimum: iOS Safari ZOOMS the whole page when a focused field is smaller, which looks
     like the layout breaking on focus. */
  font: inherit; font-size: max(16px, .95rem);
}
.form-narrow textarea { min-height: 7rem; resize: vertical; }

/* A one-time code is not a text field.
   Six digits, centred, spaced and large, so the value reads as a code and a mistyped digit is
   obvious. Formatting follows the equivalent screen in the read-only com.oapprotocol.pubweb
   project, at the user's request (2026-10-09), so the two sign-in experiences match.

   `.field` is the label+input wrapper. It exists because `.form-narrow label` is itself a grid
   that used to CONTAIN its input; moving the input out (so `htmlFor` can point at it, which is
   what makes the label clickable and what a screen reader needs for an explicit association)
   made label and input two children of the outer grid, a full .85rem apart. `.field` restores
   the tight pairing without putting the input back inside the label. */
.form-narrow .field { display: grid; gap: .3rem; }
.form-narrow .field > label { font-size: .88rem; color: var(--color-muted); }
.form-narrow input.code-input,
.member-security input.code-input {
  text-align: center;
  font-weight: 700;
  /* `ch` on a monospace stack, so the box is sized by the content rather than guessed, and the
     tracking below cannot push digits out of view on a narrow phone. */
  font-family: var(--mono);
  font-size: clamp(1.1rem, 4.5vw, 1.45rem);
  letter-spacing: .5em;
  /* The tracking is applied AFTER each character including the last, which visually offsets the
     whole value to the left of centre. Half of it back as padding re-centres it — without this
     the text looks misaligned in a field that is otherwise symmetrical. */
  padding-left: calc(.7rem + .5em);
  max-width: 14ch;
  justify-self: start;
}
.form-narrow input:focus-visible, .form-narrow select:focus-visible, .form-narrow textarea:focus-visible {
  outline: 2px solid var(--color-primary); outline-offset: 1px;
}
/* Buttons wrap instead of overflowing on a narrow screen. */
.form-narrow .row, .content-page .row { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; }
.content-page { max-width: 68ch; margin: 0 auto; padding: 1.5rem clamp(1rem, 4vw, 2rem) 2rem; }

.join-panel { margin-top: 1.8rem; padding: 1.1rem 1.2rem; border: 1px solid var(--color-border);
  border-radius: var(--radius); background: var(--color-card); }
.join-panel h2 { font-size: 1.05rem; margin: 0 0 .3rem; }
.join-panel p { margin: 0 0 .6rem; color: var(--color-muted); font-size: .9rem; }

/* The membership/sponsorship modal shell. `dvh` so an open mobile keyboard does not push the
   panel off screen, which `vh` does because it ignores the browser chrome. */
.mm-backdrop { position: fixed; inset: 0; z-index: 1100; display: grid; place-items: center;
  background: rgba(0,0,0,.55); padding: 1rem; overflow-y: auto; }
.mm-panel { width: min(100%, 560px); max-height: min(86dvh, 86vh); overflow-y: auto;
  background: var(--color-card); color: var(--color-foreground);
  border: 1px solid var(--color-border); border-radius: var(--radius);
  box-shadow: var(--shadow-card-hover); padding: 1.3rem clamp(1rem, 4vw, 1.6rem); position: relative; }
.mm-panel h2, .mm-panel h3 { margin: 0 0 .6rem; }
.mm-x { position: absolute; top: .5rem; right: .6rem; background: none; border: 0;
  color: var(--color-muted); font-size: 1.2rem; line-height: 1; cursor: pointer; padding: .2rem .35rem; }
.mm-x:hover { color: var(--color-foreground); }
.mm-err { color: var(--red); font-size: .85rem; }
@media (max-width: 560px) {
  /* Full-height sheet on a phone: a centred card with a tall form becomes a scroll-within-scroll. */
  .mm-backdrop { place-items: end stretch; padding: 0; }
  .mm-panel { width: 100%; max-height: 92dvh; border-radius: var(--radius) var(--radius) 0 0; }
}

/* ============ Schema-driven forms (imports/ui/forms/SchemaForm.jsx) ============ */
.sf-group { border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  padding: .9rem 1rem; margin: 0 0 .9rem; display: grid; gap: .7rem; }
.sf-group > legend, .sf-group > summary { font-weight: 600; font-size: .9rem; padding: 0 .3rem; }
.sf-group > summary { cursor: pointer; }
.sf-optional { color: var(--color-muted); font-weight: 400; font-size: .8rem; }
.sf-subgroup { display: grid; gap: .6rem; }
.sf-subgroup h4 { margin: .3rem 0 0; font-size: .82rem; color: var(--color-muted);
  text-transform: uppercase; letter-spacing: .08em; }
.sf-field { display: grid; gap: .25rem; }
.sf-check { display: flex; align-items: center; gap: .5rem; }
/* A checkbox must not stretch to the field width the way text inputs do. */
.sf-check input { width: auto; flex: 0 0 auto; }
.sf-err { color: var(--red); font-size: .8rem; }
.sf [aria-invalid="true"] { border-color: var(--red); }

/* Badges: a multi-select as toggle chips. `<select multiple>` is unusable on a phone, which is
   where most of these submissions come from. */
.sf-badges { border: 0; padding: 0; margin: 0; display: grid; gap: .4rem; }
.sf-badges > legend { font-size: .88rem; color: var(--color-muted); padding: 0; }
.sf-badge-row { display: flex; flex-wrap: wrap; gap: .4rem; }
.sf-badge { font: inherit; font-size: .85rem; padding: .35rem .75rem; border-radius: 999px;
  border: 1px solid var(--color-border); background: transparent; color: var(--color-foreground);
  cursor: pointer; transition: background var(--transition-fast), border-color var(--transition-fast); }
.sf-badge:hover { border-color: var(--color-primary); }
.sf-badge.is-on { background: var(--color-primary); border-color: var(--color-primary); color: var(--on-brand); }
.sf-badge:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

/* ============ /join — mobile first, top down ============
   The application was laid out for a desktop and used on a phone: inputs at the browser's default
   height are about 28px tall, well under the ~44px a finger reliably hits, and a two-column
   fieldset forced sideways scrolling. This is a single column at every width, with the 44px
   minimum and the 16px font that stops iOS Safari zooming the page on focus.

   Ordered top-down deliberately: one question per row, groups in the order they are asked, and the
   submit button last and full-width on a phone where a right-aligned button is a reach. */
.join-group { border: 1px solid var(--color-border); border-radius: var(--radius);
  padding: 1rem; margin: 0 0 1rem; display: grid; gap: .9rem; }
.join-group > legend { font-weight: 600; font-size: .95rem; padding: 0 .4rem; }
.join-group label { display: grid; gap: .35rem; font-size: .9rem; color: var(--color-muted); }
.join-group input, .join-group select, .join-group textarea {
  width: 100%;
  /* 44px is the smallest reliable touch target; 16px font stops iOS zooming on focus, which looks
     like the layout breaking. */
  min-height: 44px; padding: .65rem .75rem; font: inherit; font-size: max(16px, .95rem);
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-card); color: var(--color-foreground);
}
.join-group textarea { min-height: 7rem; resize: vertical; }
/* A checkbox must stay its own size and gain a generous hit area around its label instead. */
.join-group input[type="checkbox"] { width: auto; min-height: 0; margin: 0; }
.join-group label:has(input[type="checkbox"]) {
  grid-template-columns: auto 1fr; align-items: center; gap: .6rem;
  min-height: 44px; color: var(--color-foreground);
}
.join-group input:focus-visible, .join-group select:focus-visible, .join-group textarea:focus-visible {
  outline: 2px solid var(--color-primary); outline-offset: 1px;
}

@media (max-width: 560px) {
  .join-group { padding: .85rem; border-radius: var(--radius-sm); }
  /* Full width, and last in the flow — a right-aligned button is a reach on a phone. */
  .join-group ~ .row .btn, .content-page > .row .btn { width: 100%; justify-content: center; }
}
@media (min-width: 900px) {
  /* Still ONE column. Widening the field is the right use of the space; two columns would make the
     eye jump between unrelated questions. */
  .join-group { padding: 1.15rem 1.3rem; }
}

/* ============ Header actions: join (green) vs sign-in (blue) ============
   Two intents, so two colours. Both are SURFACES carrying a white label, which is the bar they are
   held to in tests/contrast.tests.js — not the text bar, since nothing reads them as text. */
.btn-join, .btn-members {
  color: var(--on-brand); border: 1px solid transparent; font-weight: 700;
  /* 44px so the primary actions are reliably hittable on a phone. */
  min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
  padding: .5rem 1rem; border-radius: 10px; text-decoration: none; white-space: nowrap;
}
.btn-join { background: var(--join); }
.btn-members { background: var(--members); }
.btn-join:hover, .btn-members:hover { filter: brightness(1.08); }
.btn-join:focus-visible, .btn-members:focus-visible {
  outline: 2px solid var(--color-foreground); outline-offset: 2px;
}
/* External nav items say so, visibly and to a screen reader. */
.be-menu__ext { margin-left: .2rem; font-size: .8em; opacity: .75; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ============ The header at every width: one nav, three presentations ============

   BREAKPOINTS. Three, and they are the only ones the header uses:

     >= 1200px   DESKTOP. The panel is a row in the bar: links, then the two actions.
      768-1023   TABLET.  Hamburger + drawer. There is room for the brand and the actions in
                          the bar, but not for five links as well — and a nav that scrolls
                          sideways hides items with nothing to say they are there.
       < 768px   PHONE.   Hamburger + drawer, and the drawer is full width.

   The drawer cutoff (1023/1024) is duplicated in imports/ui/public/navDrawer.js as
   DRAWER_MAX_WIDTH, because a resize past it must force the drawer closed. A media query
   cannot be read from JS without matchMedia duplicating the literal anyway; if you move one,
   move both.

   WHY NO SIDEWAYS-SCROLLING NAV. That is what was here before (`overflow-x: auto` on the
   list): it fits any number of items into any width and makes every item past the fold
   invisible, with no affordance. A drawer shows all five.
   -------------------------------------------------------------------------------------- */

/* Desktop: the panel is a row, and the burger does not exist. */
.hdr-panel { display: flex; align-items: center; gap: 1.5rem; margin-left: auto; order: 2; }
.hdr-burger { display: none; }
/* Inert and invisible until the drawer opens. `visibility` as well as opacity, so it cannot
   swallow a click on the page behind it while closed. */
.nav-scrim { display: none; }

@media (max-width: 1199px) {
  .brand img.wordmark { height: 34px; }
  .hdr-cta small { display: none; }

  .hdr-burger {
    order: 4;
    display: inline-flex; align-items: center; justify-content: center;
    /* 44px: the minimum touch target both platform guidelines specify. The e2e spec asserts
       it, because a 32px icon button is the single most common mobile defect here.
       `flex: 0 0 44px` is what makes the declaration hold. `width` alone does NOT: the burger is
       a flex item and `flex-shrink` defaults to 1, so a crowded bar COMPRESSES it below its own
       declared size. Adding the language picker took it to 41px at 390px wide — a 3px overrun
       that silently broke the touch target, and the same thing would happen to the next control
       anyone adds. The target is non-negotiable, so it is the one item that may not shrink;
       everything else in the bar gives way instead. */
    flex: 0 0 44px;
    width: 44px; height: 44px; margin-left: .25rem; padding: 0;
    background: transparent; border: 1px solid var(--color-border);
    border-radius: var(--radius-sm); color: var(--color-foreground); cursor: pointer;
  }
  .hdr-burger:focus-visible { outline: 2px solid var(--color-link); outline-offset: 2px; }
  /* Three bars from one element: the middle is the box, the outer two are pseudo-elements.
     `currentColor` so it follows the theme with no second colour to keep in step. */
  .hdr-burger__bars,
  .hdr-burger__bars::before,
  .hdr-burger__bars::after {
    display: block; width: 18px; height: 2px; border-radius: 2px;
    background: currentColor; content: ''; position: relative;
    transition: transform var(--transition-fast), opacity var(--transition-fast);
  }
  .hdr-burger__bars::before { position: absolute; top: -6px; }
  .hdr-burger__bars::after { position: absolute; top: 6px; }
  /* Open: the bars become an X, so the control reads as "close" without a second icon. */
  :root[data-nav-open] .hdr-burger__bars { background: transparent; }
  :root[data-nav-open] .hdr-burger__bars::before { transform: translateY(6px) rotate(45deg); }
  :root[data-nav-open] .hdr-burger__bars::after { transform: translateY(-6px) rotate(-45deg); }

  /* THE DRAWER. Off-canvas to the right, not `display: none` → `block`: a hidden panel cannot
     animate, and `visibility` is what keeps its links out of the tab order while closed.
     Transform rather than `right`, so it composites instead of reflowing the page each frame. */
  .hdr-panel {
    position: fixed; top: 0; right: 0; z-index: 60;
    width: min(20rem, 86vw); max-width: 86vw;
    height: 100vh; height: 100dvh;   /* dvh so a mobile URL bar cannot clip the last item */
    /* The bottom pad reserves room for OTHER bottom-fixed chrome, not just the device safe
       area. The consent banner is fixed to the bottom of the viewport with a higher stacking
       order, so the panel's LAST control sat underneath it and could not be clicked — the same
       defect as the chat launcher over the footer's privacy controls, and the reason the rule
       here is "fixed chrome must reserve space, not raise z-index". The panel already scrolls
       (`overflow-y: auto`), so the extra allowance costs nothing but breathing room at the end
       of a long nav. */
    margin: 0; padding: calc(var(--safe-top) + 4.5rem) 1.25rem calc(var(--safe-bottom) + 6rem);
    flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 1.25rem;
    background: var(--color-card); border-left: 1px solid var(--color-border);
    box-shadow: var(--shadow);
    overflow-y: auto; overscroll-behavior: contain;
    transform: translateX(100%); visibility: hidden;
    /* `visibility` is stepped with a DELAY, never interpolated: hiding waits for the slide-out
       to finish, and showing happens at once. Transitioning it the obvious way left the panel
       `visibility: hidden` at the moment it was opened — and a hidden element cannot take
       focus, so the focus trap had nothing to move focus to and the keyboard tabbed straight
       into the page behind the scrim. */
    transition: transform var(--transition-normal), visibility 0s var(--transition-normal);
  }
  :root[data-nav-open] .hdr-panel {
    transform: translateX(0); visibility: visible;
    transition: transform var(--transition-normal), visibility 0s;
  }

  /* The links stack and become rows big enough to hit. */
  .hdr-panel .be-menu--top,
  .hdr-panel .be-menu--top ul { flex-direction: column; align-items: stretch; gap: .25rem; width: 100%; }
  .hdr-panel .be-menu--top a {
    display: block; padding: .7rem .6rem; border-radius: var(--radius-sm);
    font-size: 1rem; color: var(--color-foreground);
  }
  .hdr-panel .be-menu--top .is-active > a { background: var(--chip); }

  /* The two actions sit at the bottom of the sheet, full width, in reading order:
     join first because the drawer is mostly opened by people who are not members yet. */
  .hdr-panel .hdr-cta {
    flex-direction: column; align-items: stretch; gap: .6rem;
    margin: auto 0 0; padding-top: 1rem; border-top: 1px solid var(--color-border);
  }
  .hdr-panel .hdr-cta .btn { justify-content: center; padding: .7rem 1rem; font-size: .95rem; }

  .nav-scrim {
    display: block; position: fixed; inset: 0; z-index: 50;
    background: var(--scrim); opacity: 0; visibility: hidden;
    transition: opacity var(--transition-normal), visibility 0s var(--transition-normal);
  }
  :root[data-nav-open] .nav-scrim {
    opacity: 1; visibility: visible;
    transition: opacity var(--transition-normal), visibility 0s;
  }
  /* The page behind must not scroll under an open drawer — on iOS it otherwise rubber-bands
     the body while the drawer stays put, which reads as the drawer being stuck. */
  :root[data-nav-open] body { overflow: hidden; }
}

@media (max-width: 480px) {
  /* A 320px-wide phone fits the brand, the theme switch and the burger — and nothing else. */
  .hdr-panel { width: 100vw; max-width: 100vw; border-left: 0; }
  .brand img.wordmark { height: 30px; }
}

/* The slide is decoration. Honour a reduced-motion preference by appearing instead. */
@media (prefers-reduced-motion: reduce) {
  .hdr-panel, .nav-scrim, .hdr-burger__bars,
  .hdr-burger__bars::before, .hdr-burger__bars::after { transition: none; }
}


/* ============ /attest — the signed-content ledger ============
   A technical instrument, so it is plain: a monospace hash that wraps instead of pushing the
   page sideways, and a verdict that is legible at a glance in both themes. */
.attest-page { padding: 1.5rem 0 3rem; }
.attest-page .page-header h1 { margin-bottom: .4rem; }
/* `break-all`, because a 64-character hash has no spaces to wrap at and would otherwise force
   the whole page to scroll sideways on a phone. */
.attest-hash { font-family: var(--mono); font-size: .78rem; word-break: break-all; cursor: pointer; }
.attest-check { display: grid; gap: .5rem; max-width: 46rem; }
.attest-check label { font-size: .8rem; font-weight: 600; color: var(--color-muted); }
.attest-check input { font-family: var(--mono); font-size: .85rem; }
.attest-check .btn { justify-self: start; }
@media (min-width: 640px) {
  /* The field takes the room and the button sits beside it, rather than a full-width button
     that reads as the primary action of the whole page. */
  .attest-check { grid-template-columns: 1fr auto; align-items: end; }
  .attest-check label { grid-column: 1 / -1; margin: 0; }
}
.attest-page .table code { background: none; padding: 0; }
.attest-page .alert-success { background: var(--tint-teal); border-color: var(--teal); color: var(--color-foreground); }
.attest-page .alert-danger { background: var(--tint-red); border-color: var(--red); color: var(--color-foreground); }
.attest-page .alert strong { display: block; margin-bottom: .4rem; }


/* ============ The footer's single legal bar ============
   The three-column grid (identity / Ecosystem / Explore) was removed on 2026-10-07: the footer
   is limited to privacy, T&C, conduct, copyright and a small Get-in-touch button. The Explore
   column was a second rendering of the header navigation, and a second list of links drifts the
   moment a page is renamed.

   These controls used to be React (`PublicLayout`'s `.footer-legal`), which meant they rendered
   on every route EXCEPT `/` — so the homepage had no cookie-settings control and no CPRA
   opt-out. They are in the shared footer template now; the rules below style the template's
   `.foot-legal` and the legacy `.footer-legal` identically so nothing regresses mid-migration. */
.foot-legal, .footer-legal { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem;
  color: var(--color-muted); font-size: .82rem; }
.foot-legal a, .footer-legal a { color: var(--color-muted); text-decoration: none; }
.foot-legal a:hover, .footer-legal a:hover { color: var(--color-foreground); text-decoration: underline; }
.foot-legal .link-btn, .footer-legal .link-btn {
  background: none; border: 0; padding: 0; font: inherit; color: var(--color-muted);
  cursor: pointer; text-decoration: none;
}
.foot-legal .link-btn:hover, .footer-legal .link-btn:hover { color: var(--color-foreground); text-decoration: underline; }
.foot-legal .link-btn:focus-visible, .footer-legal .link-btn:focus-visible {
  outline: 2px solid var(--color-link); outline-offset: 2px; border-radius: 3px;
}
/* The Get-in-touch trigger, small. `btn-sm` metrics applied by SELECTOR rather than by adding a
   class to the include: the include is shared, so a literal class there would shrink every
   trigger on the site. */
.foot-legal .be-form-trigger {
  padding: .25rem .6rem; font-size: .78rem; line-height: 1.4; border-radius: var(--radius-sm);
}
@media (max-width: 640px) {
  /* Stacked rather than wrapped mid-item, so no separator is orphaned on its own line. */
  .foot-legal, .footer-legal { gap: .3rem .5rem; }
}


/* ============ {{blog-index}} / {{kb-index}} / {{press-index}} summary panels ============
   Server-rendered by server/lib/content-index.js, in BOOTSTRAP 3.3.7 idiom only — the vendored
   sheet has no .card/.d-flex/.g-*, and a v5-idiom panel renders as unstyled text with no error.
   These rules only add spacing and the meta row; the panel chrome itself is Bootstrap's. */
.be-index { display: grid; gap: 1rem; margin: 1.4rem 0; }
@media (min-width: 768px) { .be-index { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.be-index .be-index__item { margin-bottom: 0; }
.be-index .be-index__item h3 { margin-top: 0; }
.be-index .be-index__item h3 a { color: var(--color-foreground); text-decoration: none; }
.be-index .be-index__item h3 a:hover { color: var(--color-link); text-decoration: underline; }
.be-index__meta { display: flex; align-items: center; gap: .5rem; font-size: .8rem; margin: .2rem 0 .6rem; }
.be-index__meta time { font-size: .8rem; }


/* ============ {{share-popout}} — the share panel ============
   Server-rendered by server/lib/share-panel.js; behaviour is the document delegate in
   imports/ui/helpers/formatting.js. No third-party script: every platform is a plain
   share-intent link, so nothing loads until the visitor clicks. */
.asw { position: relative; display: inline-block; }
.asw-panel {
  position: absolute; z-index: 60; right: 0; top: calc(100% + .4rem);
  width: min(22rem, 90vw); padding: .9rem 1rem;
  background: var(--color-card); border: 1px solid var(--color-border);
  border-radius: var(--radius-sm); box-shadow: var(--shadow-card-hover);
}
/* `{{share-inline}}` renders the panel in flow instead of floating. */
.asw [data-share-panel]:not([hidden]).asw-panel { display: block; }
.asw-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
.asw-title { margin: 0; font-weight: 600; font-size: .9rem; }
.asw-close { background: none; border: 0; font-size: 1.2rem; line-height: 1; cursor: pointer;
  color: var(--color-muted); padding: 0 .2rem; }
.asw-close:hover { color: var(--color-foreground); }
/* `break-all`, because a long URL has no spaces to wrap at and would push the panel sideways. */
.asw-url { margin: .4rem 0 .6rem; font-family: var(--mono); font-size: .72rem;
  color: var(--color-muted); word-break: break-all; }
.asw-section { margin: .6rem 0 .3rem; font-size: .72rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--color-muted); }
.asw-tags { display: flex; flex-wrap: wrap; gap: .3rem; }
.asw-tag { font-size: .74rem; padding: .2rem .5rem; border-radius: 999px; cursor: pointer;
  background: transparent; color: var(--color-muted); border: 1px solid var(--color-border); }
.asw-tag.is-on { background: var(--chip); color: var(--color-foreground); border-color: var(--color-link); }
.asw-tag:focus-visible, .asw-btn:focus-visible { outline: 2px solid var(--color-link); outline-offset: 2px; }
.asw-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(6.2rem, 1fr)); gap: .35rem; }
.asw-btn { display: flex; align-items: center; gap: .4rem; cursor: pointer;
  /* 44px tall: the minimum touch target both platform guidelines specify. */
  min-height: 44px; padding: .35rem .5rem; font-size: .78rem; text-align: left;
  background: transparent; color: var(--color-foreground);
  border: 1px solid var(--color-border); border-radius: var(--radius-sm); }
.asw-btn:hover { background: var(--chip); }
.asw-icon { width: 10px; height: 10px; border-radius: 2px; flex: 0 0 auto; background: var(--color-muted); }
.asw-x .asw-icon { background: var(--letterbox); }
.asw-facebook .asw-icon { background: var(--brand-facebook); }
.asw-linkedin .asw-icon { background: var(--brand-linkedin); }
.asw-reddit .asw-icon { background: var(--brand-reddit); }
.asw-whatsapp .asw-icon { background: var(--brand-whatsapp); }
.asw-telegram .asw-icon { background: var(--brand-telegram); }
.asw-row { display: flex; gap: .4rem; margin-top: .7rem; }

/* ---------------------------------------------------------------------------
   THE SHARE RAIL — {{share-popout}}
   Fixed to the right edge and centred in the VIEWPORT, so it stays reachable the whole way
   through a long article without scrolling over the text.

   It is opt-in per page. It used to live in the shared footer, which put it on every page
   including forms and legal notices — a persistent floating control there is noise, and it also
   meant no page could choose a different share message.

   z-index sits BELOW the chat launcher (1001) and the consent banner (1000) and above the sticky
   header (40): a share tab must not cover a consent decision or the assistant, and both of those
   are bottom-anchored so they never collide with a vertically centred rail anyway.
   --------------------------------------------------------------------------- */
.asw--rail { position: fixed; right: 0; top: 50%; transform: translateY(-50%); z-index: 950; }
.asw-open--rail {
  display: flex; flex-direction: column; align-items: center; gap: .4rem; cursor: pointer;
  padding: .7rem .45rem; border: 1px solid var(--color-border); border-right: 0;
  border-radius: var(--radius) 0 0 var(--radius);
  background: var(--color-card); color: var(--color-foreground);
  box-shadow: var(--shadow-card-hover);
}
.asw-open--rail:hover { background: var(--chip); }
.asw-open--rail:focus-visible { outline: 2px solid var(--color-link); outline-offset: 2px; }
/* The label reads downward so the tab stays narrow; `mixed` keeps Latin glyphs upright. */
.asw-open__label { writing-mode: vertical-rl; text-orientation: mixed; font-size: .72rem;
  letter-spacing: .1em; text-transform: uppercase; }
.asw-open__icon { width: 12px; height: 12px; border-radius: 2px; background: var(--color-link); }

/* The panel pops out INWARD from the tab, not downward: anchored to the rail it would otherwise
   extend past the bottom of a short viewport and be unreachable. */
.asw--rail .asw-panel {
  position: absolute; right: calc(100% + .35rem); top: 50%; left: auto; bottom: auto;
  transform: translateY(-50%); width: min(20rem, calc(100vw - 4rem));
}

/* On a phone the rail stays, but the panel becomes a sheet: 20rem beside a 44px tab does not fit,
   and a horizontally cramped panel is how the share grid ends up unreadable. */
@media (max-width: 600px) {
  .asw--rail .asw-panel {
    position: fixed; right: .75rem; left: .75rem; top: auto; bottom: .75rem;
    width: auto; transform: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .asw-open--rail { transition: none; }
}
@media (max-width: 480px) {
  /* Full-bleed on a phone rather than a 22rem panel clipped by the viewport edge. */
  .asw-panel { position: fixed; left: 1rem; right: 1rem; top: auto; bottom: 1rem; width: auto; }
}
@media (prefers-reduced-motion: reduce) { .asw-panel { transition: none; } }


/* The full application is long, so its modal scrolls instead of growing past the viewport.
   `dvh` because a mobile URL bar would otherwise clip the submit button — the one control the
   form exists for. */
.mm-panel--tall { max-height: 88vh; max-height: 88dvh; overflow-y: auto; overscroll-behavior: contain; }
.mm-panel--tall .join-group { margin: 0 0 1rem; }

/* ---------------------------------------------------------------------------
   THE VIDEO MODAL — blocks/video.html ({{video}} / {{video <id-or-url>}})
   These rules lived in private/templates/pages/home.html's own <style> block, so the modal was
   styled on the home page and NOWHERE else. Unstyled, `.vmodal` has no `display:none` (it is
   always on screen) and `.vbackdrop` collapses to zero height — so "click outside to close" had
   no target to click, exactly as the nav scrim did before the header work. Chrome is styled once.

   z-index 1100 matches .mm-backdrop: a modal is above the sticky header, the share rail and the
   chat launcher, all of which it deliberately covers while open.
   --------------------------------------------------------------------------- */
.vmodal { position: fixed; inset: 0; z-index: 1100; display: none;
  align-items: center; justify-content: center; padding: 1.2rem; }
.vmodal.on { display: flex; }
.vbackdrop { position: absolute; inset: 0; background: var(--scrim); }
.vbox { position: relative; width: min(960px, 100%); z-index: 1; }
/* padding-top is the 16:9 ratio box — the iframe is absolutely positioned inside it. */
.vframe { position: relative; padding-top: 56.25%; overflow: hidden;
  background: var(--color-background); border-radius: var(--radius);
  border: 1px solid var(--color-border); box-shadow: var(--shadow-card-hover); }
.vframe iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
/* `#fff` rather than a token: the palette has no on-colour foregrounds, and .livechat-fab sets
   white on --color-primary the same way. A glyph this size is large text (AA 3:1), which white on
   --red clears; it would not clear 4.5:1 as body copy, so do not reuse this pairing for prose. */
/* z-index 2 is LOAD-BEARING. `.vclose` and `.vframe` are both positioned children of `.vbox`
   with `z-index: auto`, so the later one in the DOM paints on top — and the button is offset only
   -14px, so its centre lies INSIDE the frame. The iframe therefore intercepted the pointer and the
   close button could not be clicked at all, by anyone. An iframe swallows events silently, so this
   looked like a dead button rather than a stacking problem. */
.vclose { position: absolute; top: -14px; right: -14px; z-index: 2; width: 40px; height: 40px;
  border-radius: 50%; background: var(--red); color: var(--on-brand); border: 0;
  font-size: 1.4rem; line-height: 1; cursor: pointer; box-shadow: var(--shadow-card-hover); }
.vclose:focus-visible { outline: 2px solid var(--color-link); outline-offset: 2px; }
/* The trigger. `.btn-video` is the Bootstrap 3 button plus the play glyph. */
.btn-video { display: inline-flex; align-items: center; gap: .45rem; }
.video-heading { margin: 0 0 .4rem; font-weight: 600; }
.block-video { margin: 1rem 0; }

/* ---------------------------------------------------------------------------
   THE CAROUSEL — imports/ui/public/carousel.js
   Progressive enhancement: without the script the markup is a plain stack of images, which is
   usable. Nothing here hides a slide in a way only JS can undo.

   `--carousel-per-view` is set by the script from the element's own `data-items`, so the item
   width is one declaration rather than a media query per breakpoint.
   --------------------------------------------------------------------------- */
.carousel { position: relative; overflow: hidden; touch-action: pan-y; --carousel-per-view: 3; }
.carousel__track {
  display: flex; align-items: stretch;
  transition: transform .45s cubic-bezier(.22, .61, .36, 1);
  will-change: transform;
}
.carousel.is-dragging .carousel__track { transition: none; cursor: grabbing; }
.carousel__item {
  flex: 0 0 calc(100% / var(--carousel-per-view));
  max-width: calc(100% / var(--carousel-per-view));
  padding: 0 .35rem; box-sizing: border-box;
}
/* The images carried `style="max-height:500px"` inline on every slide — 24 copies of one rule
   that no stylesheet could override and the contrast test could not see. Here once instead. */
.carousel__item img {
  display: block; width: 100%; height: auto; max-height: 500px; object-fit: cover;
  border-radius: var(--radius); background: var(--color-card);
}

/* The rollover pop-out. Transform only — animating width or margin would reflow the whole track
   on every hover, and on a 24-slide strip that is visible jank. */
.carousel__item > * { transition: transform .25s ease, box-shadow .25s ease; }
.carousel__item:hover > *,
.carousel__item:focus-within > * {
  transform: scale(1.04); box-shadow: var(--shadow-card-hover); position: relative; z-index: 2;
}

.carousel__nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
  display: grid; place-items: center; font-size: 1.4rem; line-height: 1;
  border: 1px solid var(--color-border); background: var(--color-card); color: var(--color-foreground);
}
.carousel__nav--prev { left: .4rem; }
.carousel__nav--next { right: .4rem; }
.carousel__nav:hover { background: var(--chip); }
.carousel__nav:focus-visible { outline: 2px solid var(--color-link); outline-offset: 2px; }
.carousel__nav[disabled] { opacity: .35; cursor: default; }

.carousel__dots { display: flex; justify-content: center; gap: .3rem; margin-top: .5rem; }
.carousel__dot {
  width: 8px; height: 8px; padding: 0; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--color-border); background: transparent;
}
.carousel__dot.is-on { background: var(--color-link); border-color: var(--color-link); }
.carousel__dot:focus-visible { outline: 2px solid var(--color-link); outline-offset: 2px; }

/* A phone shows one slide, so the arrows sit over the image rather than beside it, and the
   pop-out is dropped: there is no hover on a touch screen and a scale on tap reads as a glitch. */
@media (max-width: 600px) {
  .carousel__item { padding: 0 .2rem; }
  .carousel__item:hover > * { transform: none; box-shadow: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* Autoplay is already disabled in the script — this covers the manual transitions too. */
  .carousel__track { transition: none; }
  .carousel__item > * { transition: none; }
  .carousel__item:hover > * { transform: none; }
}

/* ---------------------------------------------------------------------------
   PAGE UTILITIES — the classes that replaced 35 inline `style=` attributes.

   Why they exist at all: an inline style cannot be themed, cannot be measured by
   tests/contrast.tests.js, overrides the stylesheet by specificity, and would be blocked by the
   strict `style-src` that server/web/waf.js still records as an open item. /membership alone
   carried 42 of them, 41 being one `max-height` repeated per carousel image.

   One of them was a real defect: `color:#9fb2d6` on /technological was a hand-picked literal used
   as TEXT, so it was invisible to the contrast test and could not respond to the theme. It is
   `--color-muted` now, which is measured.

   Kept deliberately small and named for what the page meant, not for the declaration — a full
   utility framework in here would just move the problem.
   --------------------------------------------------------------------------- */
.u-tint-teal { background: linear-gradient(180deg, var(--tint-teal), transparent); }
.u-muted { color: var(--color-muted); }
.u-note { color: var(--color-muted); margin-top: 10px; }
.u-note-14 { color: var(--color-muted); margin-top: 14px; }
.u-meta { font-size: .76rem; color: var(--color-muted); }
.u-card-note { margin: 0; color: var(--color-muted); font-size: .9rem; }
.u-stat { font-size: 1.2rem; font-weight: 700; }
.u-subhead { margin-top: 18px; font-family: var(--display); font-weight: 700; color: var(--text); }
.u-hero-title { font-size: clamp(1.6rem, 3.4vw, 2.3rem); font-weight: 800; }
.u-bare-top { border: 0; padding: .9rem 0 0; }
.u-mt-07 { margin-top: .7rem; }
.u-mt-12 { margin-top: 1.2rem; }
.u-mt-14 { margin-top: 14px; }
.u-mt-16 { margin-top: 16px; }
.u-pt-0 { padding-top: 0; }
.u-pad-section { padding-top: clamp(52px, 7vw, 86px); padding-bottom: clamp(52px, 7vw, 86px); }
.u-center { display: flex; justify-content: center; align-items: center; }
.u-center-text { text-align: center; }
.u-mi-auto { margin-inline: auto; }

/* A panel header with more than one action. Without this the new link and the primary button
   stack instead of sitting side by side, because .panel-head is a space-between flex row. */
.panel-head-actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }

/* The featured item at the top of an index — the next event on /events.
   `.jumbotron` because the vendored Bootstrap is 3.3.7 and has no `.card`. */
.be-index__feature { padding: 1.4rem 1.6rem; margin-bottom: 1.2rem;
  background: var(--color-card); border: 1px solid var(--color-border);
  border-radius: var(--radius); }
.be-index__feature h2 { margin: .2rem 0 .4rem; }
.be-index__kicker { margin: 0; font-size: .72rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--color-muted); }
.be-index__where { margin: .2rem 0 .6rem; color: var(--color-muted); }

/* ---------------------------------------------------------------------------
   SUBMENUS — a row of {{menu-top}} that has children.
   One markup shape, two presentations: a POPOUT panel on desktop, an inline THREAD in the
   drawer. Same `<ul class="be-menu__sub">`, same caret button, so there is one set of links and
   one set of data-testids — a second nav inside the drawer duplicates every link and the copies
   drift the moment one is edited. That is the rule the header already follows.

   Collapsed is the `hidden` ATTRIBUTE (display: none), never a zero height: navDrawer's
   focusables() filters on getClientRects(), and a collapsed-but-boxed list parks trapped focus
   on links nobody can see.
   --------------------------------------------------------------------------- */
.be-menu__item.has-children { position: relative; }
.be-menu__caret {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; margin-left: .15rem; padding: 0;
  background: none; border: 0; cursor: pointer; color: var(--color-muted);
  font-size: .8rem; line-height: 1;
}
.be-menu__caret:hover { color: var(--color-foreground); }
.be-menu__caret:focus-visible { outline: 2px solid var(--color-link); outline-offset: 2px; }
.be-menu__caret > span { display: block; transition: transform .18s ease; }
.be-menu__caret[aria-expanded="true"] > span { transform: rotate(180deg); }

/* The submenu list itself, in both presentations. */
.be-menu__sub { list-style: none; margin: 0; padding: 0; }

/* `[hidden]` IS ONLY A UA RULE, and any author `display` beats it.
   `.be-menu--top ul { display: flex }` already matched this element, so the submenu rendered
   OPEN however many times the attribute was set — the caret toggled `hidden` faithfully and
   nothing happened. Found by the e2e: "starts closed" failed with "received: visible".
   This selector carries a class AND an attribute, so it outranks the type selector that was
   winning, and the reveal rules below outrank it in turn. */
.be-menu__sub[hidden] { display: none; }
.be-menu__sub .be-menu__link { display: block; padding: .35rem 0; font-weight: 500; }

/* DESKTOP: a popout panel under the parent. `position: absolute` inside the `has-children` li,
   which is why that carries `position: relative`. */
@media (min-width: 1200px) {
  .be-menu--top .be-menu__sub {
    position: absolute; top: calc(100% + .5rem); left: 0; z-index: 60;
    /* No `display` here: setting one would re-beat `[hidden]` and reopen the bug above. The
       list's own display comes from the reveal rules, which is the only place it is needed. */
    min-width: 12rem; flex-direction: column; gap: 0;
    padding: .5rem .75rem; background: var(--color-card);
    border: 1px solid var(--color-border); border-radius: var(--radius);
    box-shadow: var(--shadow-card-hover);
  }
  /* THE CARET IS THE ONLY TRIGGER. There is no :hover or :focus-within reveal, and that is a
     decision rather than an omission.
     Both were implemented first and both broke Escape, in the same way: after clicking the caret
     the pointer is still over the item, so `:hover` re-revealed what Escape had just closed —
     and `:focus-within` did too, because Escape puts focus on the caret, which is INSIDE the
     item. CSS state cannot be dismissed by a keypress, so with either rule present "Escape
     closes the menu" is unimplementable. The e2e found it as "received: visible".
     Caret-only is also better behaviour: hover menus open on accidental pointer travel across a
     nav bar, and they do not exist at all on a touch screen. The caret is a real <button>, so
     Enter and Space work without any of this.
     An open panel stacks its links as a block, not as the flex row the top menu would give it. */
  .be-menu--top .be-menu__sub:not([hidden]) { display: block; }
}

/* DRAWER (phone and tablet): an inline thread, indented under its parent — not a popout. A panel
   floating over a drawer that is itself a panel has nowhere to go. */
@media (max-width: 1199px) {
  .be-menu__item.has-children { display: flex; flex-wrap: wrap; align-items: center; }
  .be-menu__item.has-children > .be-menu__link { flex: 1 1 auto; }
  .be-menu__caret { width: 44px; height: 44px; }   /* a real touch target */
  .be-menu__sub:not([hidden]) {
    display: block; flex: 1 0 100%; margin: .2rem 0 .4rem; padding-left: .9rem;
    border-left: 2px solid var(--color-border);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The caret rotation is the only animation here, and it is the one someone with vestibular
     sensitivity notices on every nav interaction. */
  .be-menu__caret > span { transition: none; }
}

/* ---------------------------------------------------------------------------
   LEGAL PAGES — /privacy, /termsandconditions, /codeofconduct
   All three were raw pastes of the previous theme's markup with no header and no footer, using
   classes (`split-box-content`, `col-lg-height`, `about-us-inner`) that live only in
   public/assets/css/theme.css and helper.css — neither of which PublicLayout loads. So every
   wrapper was inert and the prose ran the full width of the viewport, unstyled.

   The prose itself is UNCHANGED, because it is legal text. It is `<strong>` headings and
   `<br><br>` paragraph breaks rather than semantic elements, so this styles what is actually
   there instead of pretending otherwise — a `<strong>` that starts a line reads as a heading.

   Everything here is a token, so these pages follow light/dark like the rest of the site. That
   was the other half of the report: they could not respond to the theme because nothing they
   used was themed.
   --------------------------------------------------------------------------- */
/* THE LEGAL PAGES MOVED FROM `article.legal` TO `section.legal-band > .wrap` (2026-10-12).
   The wrapper changed; the READABLE MEASURE is a property of long prose, not of a class name, so
   it travels with it. Without this the three legal pages render at the site-wide `.wrap` width of
   1200px — which `tests/e2e/public/legal-pages.spec.js` catches as "legal prose runs the full
   page width", and which is the complaint the 68ch below was introduced to answer in the first
   place.

   SCOPED TO `.legal-band`, never to `.wrap`: that class is site-wide chrome shared with `/` and
   every other template, and narrowing it globally would reflow the whole site.

   The remaining `.legal` rules below are currently unreferenced — the restyle is in progress and
   porting its typography is the operator's call, not something to guess at here. They are left
   rather than deleted so the intended treatment is still legible while that work lands. */
.legal-band .wrap {
  max-width: 68ch;
  margin-left: auto;
  margin-right: auto;
}

.legal {
  /* A measure, not the viewport. ~70 characters is the readable width for long prose; full-width
     legal text is the thing people say they cannot read. */
  max-width: 68ch;
  margin: 0 auto;
  color: var(--color-foreground);
  font-size: 1rem;
  line-height: 1.7;
}
.legal h1 { margin: 0 0 1.2rem; font-family: var(--display); font-size: clamp(1.6rem, 3.4vw, 2.2rem); }
.legal h2, .legal h3 { margin: 2rem 0 .6rem; font-family: var(--display); font-size: 1.15rem; }
.legal a { color: var(--color-link); }
.legal a:hover { color: var(--color-foreground); }
/* A `<strong>` on its own line is a section heading in this prose. `display: block` plus the
   spacing is what turns the pasted shape into something with structure, without touching a word
   of it. */
.legal strong {
  display: block;
  margin: 1.8rem 0 .4rem;
  font-family: var(--display);
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: .01em;
  color: var(--headline);
}
.legal p { margin: 0 0 1rem; }
.legal ul, .legal ol { margin: 0 0 1rem 1.2rem; }
.legal li { margin: .3rem 0; }
.legal .text-muted, .legal small { color: var(--color-muted); }
/* The tables and panels /privacy uses for its data-categories section. */
.legal table { width: 100%; margin: 0 0 1.2rem; }
.legal th, .legal td { padding: .5rem .6rem; border-bottom: 1px solid var(--color-border); text-align: left; }
.legal th { color: var(--headline); font-weight: 700; }

@media (max-width: 600px) {
  .legal { font-size: .95rem; }
}

/* ---------------------------------------------------------------------------
   THE PAGE BACKGROUND — specificity, not source order.

   `html body` is 0,0,2 and beats `body` at 0,0,1 whatever the cascade order.

   imports/ui/layouts/PublicLayout.jsx injects the vendored bootstrap.min.css at RUNTIME, so it
   lands AFTER every bundled stylesheet however they are ordered, and Bootstrap 3 sets
   `body { background-color: var(--surface-invert) }`. At equal specificity the later rule wins — so every
   PublicLayout page rendered a WHITE background in dark mode.

   It hid for so long because the nine page templates that carry their own inline <style> each
   restate `body { background: var(--bg) }`, and an inline <style> beats a linked sheet. So the
   pages with the most CSS of their own looked right while /privacy, /termsandconditions and
   /codeofconduct — which correctly have no styles of their own — did not. Reported 2026-10-08.
   --------------------------------------------------------------------------- */
html body {
  background: var(--color-background);
  color: var(--color-foreground);
}

/* ---------------------------------------------------------------------------
   "THE MODEL IS WORKING" — imports/ui/admin/AiBusy.jsx
   A local generation measured 29.2s for a 250-word press release, so every AI button in the
   console needs to look busy for a long time. A disabled button is indistinguishable from a dead
   one, which is why there is a moving indicator AND an elapsed count AND a plain sentence about
   how long it takes.
   --------------------------------------------------------------------------- */
.ai-busy {
  display: flex; align-items: flex-start; gap: .7rem;
  margin: .6rem 0; padding: .7rem .9rem;
  background: var(--color-card); border: 1px solid var(--color-border);
  border-left: 3px solid var(--color-link); border-radius: var(--radius);
  color: var(--color-foreground);
}
.ai-busy__text { line-height: 1.45; }
.ai-busy__text small { color: var(--color-muted); }
.ai-busy__elapsed { font-family: var(--mono); color: var(--color-muted); }

/* The macOS-style segmented spinner, in CSS — twelve spokes fading in sequence. Drawn rather
   than animated as a rotation so it reads as the familiar indeterminate indicator, and so there
   is no image to load on an admin page. */
.ai-spin {
  position: relative; flex: 0 0 auto; width: 20px; height: 20px; margin-top: .15rem;
}
.ai-spin::before,
.ai-spin::after,
.ai-spin > span { display: none; }
.ai-spin {
  border-radius: 50%;
  /* A conic sweep is one declaration and matches the spoke look closely enough at this size. */
  background: conic-gradient(from 0deg, transparent 0 25%, var(--color-link) 100%);
  mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px));
  animation: ai-spin 0.9s linear infinite;
}
@keyframes ai-spin { to { transform: rotate(1turn); } }

/* The locked inputs. `<fieldset disabled>` already blocks interaction; this makes it VISIBLE,
   because a field that silently ignores typing is worse than one that looks unavailable. */
.ai-lock { border: 0; padding: 0; margin: 0; min-width: 0; }
.ai-lock.is-running { opacity: .55; cursor: progress; }
.ai-lock.is-running * { cursor: progress !important; }

@media (prefers-reduced-motion: reduce) {
  /* No rotation. The elapsed counter still ticks, so there is still proof of life — which is why
     the counter is not decoration. */
  .ai-spin { animation: none; background: var(--color-link);
    mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 3px)); }
}

/* ---------------------------------------------------------------------------
   THE LANGUAGE PICKER — imports/ui/components/LanguagePicker.jsx
   `.lang` had no rules at all, which is part of why the switcher was never mounted: even if it
   had been, it would have rendered as an unstyled browser select in the middle of the header.
   --------------------------------------------------------------------------- */
.hdr-lang { display: inline-flex; align-items: center; }
.lang {
  appearance: none;
  padding: .3rem 1.5rem .3rem .5rem;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm, 8px);
  background: var(--color-card); color: var(--color-foreground);
  font-size: .78rem; font-weight: 600; cursor: pointer;
  /* The caret is drawn rather than left to the platform, so it follows the theme instead of
     staying black on a dark header. */
  background-image: linear-gradient(45deg, transparent 50%, var(--color-muted) 50%),
                    linear-gradient(135deg, var(--color-muted) 50%, transparent 50%);
  background-position: calc(100% - 14px) 52%, calc(100% - 9px) 52%;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.lang:hover { border-color: var(--color-link); }
.lang:focus-visible { outline: 2px solid var(--color-link); outline-offset: 2px; }
/* The options themselves are platform-drawn, so they need an explicit pairing or a dark theme
   gives dark-on-dark on some engines. */
.lang option { background: var(--color-card); color: var(--color-foreground); }

/* COMPACT AND FIXED, at every width. The picker sits in the bar beside the theme switch, and the
   bar has two hard constraints it must not violate:
     - the hamburger is `flex: 0 0 44px` and may not shrink (a 41px target is the defect that
       started this), so any growth here comes out of something else; and
     - `.be-menu__list` is `flex-wrap: wrap`, so on desktop a wide control does not overflow —
       it silently pushes a nav item onto a SECOND ROW and doubles the header height.
   A fixed narrow width satisfies both. The closed control shows a truncated native name; the
   OPEN list is drawn by the platform at its natural width, so nothing is actually lost. */
.hdr-lang { flex: 0 0 auto; }
.lang {
  width: 4.6rem; min-width: 0;
  text-overflow: ellipsis; white-space: nowrap; overflow: hidden;
}

@media (max-width: 1199px) {
  /* Narrower still, and a real touch target height, since the bar also carries the burger. */
  .lang { width: 4rem; min-height: 44px; }
  .lang { background-position: calc(100% - 11px) 52%, calc(100% - 6px) 52%; }
}

/* ---------------------------------------------------------------------------
   CAROUSEL — MARQUEE MODE (opt-in via `data-marquee`)
   A continuous strip for logo bands. The paged carousel rewinds by animating back past every
   item, which on a 99-logo sponsor band is the most visible thing on the page; this never
   travels backwards.

   The track holds the items TWICE and translates exactly -50%, so the restart frame is identical
   to the one before it and the loop has no seam. Duration comes from the item count, so adding a
   sponsor lengthens the strip rather than speeding it up.
   --------------------------------------------------------------------------- */
.carousel--marquee { overflow: hidden; }
.carousel--marquee .carousel__track {
  width: max-content;
  display: flex;
  transform: none;
  transition: none;
  animation: carousel-marquee var(--marquee-duration, 60s) linear infinite;
}
.carousel--marquee .carousel__item {
  /* Sized from the same per-view variable the paged mode uses, so a logo is the same size in
     both and the band does not change height when the mode is switched. */
  flex: 0 0 calc(100vw / var(--carousel-per-view, 5));
  max-width: calc(var(--maxw) / var(--carousel-per-view, 5));
}
@keyframes carousel-marquee {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
/* Pausing on hover is what makes a moving logo readable — and clickable, if one ever links out. */
.carousel--marquee:hover .carousel__track,
.carousel--marquee:focus-within .carousel__track { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  /* Not merely slowed: continuous horizontal motion is the specific thing that triggers
     vestibular symptoms, and a logo band carries no information that is lost by stopping. */
  .carousel--marquee .carousel__track { animation: none; transform: none; }
  .carousel--marquee { overflow-x: auto; }
}

/* ---- The end-of-content share row (blocks/socialshare-pagefooter.html) -------------------
   Styled ONCE here, never in the block's own <style>: a block is injected into pages that do
   not carry its style, which is how the video modal ended up unstyled everywhere but `/`.

   Tokens only, and no `var(--x, #literal)` fallbacks. The first draft of this used
   `var(--surface, #fff)`, `var(--text-primary, #1A1A1A)` and `var(--oapf-turquoise, #3DB5B0)`:
   none of those three tokens exists in this palette, so every rule would have fallen through to
   its literal — painting a WHITE card with near-black text on a dark page, and doing it silently,
   which is exactly the failure a fallback on a guaranteed token hides. The turquoise is also the
   OAPF Foundation shield brand, which this site explicitly does not use. */
.doc-sharebar {
  margin: 2.5rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--color-border);
}
.doc-sharebar__label {
  margin: 0 0 .6rem;
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.share-bar { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
.share-bar__btn {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  padding: .5rem .75rem;
  border-radius: 999px;
  border: 1px solid var(--color-border);
  background: var(--color-card);
  color: var(--color-foreground);
  font: inherit;
  font-size: .82rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.share-bar__btn:hover { border-color: var(--color-primary); }
.share-bar__btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
/* 44px is the minimum comfortable touch target, and these are icon-and-label pills that would
   otherwise land around 32px on a phone — the same floor the hamburger is held to. */
@media (max-width: 767px) {
  .share-bar__btn { min-height: 44px; }
}
.share-bar__btn svg { flex: 0 0 16px; }
/* `hidden` is only a UA rule and any author `display` beats it — the trap the submenu caret
   already paid for. `.share-bar` sets no display on its children, but the native-share button
   starts hidden and is revealed by the delegate, so this makes the attribute authoritative. */
.share-bar__btn[hidden] { display: none; }

/* ---- The assistant, in the drawer and only there -----------------------------------------
   Below the drawer breakpoint the floating launcher is a SECOND thing hovering over the page
   beside the hamburger, and it sits over the last footer line — the overlap the theme toggle and
   the chat launcher already hit once, where fixed chrome shared space with a control instead of
   reserving it. So on a phone and a tablet the launcher is hidden and this row replaces it; at
   desktop this row is hidden and the launcher returns. One affordance at a time.

   1199px is DRAWER_MAX_WIDTH from imports/ui/public/navDrawer.js. It is duplicated here for the
   same reason the drawer's own cutoff is, and carries the same obligation: MOVE ONE, MOVE BOTH —
   a resize past it must force the drawer closed, and this row must disappear with it. */
.hdr-assistant { display: none; }
@media (max-width: 1199px) {
  .hdr-assistant {
    display: block;
    width: 100%;
    margin-top: .6rem;
    padding: .7rem .9rem;
    /* 44px is the comfortable touch minimum the hamburger is already held to. */
    min-height: 44px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-card);
    color: var(--color-foreground);
    font: inherit;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
  }
  .hdr-assistant:hover { border-color: var(--color-primary); }
  .hdr-assistant:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
  /* The launcher stands down while the drawer offers the same thing. `.livechat-panel` is NOT
     hidden: once opened from the drawer the conversation must still be on screen. */
  .livechat-fab { display: none; }
}

/* ---- header search -------------------------------------------------------------------------
   In .hdr-panel, so the stylesheet that turns that element into the drawer carries the field with
   it: one set of controls, a field inline on desktop and inside the drawer on a phone, with no
   second copy and no duplicated data-testid. Tokens only — gen-palette.mjs fails the build on a
   literal colour under private/branding/. */
.hdr-search { display: flex; align-items: center; gap: .35rem; }
.hdr-search-input {
  background: var(--card); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius-sm, 8px); padding: .35rem .6rem; font: inherit; min-width: 0; width: 11rem;
}
.hdr-search-input::placeholder { color: var(--subtle); }
.hdr-search-input:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }
.hdr-search-go {
  background: transparent; color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius-sm, 8px); cursor: pointer;
  /* 44px, the same touch-target floor the hamburger is held to. */
  min-width: 44px; min-height: 44px; line-height: 1; font-size: 1.05rem;
}
.hdr-search-go:focus-visible { outline: 2px solid var(--blue); outline-offset: 1px; }

@media (max-width: 1199px) {
  /* In the drawer the field is a full-width row like every other control there, rather than a
     narrow box floating in a column. */
  .hdr-search { width: 100%; }
  .hdr-search-input { width: 100%; }
}


/* mobile.css */
/* Mobile-first enhancements (F7) — safe-area insets, touch targets, responsive
   nav. Desktop is unaffected (rules are media-scoped or harmless inset padding). */


/* Honor the notch/home-indicator on installed PWA / native shells. */
.header { padding-top: var(--safe-top); }
.footer { padding-bottom: var(--safe-bottom); }

/* No accidental horizontal scroll on small screens. */
html, body { max-width: 100%; overflow-x: hidden; }

@media (max-width: 768px) {
  .nav.container { flex-wrap: wrap; gap: 8px; }
  .nav-links { gap: 4px; overflow-x: auto; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; }
  .nav-link { padding: 10px 12px; min-height: 44px; display: inline-flex; align-items: center; white-space: nowrap; }
  .main.container { padding-left: 16px; padding-right: 16px; }

  /* Comfortable tap targets everywhere. */
  .button, button, select, input, .notif-bell-btn { min-height: 44px; }

  /* Tables scroll horizontally instead of breaking layout. */
  .admin-table { display: block; overflow-x: auto; white-space: nowrap; }

  /* Notification panel goes full-width on phones. */
  .notif-panel { position: fixed !important; left: 8px; right: 8px; top: 56px; width: auto !important; }
}

/* Larger hit area for the bell regardless of viewport. */
.notif-bell-btn { padding: 8px; }
