/* ==========================================================================
   site.css — Iglesia Apostólica "1ra de Adelanto"
   The only stylesheet. Tokens from SPEC.md §2 (the flyer's THEME variable block).
   Navy = hero / header / footer / dark cards. Cream = reading surface.
   Gold = accents, rules, primary buttons only (never body text on cream).
   No web fonts, no @import, no third-party requests.
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Self-hosted type. Files in assets/fonts, SIL OFL licences beside them.
   Archivo  variable, wght 100-900, wdth 62-125 - wordmark, headings, UI, body
   Fraunces variable, wght 100-900 at opsz 40   - scripture and pull quotes
   Pinyon Script                                - the motto line only
   Shipping them means Windows and Android render what macOS renders.
   -------------------------------------------------------------------------- */
@font-face{
  font-family:"Archivo Variable";
  src:url("assets/fonts/archivo-var.woff2") format("woff2");
  font-weight:100 900;font-stretch:62% 125%;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"Fraunces Variable";
  src:url("assets/fonts/fraunces-var.woff2") format("woff2");
  font-weight:100 900;font-style:normal;font-display:swap;
}
@font-face{
  font-family:"Pinyon Script Web";
  src:url("assets/fonts/pinyon-script.woff2") format("woff2");
  font-weight:400;font-style:normal;font-display:swap;
}

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
:root{
  /* navy family (backgrounds) */
  --navy-1:#223a6e;
  --navy-2:#16264d;
  --navy-3:#0a1128;
  /* light family (text on navy, and paper) */
  --ink-on-dark:#f5f1e6;
  --muted-1:#cfd9f0;
  --muted-2:#aebbdd;
  --muted-3:#8fa2cc;
  --display:#f3ead8;
  --verse-on-dark:#c8d3ec;
  --cream:#faf7f2;
  --cream-2:#f3ead8;
  --paper-line:#e6dcc6;
  --white:#ffffff;
  --text:#16264d;
  --text-muted:#4a5877;
  /* accents */
  --gold:#e3c169;
  --gold-deep:#cfa94a;
  --gold-tint:#fbf3dc;
  --hair-gold:rgba(227,193,105,.55);
  --hair-gold-soft:rgba(227,193,105,.35);
  --card-dark:rgba(10,17,40,.55);
  --live:#e8413c;
  --live-glow:rgba(232,65,60,.9);
  --fb:#1877F2;
  --zelle:#6D1ED4;
  --error-text:#8a1c1c;
  --error-bg:#fbe9e7;
  --focus:#16264d;          /* focus ring on cream/white (13.9:1 vs cream) */
  --focus-on-dark:#e3c169;  /* focus ring on navy surfaces (8.5:1 vs navy-2) */
  --big-shadow:rgba(0,0,0,.45);
  --ink-wash:rgba(245,241,230,.12);
  --navy-wash:rgba(22,38,77,.08);
  /* type */
  --font-display:"Archivo Variable","Avenir Next Condensed","Helvetica Neue","Arial Narrow",sans-serif;
  --font-body:"Archivo Variable","Avenir Next",-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  --font-script:"Pinyon Script Web","Snell Roundhand","Brush Script MT",cursive;
  --font-serif:"Fraunces Variable","Iowan Old Style",Palatino,Georgia,serif;
  /* type scale (fluid; mobile → desktop) */
  --fs-body:1.0625rem;
  --fs-small:0.9375rem;
  --fs-lead:clamp(1.2rem,2.2vw,1.375rem);
  --fs-h3:clamp(1.25rem,2.4vw,1.5rem);
  --fs-h2:clamp(1.75rem,4.5vw,2.5rem);
  --fs-h1:clamp(2.25rem,6vw,3.5rem);
  --fs-hero:clamp(2.75rem,9vw,5.75rem);
  --fs-eyebrow:0.95rem;
  --fs-script:clamp(1.5rem,3.4vw,2.25rem);
  --lh-body:1.6;
  --lh-tight:1.05;
  /* spacing */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px; --s8:64px; --s9:96px;
  --section-y:clamp(48px,8vw,96px);
  --container:1100px;
  --container-narrow:760px;
  --gutter:clamp(20px,4vw,32px);
  /* shape */
  --r-sm:8px; --r-md:14px; --r-lg:22px; --r-pill:999px;
  --shadow-1:0 6px 24px rgba(10,17,40,.12);
  --shadow-2:0 14px 40px rgba(10,17,40,.22);
  --dove-glow:drop-shadow(0 0 32px rgba(170,200,255,.5)) drop-shadow(0 10px 24px rgba(0,0,0,.3));
  --header-h:64px;
}

/* --------------------------------------------------------------------------
   2. Reset and base
   -------------------------------------------------------------------------- */
*,*::before,*::after{box-sizing:border-box}
[hidden]{display:none!important}
html{
  -webkit-text-size-adjust:100%;
  text-size-adjust:100%;
  scroll-behavior:smooth;
  scroll-padding-top:calc(var(--header-h) + var(--s5));
  overflow-x:clip;
}
body{
  margin:0;
  font:400 var(--fs-body)/var(--lh-body) var(--font-body);
  color:var(--text);
  background:var(--cream);
  overflow-x:clip;
  overflow-wrap:anywhere;
  -webkit-font-smoothing:antialiased;
}
img,svg,video,audio{max-width:100%;height:auto;display:block}
h1,h2,h3,h4,p,figure,address,ul,ol{margin:0}
address{font-style:normal}
ul,ol{padding-left:1.25em}
a{color:inherit;text-underline-offset:3px}
a:hover{text-decoration-thickness:2px}
button{font:inherit;color:inherit}
strong{font-weight:700}
code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.95em}
:focus-visible{outline:3px solid var(--focus);outline-offset:3px}
.site-header :focus-visible,.hero :focus-visible,.page-hero :focus-visible,.section--dark :focus-visible,.card--dark :focus-visible,.infocard :focus-visible,.site-footer :focus-visible,.skip-link:focus-visible{outline-color:var(--focus-on-dark)}
:focus:not(:focus-visible){outline:none}

/* Headings */
h1,h2,h3,.hero-display{font-family:var(--font-display);font-weight:700;line-height:var(--lh-tight);letter-spacing:0}
h1{font-size:var(--fs-h1)}
h2{font-size:var(--fs-h2)}
h3{font-size:var(--fs-h3);line-height:1.15}
.h3{font-size:var(--fs-h3)}
p + p{margin-top:var(--s4)}
.lead{font-size:var(--fs-lead);line-height:1.5}
.small{font-size:var(--fs-small);color:var(--text-muted)}
.center{text-align:center}

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */
.container{width:100%;max-width:var(--container);margin-inline:auto;padding-inline:var(--gutter)}
.container--narrow{max-width:var(--container-narrow)}
.section{padding-block:var(--section-y)}
.section--tight{padding-block:calc(var(--section-y) / 2)}
.section--alt{background:var(--cream-2)}
.section--dark{background:var(--navy-2);color:var(--ink-on-dark)}
.section--dark h1,.section--dark h2,.section--dark h3{color:var(--ink-on-dark)}
.section--dark .small{color:var(--muted-2)}
.section-head{max-width:60ch;margin-bottom:var(--s6)}
.section-head--center{margin-inline:auto;text-align:center}
.section-head h2{margin-top:var(--s2)}
.section-head .lead{margin-top:var(--s4);color:var(--text-muted)}
.section--dark .section-head .lead{color:var(--muted-1)}
.section-foot{margin-top:var(--s6)}
.grid-2,.grid-3{display:grid;gap:var(--s5);grid-template-columns:1fr}
.grid--center{align-items:center}
@media (min-width:600px){
  .grid-2,.grid-3{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (min-width:900px){
  .grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}
}
.visually-hidden{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* Eyebrow */
.eyebrow{
  font:600 var(--fs-eyebrow)/1.3 var(--font-display);
  text-transform:uppercase;
  letter-spacing:.22em;
  color:var(--text-muted);
  margin:0;
}
.section--dark .eyebrow,.hero .eyebrow,.page-hero .eyebrow,.card--dark .eyebrow,.site-footer .eyebrow,.infocard .eyebrow{color:var(--gold)}
h2.eyebrow{font-size:var(--fs-eyebrow)}

/* Gold rule */
.rule-gold{
  height:1px;width:min(340px,60%);margin:var(--s5) auto;border:0;
  background:linear-gradient(90deg,transparent,var(--gold),transparent);
}

/* --------------------------------------------------------------------------
   4. Skip link
   -------------------------------------------------------------------------- */
.skip-link{
  position:absolute;left:var(--s3);top:-100px;z-index:100;
  padding:var(--s3) var(--s4);border-radius:var(--r-sm);
  background:var(--gold);color:var(--text);font-weight:700;text-decoration:none;
}
.skip-link:focus{top:var(--s3);outline-offset:0}

/* --------------------------------------------------------------------------
   5. Header, wordmark, language toggle, navigation
   -------------------------------------------------------------------------- */
.site-header{
  position:sticky;top:0;z-index:50;
  background:var(--navy-3);color:var(--ink-on-dark);
  border-bottom:1px solid var(--hair-gold-soft);
}
.header-row{
  display:flex;align-items:center;flex-wrap:wrap;
  gap:var(--s2);min-height:var(--header-h);
}
.brand{
  display:inline-flex;align-items:center;gap:var(--s3);
  margin-right:auto;min-height:44px;padding-block:var(--s2);
  color:var(--ink-on-dark);text-decoration:none;
}
.brand-dove{width:40px;height:40px;flex-shrink:0;filter:drop-shadow(0 0 10px rgba(170,200,255,.45))}
.brand-text{display:flex;flex-direction:column;line-height:1.1}
.brand-name{font:700 1.05rem/1.1 var(--font-display);letter-spacing:.14em;color:var(--ink-on-dark)}
.brand-sub{font-size:.75rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted-2);margin-top:2px}
@media (max-width:399px){.brand-sub{display:none}}
@media (max-width:479px){
  .brand{gap:var(--s2)}
  .brand-name{font-size:.95rem;letter-spacing:.08em}
  .lang-toggle,.nav-toggle{padding:0 10px;letter-spacing:.06em}
}

.lang-toggle,.nav-toggle{
  display:inline-flex;align-items:center;justify-content:center;
  min-height:44px;min-width:44px;padding:0 14px;
  border-radius:var(--r-pill);background:var(--navy-3);
  font:700 .95rem/1 var(--font-display);letter-spacing:.1em;text-transform:uppercase;
  cursor:pointer;transition:background-color .2s,color .2s;
}
.lang-toggle{border:1px solid var(--gold);color:var(--gold)}
.lang-toggle:hover{background:var(--gold);color:var(--text)}
.nav-toggle{border:1px solid var(--muted-2);color:var(--ink-on-dark)}
.nav-toggle:hover{background:var(--ink-wash)}
.nav-toggle[aria-expanded="true"]{background:var(--navy-2);border-color:var(--gold)}

/* Phone menu: full-bleed panel under the header */
.nav{
  display:none;flex-basis:calc(100% + 2 * var(--gutter));
  margin-inline:calc(-1 * var(--gutter));
  background:var(--navy-2);
  border-top:1px solid var(--hair-gold-soft);
}
.nav.is-open{display:block}
.nav ul{list-style:none;margin:0;padding:var(--s2) 0}
.nav li + li{border-top:1px solid var(--hair-gold-soft)}
.nav a{
  display:flex;align-items:center;min-height:52px;
  padding:0 var(--gutter);
  color:var(--ink-on-dark);font:600 1rem/1.2 var(--font-body);text-decoration:none;
  transition:background-color .2s;
}
.nav a:hover{background:var(--ink-wash)}
.nav a[aria-current="page"]:not(.btn){
  text-decoration:underline;text-decoration-color:var(--gold);
  text-decoration-thickness:2px;text-underline-offset:6px;
}
.nav .nav-cta{border-top:0;padding:var(--s3) var(--gutter) var(--s2)}
.nav a.btn-nav{min-height:44px;justify-content:center}

@media (min-width:900px){
  .header-row{flex-wrap:nowrap;gap:var(--s4)}
  .nav-toggle{display:none}
  .nav{display:block;flex-basis:auto;margin:0;background:transparent;border:0;order:1}
  .lang-toggle{order:2}
  .nav ul{display:flex;align-items:center;gap:var(--s5);padding:0}
  .nav li + li{border:0}
  .nav .nav-cta{padding:0}
  .nav a{min-height:44px;padding:0 var(--s1);border-radius:var(--r-sm)}
  .nav a:hover{background:transparent;text-decoration:underline;text-decoration-color:var(--gold);text-decoration-thickness:2px;text-underline-offset:6px}
  .nav a.btn-nav{min-height:44px;padding:8px 20px;margin-left:var(--s2)}
}

/* --------------------------------------------------------------------------
   6. Buttons
   -------------------------------------------------------------------------- */
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:var(--s2);
  min-height:48px;min-width:44px;padding:12px 24px;
  border-radius:var(--r-pill);border:2px solid transparent;
  font:600 var(--fs-body)/1 var(--font-body);text-decoration:none;text-align:center;
  cursor:pointer;background:transparent;
  transition:background-color .2s,color .2s,border-color .2s,opacity .2s;
}
.btn:hover{text-decoration:none}
.btn:disabled,.btn[aria-disabled="true"]{opacity:.65;cursor:default}
.btn-primary{background:var(--gold);color:var(--text);border-color:var(--gold)}
.btn-primary:hover{background:var(--gold-deep);border-color:var(--gold-deep);color:var(--text)}
.btn-outline{color:var(--text);border-color:var(--text)}
.btn-outline:hover{background:var(--navy-wash)}
.hero .btn-outline,.page-hero .btn-outline,.section--dark .btn-outline,.card--dark .btn-outline,.site-footer .btn-outline,.infocard .btn-outline{
  color:var(--ink-on-dark);border-color:var(--ink-on-dark);
}
.hero .btn-outline:hover,.page-hero .btn-outline:hover,.section--dark .btn-outline:hover,.card--dark .btn-outline:hover,.site-footer .btn-outline:hover,.infocard .btn-outline:hover{
  background:var(--ink-wash);
}
.btn-nav{min-height:44px;padding:8px 20px}
.btn-sm{min-height:44px;padding:8px 18px;font-size:1rem}
.btn-row{display:flex;flex-wrap:wrap;gap:var(--s3);align-items:center}
.btn-row--center{justify-content:center}
.hero .btn-row{justify-content:center}
@media (max-width:599px){
  .btn-block{width:100%}
}

/* Live dot */
.live-dot{
  display:inline-block;width:11px;height:11px;border-radius:50%;flex-shrink:0;
  background:var(--live);box-shadow:0 0 8px var(--live-glow);vertical-align:middle;
}
.eyebrow .live-dot{margin-right:var(--s2);position:relative;top:-1px}

/* Facebook line */
.fb-link{display:inline-flex;align-items:center;gap:var(--s3);text-decoration:none;min-height:44px}
.fb-link:hover{text-decoration:underline;text-decoration-thickness:2px}
.fb-badge{
  display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;
  width:34px;height:34px;border-radius:8px;background:var(--fb);
}
.fb-badge svg{width:18px;height:18px}

/* Text links that must be 44 px tap targets (map links, footer pages, episode links) */
.map-links a,.footer-nav a,.episode .links a{display:inline-flex;align-items:center;min-height:44px;padding-block:0;text-decoration:underline;font-weight:600}
.map-links a:hover,.episode .links a:hover{text-decoration-thickness:2px}

/* --------------------------------------------------------------------------
   7. Hero (home) and page hero (inner pages)
   -------------------------------------------------------------------------- */
.hero{
  position:relative;isolation:isolate;overflow:hidden;
  min-height:min(92vh,860px);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  text-align:center;color:var(--ink-on-dark);
  padding:var(--s6) 0 var(--s8);
  background:radial-gradient(ellipse 120% 80% at 50% 20%,var(--navy-1),var(--navy-2) 45%,var(--navy-3));
}
.hero-photo{
  position:absolute;inset:0;z-index:-2;
  width:100%;height:100%;object-fit:cover;object-position:center;
  opacity:.18;mix-blend-mode:luminosity;
}
.hero-rays{
  position:absolute;top:-4%;left:50%;transform:translateX(-50%);z-index:-1;
  width:min(1200px,170vw);height:auto;opacity:.12;pointer-events:none;
}
.hero-inner{position:relative;display:flex;flex-direction:column;align-items:center}
/* Scrim behind the hero text so copy stays ≥ 4.5:1 over bright photo regions */
.hero-inner::before{content:"";position:absolute;inset:-32px;z-index:-1;border-radius:50%;background:radial-gradient(ellipse at center,rgba(10,17,40,.6),rgba(10,17,40,0) 72%);pointer-events:none}
.hero .rule-gold{margin:var(--s4) auto}
.hero-dove{
  width:min(62vw,340px);height:auto;margin:var(--s2) auto;
  filter:var(--dove-glow);
}
.hero-eyebrow{margin-top:var(--s2);color:var(--muted-1)}
.hero .hero-eyebrow{color:var(--gold)}
.hero-display{
  font-size:var(--fs-hero);line-height:.95;
  text-transform:uppercase;letter-spacing:.04em;
  color:var(--display);text-shadow:0 6px 40px var(--big-shadow);
  margin:0;
}
.hero-sub{
  margin-top:var(--s4);font-size:var(--fs-lead);line-height:1.4;
  color:var(--ink-on-dark);font-weight:500;max-width:36ch;
}
.hero-invite{margin-top:var(--s3);font-style:italic;color:var(--muted-1);max-width:40ch}
.hero .btn-row{margin-top:var(--s5)}
@media (max-height:700px){
  .hero{padding:var(--s4) 0 var(--s6);min-height:0}
  .hero-dove{width:min(36vw,200px);margin:0 auto}
  .hero .rule-gold{margin:var(--s3) auto}
  .hero-sub{margin-top:var(--s3)}
  .hero-invite{margin-top:var(--s2)}
  .hero .btn-row{margin-top:var(--s4)}
}

.motto{
  margin:0;
  font:700 clamp(1.05rem,3.4vw,1.5rem)/1.25 var(--font-display);
  text-transform:uppercase;letter-spacing:.2em;color:var(--ink-on-dark);
}
.motto .motto-accent{color:var(--gold)}
.motto .motto-en{
  display:block;margin-top:var(--s1);
  font:400 var(--fs-script)/1.2 var(--font-script);
  text-transform:none;letter-spacing:0;color:var(--muted-1);
}
.script{font:400 var(--fs-script)/1.2 var(--font-script)}

.page-hero{
  color:var(--ink-on-dark);text-align:center;
  padding-block:clamp(40px,7vw,72px);
  background:radial-gradient(ellipse 120% 80% at 50% 20%,var(--navy-1),var(--navy-2) 45%,var(--navy-3));
}
.page-hero h1{margin-top:var(--s3);color:var(--display)}
.page-hero .page-hero-sub{margin-top:var(--s3);font-size:var(--fs-lead);color:var(--muted-1)}

/* --------------------------------------------------------------------------
   8. Cards, infocard, figure, verse, empty
   -------------------------------------------------------------------------- */
.card{
  background:var(--white);border:1px solid var(--paper-line);
  border-radius:var(--r-md);box-shadow:var(--shadow-1);padding:var(--s5);
}
.card h3{margin-bottom:var(--s3)}
.card h2{margin-bottom:var(--s3)}
.card .eyebrow + h3,.card .eyebrow + h2{margin-top:var(--s2)}
.card--dark{
  background:var(--navy-2);color:var(--ink-on-dark);
  border:1px solid var(--hair-gold-soft);box-shadow:var(--shadow-2);
}
.card--dark h2,.card--dark h3{color:var(--ink-on-dark)}
.card--dark .small{color:var(--muted-2)}
.card--band{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s5);padding:var(--s6)}
.card--band > div{flex:1 1 320px}

.infocard{
  border:1px solid var(--hair-gold);border-radius:var(--r-md);
  background:var(--card-dark);color:var(--ink-on-dark);
  display:flex;flex-direction:column;padding:var(--s3) var(--s3);
}
.infocard .cell{
  flex:1;display:flex;flex-direction:column;justify-content:center;gap:var(--s1);
  padding:var(--s4) var(--s4);text-align:center;
}
.infocard .cell + .cell{border-top:1px solid var(--hair-gold-soft)}
.infocard .lab{
  font:600 .9rem/1.3 var(--font-display);letter-spacing:.24em;text-transform:uppercase;color:var(--gold);
  display:inline-flex;align-items:center;justify-content:center;gap:var(--s2);
}
.infocard .val{font-size:1.4rem;font-weight:600;line-height:1.3;color:var(--ink-on-dark)}
.infocard .val a{color:inherit}
.infocard .val .fb-link{justify-content:center;font-size:1.15rem;text-align:left}
.infocard .sub{color:var(--muted-2)}
@media (min-width:700px){
  .infocard{flex-direction:row;padding:var(--s5) var(--s2)}
  .infocard .cell + .cell{border-top:0;border-left:1px solid var(--hair-gold-soft)}
  .infocard .cell--wide{flex:1.35}
  .infocard .cell--wider{flex:1.55}
}

.figure{border-radius:var(--r-lg);overflow:hidden;margin:0;box-shadow:var(--shadow-1)}
.figure img{width:100%;height:auto}
.figure figcaption{font-size:var(--fs-small);color:var(--text-muted);padding:var(--s3) var(--s4);background:var(--white)}
.section--dark .figure figcaption{background:var(--navy-3);color:var(--muted-2)}

.verse{
  font:italic 400 var(--fs-lead)/1.5 var(--font-serif);
  color:var(--text);max-width:60ch;margin:0;
}
.section--dark .verse,.hero .verse,.card--dark .verse{color:var(--verse-on-dark)}
.verse-ref{
  display:block;margin-top:var(--s3);
  font:600 .95rem/1.3 var(--font-display);font-style:normal;
  letter-spacing:.12em;text-transform:uppercase;color:var(--text-muted);
}
.section--dark .verse-ref,.hero .verse-ref,.card--dark .verse-ref{color:var(--gold)}
.verse-block{margin-top:var(--s6);padding-left:var(--s5);border-left:3px solid var(--gold)}
.verse-block--center{border-left:0;padding-left:0;text-align:center;margin-inline:auto}

.family{
  font:600 clamp(1.1rem,3vw,1.5rem)/1.3 var(--font-display);
  letter-spacing:.14em;text-transform:uppercase;text-align:center;
  margin-top:var(--s6);color:var(--text);
}
.section--dark .family{color:var(--ink-on-dark)}

.empty{
  background:var(--cream-2);border:1px dashed var(--paper-line);border-radius:var(--r-md);
  padding:var(--s6) var(--s5);text-align:center;color:var(--text);
}
.section--alt .empty{background:var(--cream);}
.empty h3{margin-bottom:var(--s3)}
.empty .btn-row{justify-content:center;margin-top:var(--s5)}
.empty .map-links,.empty .links{margin-top:var(--s4)}

/* --------------------------------------------------------------------------
   9. JSON-driven rows: events, episodes, newsletters
   -------------------------------------------------------------------------- */
.events-list{display:grid;gap:var(--s5);grid-template-columns:1fr}
@media (min-width:600px){.events-list{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:900px){.events-list{grid-template-columns:repeat(3,minmax(0,1fr))}}
.events-list--rows{grid-template-columns:1fr}
.event{display:flex;flex-direction:column;gap:var(--s3)}
.event h3{margin:0}
.event-when{font-weight:600;color:var(--text)}
.card--dark .event-when{color:var(--ink-on-dark)}
.event .btn-row{margin-top:auto;padding-top:var(--s2)}
.events-next{margin-top:var(--s5);text-align:center}
.events-next p{font-size:var(--fs-lead);font-weight:600}
.events-next .btn-row{justify-content:center;margin-top:var(--s4)}
.event-next-card{max-width:var(--container-narrow);margin-inline:auto}
.event-next-card h3{font-size:var(--fs-h2)}

.episode{display:flex;flex-direction:column;gap:var(--s3)}
.episode + .episode{margin-top:var(--s5)}
.episode-meta{color:var(--text-muted);font-size:var(--fs-small)}
.episode audio{width:100%}
.episode .links{display:flex;flex-wrap:wrap;gap:var(--s4)}

.newsletter-item{display:flex;flex-direction:column;gap:var(--s2)}
.newsletter-item + .newsletter-item{margin-top:var(--s4)}
.newsletter-item time{font-size:var(--fs-small);color:var(--text-muted)}
.newsletter-item h3 a{text-decoration:underline}

/* --------------------------------------------------------------------------
   10. Forms
   -------------------------------------------------------------------------- */
.form{max-width:var(--container-narrow)}
.form--compact{max-width:none}
.field{margin-bottom:var(--s4)}
.field label{display:block;font-weight:600;margin-bottom:var(--s2)}
.field input,.field textarea,.field select{
  display:block;width:100%;min-height:48px;
  padding:10px 14px;border:1px solid var(--text-muted);border-radius:var(--r-sm);
  background:var(--white);color:var(--text);
  font:400 1rem/1.4 var(--font-body);
}
.field textarea{min-height:140px;resize:vertical}
.field input:focus,.field textarea:focus{border-color:var(--text)}
.field-row{display:grid;gap:var(--s4);grid-template-columns:1fr}
@media (min-width:600px){.field-row{grid-template-columns:repeat(2,minmax(0,1fr))}}
.radio-group{display:flex;flex-wrap:wrap;gap:var(--s4)}
.radio-group label,.checkbox label{cursor:pointer}
.radio,.checkbox{
  display:flex;align-items:flex-start;gap:var(--s3);
  min-height:44px;padding:var(--s2) 0;
}
.radio{align-items:center}
.radio input,.checkbox input{
  width:22px;height:22px;flex-shrink:0;margin:3px 0 0;
  accent-color:var(--navy-2);
}
.radio input{margin-top:0}
.form .btn{margin-top:var(--s2)}
.form .small{margin-top:var(--s3)}
fieldset{border:0;padding:0;margin:0 0 var(--s4)}
legend{font-weight:600;margin-bottom:var(--s2);padding:0}
.hp{position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden}
.form-msg{
  padding:var(--s4) var(--s5);border-radius:var(--r-sm);margin-bottom:var(--s4);
  color:var(--text);
}
.form-msg--ok{background:var(--gold-tint);border:1px solid var(--gold)}
.form-msg--error{background:var(--error-bg);border-left:4px solid var(--error-text)}
.form-msg .fb-link{margin-top:var(--s3)}
.form[aria-busy="true"]{opacity:.85}

/* --------------------------------------------------------------------------
   11. Footer
   -------------------------------------------------------------------------- */
.site-footer{
  background:linear-gradient(180deg,var(--navy-2),var(--navy-3));
  border-top:1px solid var(--hair-gold-soft);
  color:var(--muted-1);padding-block:var(--s8) var(--s6);
}
.footer-grid{display:grid;gap:var(--s6);grid-template-columns:1fr}
@media (min-width:600px){.footer-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (min-width:900px){.footer-grid{grid-template-columns:1.3fr 1fr 1fr .8fr}}
.site-footer .eyebrow{margin-bottom:var(--s3)}
.site-footer p + p{margin-top:var(--s3)}
.site-footer a{color:var(--ink-on-dark)}
.site-footer strong{color:var(--ink-on-dark)}
.footer-brand img{filter:drop-shadow(0 0 14px rgba(170,200,255,.45));margin-bottom:var(--s3)}
.footer-name{font:700 1.25rem/1.2 var(--font-display);letter-spacing:.06em;color:var(--ink-on-dark)}
.footer-brand .motto{margin-top:var(--s3);font-size:1rem;letter-spacing:.16em}
.footer-brand .motto .motto-en{font-size:1.5rem}
.footer-nav{list-style:none;padding:0;margin:0}
.footer-nav a{text-decoration:none}
.footer-nav a:hover{text-decoration:underline}
.footer-bottom{margin-top:var(--s7);padding-top:var(--s5);border-top:1px solid var(--hair-gold-soft);font-size:var(--fs-small);color:var(--muted-2)}
.site-footer .footer-bottom .small{color:var(--muted-2)}
.site-footer .fb-link{color:var(--ink-on-dark)}

/* --------------------------------------------------------------------------
   12. Page-specific helpers (shared so every builder can use them)
   -------------------------------------------------------------------------- */
.zelle-tile{
  background:var(--white);padding:var(--s5);border:2px solid var(--zelle);
  border-radius:var(--r-md);text-align:center;max-width:380px;margin-inline:auto;
}
.zelle-tile img{max-width:300px;margin-inline:auto}
.zelle-word{display:block;margin-top:var(--s3);color:var(--zelle);font-weight:700;font-size:1.4rem}
.kv{display:grid;gap:var(--s4)}
.kv .lab{font:600 .9rem/1.3 var(--font-display);letter-spacing:.2em;text-transform:uppercase;color:var(--text-muted)}
.kv .val{font-size:1.4rem;font-weight:600;line-height:1.3;overflow-wrap:anywhere}

/* --------------------------------------------------------------------------
   13. Motion, print
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}
}
@media print{
  .site-header .nav,.nav-toggle,.lang-toggle,.footer-nav,.skip-link,.hero-rays,.hero-photo,.btn-row button{display:none!important}
  .site-header,.hero,.page-hero,.section--dark,.site-footer,.card--dark,.infocard{background:var(--white)!important;color:var(--text)!important;box-shadow:none!important}
  .hero-display,.page-hero h1,.section--dark h2,.section--dark h3,.card--dark h2,.card--dark h3,.infocard .val,.motto,.footer-name,.brand-name{color:var(--text)!important;text-shadow:none!important}
  .site-footer a,.section--dark a,.card--dark a,.infocard a,.infocard .sub,.section--dark .verse,.section--dark p,.site-footer p{color:var(--text)!important}
  .map-links a::after,.fb-link::after{content:" (" attr(href) ")";font-size:.85em;word-break:break-all}
  .hero{min-height:0}
  .card,.figure{box-shadow:none!important}
  a{text-decoration:underline}
}

/* --------------------------------------------------------------------------
   Home: welcome video band
   -------------------------------------------------------------------------- */
.section--video{background:var(--navy-3)}
.video-frame{max-width:1100px;margin-inline:auto;border-radius:16px;overflow:hidden;background:#000;
  border:1px solid var(--hair-gold);box-shadow:0 24px 60px rgba(0,0,0,.45)}
.site-video{display:block;width:100%;height:auto;aspect-ratio:16/9;background:#000}
.video-note{text-align:center;margin-top:var(--s4);color:var(--muted-2)}
@media (max-width:640px){.video-frame{border-radius:10px}}

/* --------------------------------------------------------------------------
   Home: the welcome clip runs behind the WHOLE page (fixed), and every section
   floats over it as a translucent panel with a gold border, rising into view
   as it arrives. The poster frame stands in until the video plays.
   -------------------------------------------------------------------------- */
body.page-glass{background:var(--navy-3)}
.page-video{position:fixed;inset:0;z-index:-3;width:100%;height:100%;object-fit:cover;object-position:center 40%;background:var(--navy-3);pointer-events:none}
.page-shade{position:fixed;inset:0;z-index:-2;pointer-events:none;
  background:linear-gradient(180deg,rgba(10,17,40,.28) 0%,rgba(10,17,40,.42) 55%,rgba(10,17,40,.66) 100%)}
body.page-glass .hero{background:transparent;isolation:auto}
body.page-glass .hero-rays{opacity:.07}
body.is-video .hero-dove{display:none}
body.page-glass main>section:not(.hero){
  width:min(100% - 2 * var(--gutter),var(--container));
  margin:var(--s6) auto;
  border:1px solid var(--hair-gold);border-radius:20px;overflow:hidden;
  background:rgba(250,247,242,.36);
  -webkit-backdrop-filter:blur(22px) saturate(1.1);backdrop-filter:blur(22px) saturate(1.1);
  box-shadow:0 24px 60px rgba(0,0,0,.35);
}
body.page-glass main>section.section--alt{background:rgba(243,234,216,.36)}
body.page-glass main>section.section--dark{background:rgba(22,38,77,.48)}
body.page-glass .site-header{background:rgba(10,17,40,.5);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}
body.page-glass .site-footer{background:rgba(10,17,40,.5);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-top:1px solid var(--hair-gold)}
html:not(.no-js) body.page-glass main>section:not(.hero){opacity:0;transform:translateY(28px);transition:opacity .8s ease,transform .8s ease}
html:not(.no-js) body.page-glass main>section.is-in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){
  html:not(.no-js) body.page-glass main>section:not(.hero){opacity:1;transform:none;transition:none}
}
@media (max-width:640px){
  body.page-glass main>section:not(.hero){border-radius:14px;margin:var(--s5) auto}
}

/* --------------------------------------------------------------------------
   Home: modern pass — the clip stays vivid; header, panels and footer float
   as light glass with a bright top edge; reveals ease in with a touch of scale
   -------------------------------------------------------------------------- */
body.page-glass .page-video{filter:saturate(1.12) contrast(1.04)}
body.page-glass .page-shade{
  background:
    radial-gradient(120% 90% at 50% 38%,rgba(10,17,40,0) 0%,rgba(10,17,40,.10) 55%,rgba(10,17,40,.55) 100%),
    linear-gradient(180deg,rgba(10,17,40,.16) 0%,rgba(10,17,40,.06) 45%,rgba(10,17,40,.38) 100%);
}
body.page-glass .hero-inner::before{background:radial-gradient(ellipse at center,rgba(10,17,40,.55),rgba(10,17,40,0) 70%)}
body.page-glass .hero-display{text-shadow:0 4px 30px rgba(10,17,40,.7),0 1px 2px rgba(10,17,40,.5)}
body.page-glass .hero-sub,body.page-glass .hero-invite,body.page-glass .hero-eyebrow,body.page-glass .motto{text-shadow:0 1px 14px rgba(10,17,40,.6)}
body.page-glass .site-header{
  top:10px;width:min(100% - 2 * var(--gutter),1280px);margin:10px auto 0;
  border-radius:28px;border:1px solid rgba(255,255,255,.16);border-bottom-color:rgba(255,255,255,.16);
  background:rgba(10,17,40,.42);
  -webkit-backdrop-filter:blur(20px) saturate(1.2);backdrop-filter:blur(20px) saturate(1.2);
  box-shadow:0 12px 40px rgba(0,0,0,.28),inset 0 1px 0 rgba(255,255,255,.14);
}
body.page-glass .site-header .nav{margin-inline:0;flex-basis:100%;border-radius:18px;background:rgba(10,17,40,.55);border-top:0;margin-top:var(--s2);margin-bottom:var(--s2)}
body.page-glass .site-header .nav a{padding-inline:var(--s4)}
body.page-glass main>section:not(.hero){
  border-radius:24px;border:1px solid rgba(255,255,255,.28);
  -webkit-backdrop-filter:blur(24px) saturate(1.15);backdrop-filter:blur(24px) saturate(1.15);
  box-shadow:0 30px 80px rgba(0,0,0,.30),inset 0 1px 0 rgba(255,255,255,.45);
}
body.page-glass main>section.section--dark{border-color:rgba(227,193,105,.45);box-shadow:0 30px 80px rgba(0,0,0,.30),inset 0 1px 0 rgba(255,255,255,.12)}
body.page-glass .site-footer{width:min(100% - 2 * var(--gutter),var(--container));margin:0 auto var(--s6);border-radius:24px;border:1px solid rgba(255,255,255,.16);background:rgba(10,17,40,.45)}
html:not(.no-js) body.page-glass main>section:not(.hero){transform:translateY(24px) scale(.985);transition:opacity .9s cubic-bezier(.2,.7,.2,1),transform .9s cubic-bezier(.2,.7,.2,1)}
html:not(.no-js) body.page-glass main>section.is-in{transform:none}
body.page-glass .btn{transition:transform .2s,box-shadow .2s,background-color .2s,color .2s}
body.page-glass .btn:hover{transform:translateY(-1px);box-shadow:0 10px 26px rgba(0,0,0,.25)}
@media (max-width:640px){
  body.page-glass .site-header{top:6px;margin-top:6px;border-radius:20px}
  body.page-glass main>section:not(.hero),body.page-glass .site-footer{border-radius:18px}
}
body.page-glass .brand-name,body.page-glass .brand-sub,body.page-glass .lang-toggle,body.page-glass .nav-toggle{white-space:nowrap}
body.page-glass .site-header .header-row{flex-wrap:nowrap}
@media (max-width:899px){body.page-glass .site-header .header-row{flex-wrap:wrap}}
@media (min-width:900px){body.page-glass .site-header .nav a{padding-inline:var(--s3)}}
body.page-glass .site-header .nav a{white-space:nowrap}
@media (min-width:900px) and (max-width:1139px){
  body.page-glass .site-header .nav a{font-size:.9rem;padding-inline:10px}
  body.page-glass .brand-sub{display:none}
}
@media (max-width:899px){
  body.page-glass .site-header{width:calc(100% - 16px);margin:6px 8px 0;border-radius:20px}
  body.page-glass .site-header .container{padding-inline:12px}
  body.page-glass .site-header .nav{margin-inline:-12px;flex-basis:calc(100% + 24px);border-radius:0 0 18px 18px;margin-bottom:0}
}
/* inner pages: the title band is a dark glass panel too */
body.page-glass main>section.page-hero{background:rgba(22,38,77,.48);border-color:rgba(227,193,105,.45);box-shadow:0 30px 80px rgba(0,0,0,.30),inset 0 1px 0 rgba(255,255,255,.12)}
body.page-glass .page-hero--photo .hero-photo{opacity:.22}

/* ==========================================================================
   14. Craft pass — self-hosted type, material depth, choreographed motion.
   Everything below is additive. Deleting this whole block and the @font-face
   rules at the top of the file returns the site to the previous look.
   ========================================================================== */

/* 14.1 Typography ---------------------------------------------------------
   Archivo carries the wordmark, headings, UI and body at three widths;
   Fraunces carries scripture; Pinyon carries the motto. Because all three
   are shipped with the site, Windows and Android now render exactly what
   macOS renders — previously they fell back to Arial Narrow.            */
body{
  font-optical-sizing:auto;
  font-synthesis-weight:none;
  font-feature-settings:"kern" 1,"calt" 1;
  -moz-osx-font-smoothing:grayscale;
}
h1,h2,h3,.h3{font-stretch:88%;letter-spacing:-.015em;text-wrap:balance}
p,li,figcaption,blockquote{text-wrap:pretty}
.lead{font-weight:380;letter-spacing:-.004em}
.hero-display{font-stretch:78%;font-weight:750;letter-spacing:.03em}
.page-hero h1{font-stretch:80%;font-weight:720;letter-spacing:.015em;text-transform:uppercase}
.brand-name{font-stretch:74%;font-weight:760;letter-spacing:.15em}
.eyebrow,.infocard .lab{font-stretch:86%;font-weight:640}
.nav a,.btn,.footer-nav a{font-stretch:92%}
.btn{font-weight:640;letter-spacing:.015em}
.infocard .val{font-stretch:92%;font-weight:620;letter-spacing:-.01em}
.verse{
  font-family:var(--font-serif);font-weight:360;font-style:normal;
  letter-spacing:-.008em;line-height:1.42;
}
.verse-ref{font-stretch:86%;font-weight:620}
.motto{font-stretch:82%;font-weight:720}
.hero-invite{font-family:var(--font-serif);font-weight:360;font-style:italic}
::selection{background:var(--gold);color:var(--navy-3)}
::marker{color:var(--gold-deep)}

/* 14.2 Material — a whisper of film grain over the whole page kills the
   banding that fixed video plus large gradients always produces.        */
body.page-glass::after{
  content:"";position:fixed;inset:0;z-index:-1;pointer-events:none;opacity:.35;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.4'/%3E%3C/svg%3E");
  background-size:160px 160px;
}

/* A slow light drifts across the clip, the way sun moves through a window. */
body.page-glass .page-shade::after{
  content:"";position:absolute;inset:-30%;pointer-events:none;
  background:radial-gradient(42% 32% at 24% 16%,rgba(255,240,205,.20),rgba(255,240,205,0) 62%);
  animation:craft-sheen 24s ease-in-out infinite alternate;
}
@keyframes craft-sheen{
  from{transform:translate3d(-5%,-3%,0) scale(1)}
  to{transform:translate3d(8%,5%,0) scale(1.12)}
}

/* 14.3 Panels — a gold light-edge on top, deeper ambient shadow below,
   and a hairline that catches the light along the leading edge.         */
body.page-glass main>section:not(.hero){
  position:relative;
  box-shadow:
    0 2px 6px rgba(10,17,40,.10),
    0 12px 28px rgba(10,17,40,.14),
    0 40px 90px rgba(10,17,40,.30),
    inset 0 1px 0 rgba(255,255,255,.50);
}
body.page-glass main>section:not(.hero)::before{
  content:"";position:absolute;inset:0 0 auto;height:1px;pointer-events:none;
  background:linear-gradient(90deg,transparent,rgba(227,193,105,.85) 22%,rgba(255,255,255,.9) 50%,rgba(227,193,105,.85) 78%,transparent);
  opacity:.7;
}
body.page-glass main>section.section--dark::before{opacity:.55}

/* 14.4 Header — the bar tightens and gains contrast once the page moves. */
body.page-glass .site-header{
  transition:background-color .35s ease,box-shadow .35s ease,transform .35s ease,border-color .35s ease;
}
body.is-scrolled.page-glass .site-header{
  background:rgba(10,17,40,.72);
  border-color:rgba(227,193,105,.28);
  box-shadow:0 10px 34px rgba(0,0,0,.40),inset 0 1px 0 rgba(255,255,255,.18);
}
.nav a{position:relative}
.nav a::after{
  content:"";position:absolute;left:var(--s3);right:var(--s3);bottom:6px;height:2px;
  background:linear-gradient(90deg,var(--gold-deep),var(--gold));
  border-radius:2px;transform:scaleX(0);transform-origin:50% 50%;
  transition:transform .28s cubic-bezier(.2,.7,.2,1);
}
.nav a:hover::after,.nav a[aria-current="page"]::after{transform:scaleX(1)}
.nav a:hover{text-decoration:none}

/* Reading progress — a thin gold seam across the very top of the window. */
.scroll-progress{
  position:fixed;top:0;left:0;right:0;height:3px;z-index:70;pointer-events:none;
  transform:scaleX(0);transform-origin:0 50%;
  background:linear-gradient(90deg,var(--gold-deep),var(--gold) 55%,#fff4d2);
  box-shadow:0 0 16px rgba(227,193,105,.55);
}

/* 14.5 Buttons — gold with a lit top edge, and a spring on the way up.   */
.btn{
  transition:transform .28s cubic-bezier(.34,1.4,.64,1),box-shadow .28s ease,
             background-color .2s,color .2s,border-color .2s,opacity .2s;
}
.btn-primary{
  background:linear-gradient(180deg,#f2d891 0%,var(--gold) 46%,var(--gold-deep) 100%);
  border-color:var(--gold-deep);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.62),0 8px 22px rgba(207,169,74,.34);
}
.btn-primary:hover{
  background:linear-gradient(180deg,#f7e3a8 0%,#e8ca78 46%,var(--gold-deep) 100%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.7),0 14px 34px rgba(207,169,74,.46);
}
.btn-outline{backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}
.btn:hover{transform:translateY(-2px)}
.btn:active{transform:translateY(0)}

/* 14.6 Cards lift toward the reader on hover.                            */
.card{
  transition:transform .35s cubic-bezier(.2,.7,.2,1),box-shadow .35s ease,border-color .35s ease;
}
.card:hover{
  transform:translateY(-4px);
  box-shadow:0 4px 10px rgba(10,17,40,.08),0 22px 48px rgba(10,17,40,.20);
  border-color:var(--hair-gold);
}
.card--dark:hover{border-color:var(--hair-gold)}

/* 14.7 Eyebrows get a short gold seam, the way a printed masthead does.  */
.section-head--center .eyebrow::before,.section-head--center .eyebrow::after{
  content:"";display:inline-block;vertical-align:middle;width:28px;height:1px;
  background:linear-gradient(90deg,transparent,var(--gold-deep));
  margin-right:var(--s3);position:relative;top:-2px;
}
.section-head--center .eyebrow::after{
  background:linear-gradient(90deg,var(--gold-deep),transparent);
  margin-right:0;margin-left:var(--s3);
}

/* 14.8 Motion — sections rise, then their contents follow in sequence.   */
html:not(.no-js) body.page-glass main>section:not(.hero) .container>*{
  opacity:0;transform:translateY(16px);
  transition:opacity .75s cubic-bezier(.2,.7,.2,1),transform .75s cubic-bezier(.2,.7,.2,1);
}
html:not(.no-js) body.page-glass main>section.is-in .container>*{opacity:1;transform:none}
html:not(.no-js) body.page-glass main>section.is-in .container>:nth-child(1){transition-delay:.06s}
html:not(.no-js) body.page-glass main>section.is-in .container>:nth-child(2){transition-delay:.14s}
html:not(.no-js) body.page-glass main>section.is-in .container>:nth-child(3){transition-delay:.22s}
html:not(.no-js) body.page-glass main>section.is-in .container>:nth-child(4){transition-delay:.30s}
html:not(.no-js) body.page-glass main>section.is-in .container>:nth-child(n+5){transition-delay:.36s}

/* The clip drifts a little slower than the page, so depth reads as depth. */
body.page-glass .page-video{
  transform:translate3d(0,calc(var(--parallax,0) * 1px),0) scale(1.06);
  will-change:transform;
}

/* Hero copy arrives on load rather than simply being there.              */
html:not(.no-js) .hero-inner>*{animation:craft-rise .9s cubic-bezier(.2,.7,.2,1) both}
html:not(.no-js) .hero-inner>:nth-child(1){animation-delay:.05s}
html:not(.no-js) .hero-inner>:nth-child(2){animation-delay:.14s}
html:not(.no-js) .hero-inner>:nth-child(3){animation-delay:.23s}
html:not(.no-js) .hero-inner>:nth-child(4){animation-delay:.32s}
html:not(.no-js) .hero-inner>:nth-child(n+5){animation-delay:.40s}
@keyframes craft-rise{
  from{opacity:0;transform:translateY(20px)}
  to{opacity:1;transform:none}
}

/* A quiet cue that there is more below the fold.                         */
.scroll-cue{
  position:absolute;left:50%;bottom:clamp(14px,3vh,30px);transform:translateX(-50%);
  display:flex;flex-direction:column;align-items:center;gap:var(--s2);
  color:var(--muted-1);pointer-events:none;
}
.scroll-cue .scroll-cue-line{
  width:1px;height:38px;border-radius:1px;
  background:linear-gradient(180deg,rgba(227,193,105,0),var(--gold));
  animation:craft-cue 2.4s ease-in-out infinite;
}
.scroll-cue .scroll-cue-word{
  font:640 .68rem/1 var(--font-display);font-stretch:86%;
  text-transform:uppercase;letter-spacing:.26em;color:var(--gold);
}
@keyframes craft-cue{
  0%,100%{opacity:.35;transform:scaleY(.65);transform-origin:50% 0}
  50%{opacity:1;transform:scaleY(1);transform-origin:50% 0}
}

/* 14.9 The map — a printed-looking plate of the block, drawn from
   OpenStreetMap data and tinted to the brand. Attribution is required
   by ODbL and sits under the plate on every page that shows it.         */
.map-plate{
  display:block;position:relative;margin-top:var(--s5);
  border-radius:18px;overflow:hidden;
  border:1px solid var(--hair-gold);
  box-shadow:0 2px 8px rgba(10,17,40,.10),0 18px 42px rgba(10,17,40,.22);
  text-decoration:none;color:inherit;background:var(--navy-3);
  transition:transform .35s cubic-bezier(.2,.7,.2,1),box-shadow .35s ease,border-color .35s ease;
}
.map-plate:hover{
  transform:translateY(-3px);border-color:var(--gold);
  box-shadow:0 4px 12px rgba(10,17,40,.12),0 26px 56px rgba(10,17,40,.30);
}
.map-plate img{width:100%;height:auto;display:block;aspect-ratio:3/2;object-fit:cover}
.map-chip{
  position:absolute;right:10px;top:10px;
  display:inline-flex;align-items:center;gap:6px;
  padding:7px 13px;border-radius:var(--r-pill);
  font:640 .78rem/1 var(--font-display);font-stretch:88%;
  letter-spacing:.12em;text-transform:uppercase;
  color:var(--navy-3);background:linear-gradient(180deg,#f2d891,var(--gold) 55%,var(--gold-deep));
  border:1px solid rgba(255,255,255,.45);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.6),0 6px 18px rgba(0,0,0,.35);
}
.map-chip::after{content:"\2197";font-size:.9em;line-height:1}
.map-credit{
  display:inline-block;margin-top:var(--s2);font-size:.75rem;color:var(--text-muted);
  letter-spacing:.01em;text-decoration:underline;text-underline-offset:2px;
}
.map-credit:hover{color:var(--text)}
.section--dark .map-credit,.card--dark .map-credit{color:var(--muted-3)}
@media (max-width:560px){
  .map-plate img{aspect-ratio:4/3}
  .map-chip{right:8px;top:8px;padding:6px 11px;font-size:.72rem}
}

/* 14.10 Focus rings stay visible on glass.                               */
:focus-visible{outline-offset:3px;border-radius:2px}
body.page-glass :focus-visible{box-shadow:0 0 0 6px rgba(227,193,105,.22)}

/* 14.11 Respect the reader's motion setting for every addition above.    */
@media (prefers-reduced-motion:reduce){
  body.page-glass .page-shade::after,.scroll-cue .scroll-cue-line{animation:none}
  html:not(.no-js) .hero-inner>*{animation:none;opacity:1;transform:none}
  html:not(.no-js) body.page-glass main>section:not(.hero) .container>*{opacity:1;transform:none;transition:none}
  body.page-glass .page-video{transform:none}
  .btn:hover,.card:hover,.map-plate:hover{transform:none}
  .scroll-progress{display:none}
}
@media print{
  .scroll-progress,.scroll-cue,body.page-glass::after{display:none!important}
  .map-plate{box-shadow:none!important;border:1px solid #999!important}
}

/* 14.12 Legibility over a bright clip — the welcome video is pale sky, so the
   lightest lines in the hero and the scroll cue need their own weight. */
body.page-glass .hero-invite{
  color:var(--ink-on-dark);font-weight:420;
  text-shadow:0 1px 3px rgba(10,17,40,.55),0 2px 18px rgba(10,17,40,.75);
}
body.page-glass .hero-sub{text-shadow:0 1px 3px rgba(10,17,40,.5),0 2px 20px rgba(10,17,40,.7)}
body.page-glass .motto .motto-en{color:var(--cream-2);text-shadow:0 1px 3px rgba(10,17,40,.6),0 2px 18px rgba(10,17,40,.8)}
body.page-glass .scroll-cue .scroll-cue-word{
  color:var(--cream-2);text-shadow:0 1px 3px rgba(10,17,40,.7),0 2px 14px rgba(10,17,40,.8);
}
body.page-glass .scroll-cue .scroll-cue-line{
  background:linear-gradient(180deg,rgba(243,234,216,0),var(--cream-2));
  box-shadow:0 0 10px rgba(10,17,40,.6);
}

/* 14.13 Hero fit and cue — keep the address on one line where it fits, give
   the invitation enough weight to read over sky, and make the scroll cue a
   small glass pill so it survives the brightest frames of the clip. */
.hero-sub{max-width:46ch;text-wrap:balance}
body.page-glass .hero-invite{font-weight:520;font-size:1.05rem}
.scroll-cue{
  gap:var(--s3);padding:8px 14px 10px;border-radius:var(--r-pill);
  background:rgba(10,17,40,.34);border:1px solid rgba(255,255,255,.22);
  -webkit-backdrop-filter:blur(10px) saturate(1.2);backdrop-filter:blur(10px) saturate(1.2);
  box-shadow:0 8px 24px rgba(0,0,0,.28),inset 0 1px 0 rgba(255,255,255,.22);
}
.scroll-cue .scroll-cue-line{height:26px}
body.page-glass .scroll-cue .scroll-cue-word{color:var(--gold);text-shadow:0 1px 2px rgba(10,17,40,.8)}
body.page-glass .scroll-cue .scroll-cue-line{background:linear-gradient(180deg,rgba(227,193,105,0),var(--gold));box-shadow:none}
body.page-glass .hero-inner::before{background:radial-gradient(ellipse at center,rgba(10,17,40,.62),rgba(10,17,40,0) 72%)}

/* 14.14 Header fit — seven links plus a call-to-action is a lot of bar. The
   condensed face buys room, and these two bands spend it: below 1140 the nav
   tightens, and on a 320-360 px phone the wordmark shrinks so the bar stays
   one row instead of two. Re-measure the bar at 320/360/390/900/1000/1280
   after any change here. */
@media (min-width:900px) and (max-width:1139px){
  body.page-glass .site-header .nav ul{gap:6px}
  body.page-glass .site-header .nav a{font-size:.82rem;padding-inline:7px;letter-spacing:0}
  body.page-glass .site-header .nav a.btn-nav{padding:8px 13px;margin-left:var(--s1)}
  body.page-glass .site-header .header-row{gap:var(--s3)}
  body.page-glass .brand-dove{width:34px;height:34px}
  body.page-glass .brand-name{font-size:.95rem;letter-spacing:.1em}
}
@media (min-width:1140px) and (max-width:1279px){
  body.page-glass .site-header .nav ul{gap:var(--s4)}
  body.page-glass .site-header .nav a{font-size:.92rem;padding-inline:9px}
}
@media (max-width:389px){
  body.page-glass .brand-dove{width:30px;height:30px}
  body.page-glass .brand-name{font-size:.82rem;letter-spacing:.06em}
  body.page-glass .brand{gap:var(--s2)}
  body.page-glass .lang-toggle,body.page-glass .nav-toggle{padding:0 9px;font-size:.85rem;min-width:40px}
  body.page-glass .site-header .container{padding-inline:10px}
}

/* ===== craft-2:type ===== */
/* ==========================================================================
   14.15 Typographic craft - one scale, one label voice, and scripture set
   with the care it is owed. 14.1 chose the faces; this subsection sets them.

   Three facts drive everything below.
   1. The display sizes were six independent clamps that reached their ceilings
      at six different widths, so the steps between them changed shape as the
      viewport moved. They now share one ramp. This IS a visible resize on
      desktop, not a like-for-like swap: see the table in 14.15.1.
   2. Spanish runs about 20 percent longer than English and stacks accents
      above the cap height, so no rule here depends on a line count, and
      every heading is given room above the caps for A-acute, I-acute and N-tilde.
   3. The shipped Fraunces is an optical-size 40 instance carrying a weight
      axis only, and neither variable family carries an italic at all.
      Optical size is therefore done by hand - weight, tracking and leading
      move with the size - and nothing asks for a slant the font cannot draw.

   Nothing here touches the header. --header-h, the wordmark, the toggles and
   the nav all size in fixed rem and are deliberately left off the ramp, so the
   one-row bar at 320/360/390/900/1024/1280 is unchanged by this subsection.
   ========================================================================== */

/* 14.15.1 One ramp, one scale.
   --type-t runs 0 to 1rem as the viewport travels 360px to 1192px, and each
   display size is its small value plus that progress times its own travel.
   Body, button and header sizes stay off the ramp on purpose.

   What actually changes, old -> new, in px:
            320/360    900       1192+
     lead   19.2->19   19.8->20.9  22->22
     h3     20  ->21.8 21.6->25.2  24->27
     h2     28  ->27.9 40  ->34.5  40->38
     h1     36  ->35.7 54  ->47.6  56->54
     hero   44  ->44   81  ->75.2  92->92
   Phone widths are within 2px. Desktop is not: h3 grows 3px, h2 and h1 lose
   2px at the ceiling and up to 6px mid-range because the old clamps topped out
   at 900-1024px and this one keeps climbing to 1192. Eyeball the h2/h3 rows on
   eventos and nosotros at 900 and 1280 before shipping. */
:root{
  --type-t:clamp(0rem,(100vw - 22.5rem) / 52,1rem);
  --fs-lead:calc(1.1875rem + .1875 * var(--type-t));   /* 19 to 22 */
  --fs-h3:calc(1.3625rem + .325 * var(--type-t));      /* 21.8 to 27 */
  --fs-h2:calc(1.7438rem + .6313 * var(--type-t));     /* 27.9 to 38 */
  --fs-h1:calc(2.2313rem + 1.1438 * var(--type-t));    /* 35.7 to 54 */
  --fs-hero:calc(2.75rem + 3 * var(--type-t));         /* 44 to 92 */
  --fs-eyebrow:.875rem;      /* a label is not a heading: it does not scale */
  --track-caps:.2em;
  --measure:38rem;           /* about 70 characters at body size */
}

/* 14.15.2 Leading and tracking, per level rather than one value for all.
   Tracking loosens as type gets smaller; 14.1 gave every heading the same
   -.015em. And 1.05 leading cannot clear a Spanish accent: at h2 size the
   acute on a second-line I-acute lands within half a pixel of the descender
   above it, so a two-line Spanish heading can touch itself where the English
   never does. These numbers put two to three pixels back. .h3 is sized like an
   h3 but carried no leading of its own, so it inherited whatever its parent
   had - body 1.6 in most places - and a card title marked .h3 sat looser than
   its h3 neighbours. .hero-display, .page-hero h1 and the eventos day label
   keep their own tracking: those selectors are more specific and still win. */
h1{line-height:1.08;letter-spacing:-.022em}
h2{line-height:1.12;letter-spacing:-.016em}
h3,.h3{line-height:1.24;letter-spacing:-.006em}

/* 14.15.3 One label voice. The eyebrow, the infocard label, the kv label and
   the verse reference were four sizes and four tracking values doing one job;
   they are now 14px at .2em in the condensed cut. (.service-list .lab on
   nosotros is page-owned and still sits outside this - fold it in there if the
   mismatch shows.) Tracked caps also put a space after the LAST letter, which
   drags a centred label half a space left of centre. The correction is the
   FULL tracking value, not half of it: the box shrink-wraps around the trailing
   space, so centring gives half of any indent back before it lands. The flex
   label uses padding because text-indent does not reach a flex line, and the
   masthead seam shortens its own margin instead. The motto indent is scoped to
   the two centred mottos - the seven footer mottos are flush left against the
   footer column and must not move. */
.eyebrow,h2.eyebrow{font-size:var(--fs-eyebrow);letter-spacing:var(--track-caps);line-height:1.35}
.infocard .lab,.kv .lab{font-size:var(--fs-eyebrow);letter-spacing:var(--track-caps)}
.kv .lab{font-stretch:86%;font-weight:640}
.center .eyebrow,.verse-block--center .verse-ref{text-indent:var(--track-caps)}
.infocard .lab{padding-left:var(--track-caps)}
.section-head--center .eyebrow::after{margin-left:calc(var(--s3) - var(--track-caps))}
.family{text-indent:.14em}
.hero .motto,.motto-hero .motto{text-indent:var(--track-caps)}
.motto .motto-en{text-indent:0;line-height:1.3}
.scroll-cue .scroll-cue-word{text-indent:.26em}

/* 14.15.4 Measure. Body copy in the narrow column ran about 89 characters a
   line, a third longer than is comfortable; one token holds it near 70. Only
   unclassed paragraphs are treated as prose - a .lead is a deck and may span
   the column, and .motto, .closing and friends are display type that must
   never be boxed in. The three centred contexts need the auto margin, or a
   capped paragraph would hug the left edge of a centred block. Paragraph
   spacing becomes proportional so it stays one space at 17px and at 22px; the
   flex rows, where a gap already separates the children, stand down to zero.
   The label/value stacks stand down to the old 16px instead: em spacing keys
   off the SECOND paragraph, and .val is 1.4rem in the infocard and up to 2rem
   on the Zelle card, which would have opened those pairs to 27px and 38px. */
.container--narrow > p:not([class]),.card p:not([class]){max-width:var(--measure)}
.center p:not([class]),.closing p:not([class]),.motto-hero p:not([class]){margin-inline:auto}
p + p{margin-top:1.2em}
.event p + p,.episode p + p,.contact-card p + p{margin-top:0}
.infocard .cell p + p,.kv p + p{margin-top:var(--s4)}

/* 14.15.5 Scripture. The verses carry the feeling of this site, so they get
   the serif's real attention. The reset zeroed p and figure but never
   blockquote, and .verse-block IS a blockquote, so every flush-left verse was
   still wearing the browser's 40px inline margins: on a phone that left the
   Word of God a 201px column, about twenty characters a line. Removing them,
   and easing the rule's padding on small screens, widens that measure by more
   than 40 percent. Above it: a 48-character line, leading opened to 1.46
   because a high-contrast face needs the air, and the weight lifted off 360 -
   at 360 Fraunces goes spindly at 1x, and over the navy glass the thin strokes
   simply leave. Where a verse is flush left the opening quotation mark hangs
   into the margin so the text edge, not the punctuation, lines up with the gold
   rule; the hang is per language because the Spanish guillemet is wider than
   the English double quote, and the html lang attribute i18n.applyLang already
   sets picks the right one - with JS dead, lang="es" in the markup still gives
   the Spanish hang to the Spanish text that is actually on the page. Only
   index.html has a flush-left verse-block today; the child combinator keeps the
   hang off the centred blocks and off nosotros's .verse-other pair. Centred
   verses are balanced instead - a hanging mark on a centred line reads as a
   mistake, not a refinement. .verse--big on ofrendas and .motto-hero .verse on
   nosotros are page-owned and still win on size; that is intended. */
.verse-block:not(.verse-block--center){margin-inline:0}
.verse{
  font-family:var(--font-serif);font-style:normal;font-weight:400;
  font-size:var(--fs-lead);line-height:1.46;letter-spacing:-.004em;
  max-width:48ch;
}
.section--dark .verse,.card--dark .verse,.hero .verse{font-weight:440}
.verse-block--center .verse,.verse-block--center .verse-ref,.center .verse{margin-inline:auto}
.verse-block--center .verse{text-wrap:balance}
.verse-block:not(.verse-block--center) > .verse:lang(es){text-indent:-.44em}
.verse-block:not(.verse-block--center) > .verse:lang(en){text-indent:-.3em}
.verse-ref{
  font-size:.8125rem;font-stretch:86%;font-weight:620;
  letter-spacing:var(--track-caps);margin-top:var(--s4);
}
@media (max-width:599px){
  .verse-block:not(.verse-block--center){padding-left:var(--s4)}
}

/* 14.15.6 No faked slants, and a second voice for captions. Neither variable
   family ships an italic, so every italic on this site was a browser-drawn
   shear - crude on a face with this much stroke contrast, and worst on the
   hero invitation, which is set over moving sky. font-synthesis:none turns the
   shear off at the root (14.1 had already done this for weight); emphasis moves
   to weight, the invitation stands upright, and captions become real Fraunces
   instead of a smaller grey copy of the body font. Pinyon ships one weight and
   is only ever asked for 400, so nothing loses a bold it was relying on. */
body{font-synthesis:none}
em,i{font-style:normal;font-weight:620}
.hero-invite{font-style:normal}
.figure figcaption{
  font-family:var(--font-serif);font-style:normal;font-weight:400;
  font-size:.9375rem;line-height:1.5;letter-spacing:0;
}

/* 14.15.7 The things that are figures, not words. Times, dates, the street
   number and the Zelle number get tabular lining digits, so 4:00 PM sits under
   4:00 PM in a stacked column and the address keeps its width when the reader
   switches language. The contact address needs its element in the selector
   because contacto.html's own font shorthand resets numeral variants, and that
   page's <style> block loads after this file. */
time,address,.val,.event-when,.episode-meta,.contact-when,.footer-bottom,
.verse-ref,address.contact-address{font-variant-numeric:tabular-nums lining-nums}

/* 14.15.8 Spanish puts long words in narrow cards. Hyphenation only where the
   column is genuinely narrow, and only for words long enough that a break helps
   the rag rather than interrupting the reading. -webkit-hyphens is not optional:
   without it Safari and every iOS browser ignore hyphens entirely, which is most
   of this congregation. Only unclassed paragraphs hyphenate - .card p would
   otherwise out-specify the display-type exclusion below and hyphenate the
   eyebrows inside cards. Scripture never hyphenates. */
.card p:not([class]),.empty p:not([class]),.steps li,.expect-list p{
  -webkit-hyphens:auto;hyphens:auto;hyphenate-limit-chars:9 4 4;
}
h1,h2,h3,.h3,.hero-display,.motto,.family,.eyebrow,.verse{
  -webkit-hyphens:none;hyphens:none;
}

/* 14.15.9 Leads over the dark glass. 380 is a lovely weight on cream and too
   little over a moving sky. These two surfaces get some of it back, without
   touching the hero lines 14.12 and 14.13 already tuned. */
.section--dark .lead,.card--dark .lead{font-weight:430}
.page-hero .page-hero-sub{font-weight:430;text-wrap:balance}

/* 14.15.10 Text links. The heavier underline was an ungated hover, so on a
   phone it stuck to whatever had last been tapped. It now needs a real pointer,
   and touch gets the same emphasis on press instead. The query is the De Morgan
   inverse of (hover:hover) and (pointer:fine), so the two can never both apply.
   Thickening a line on a link that has none - a button, the wordmark, a nav
   item - does nothing, which is why no exclusions are needed here. The desktop
   nav's own hover rule lives behind min-width:900px and keeps its underline on
   a large coarse-pointer screen; that is a tablet in landscape and it is fine. */
@media (hover:none),(pointer:coarse){
  a:hover,.map-links a:hover,.episode .links a:hover{text-decoration-thickness:auto}
  .fb-link:hover,.footer-nav a:hover{text-decoration-line:none}
  a:active{text-decoration-thickness:2px}
  .fb-link:active,.footer-nav a:active{text-decoration-line:underline}
}

/* 14.15.11 Motion. This subsection adds no animation, transition or transform;
   everything above is set once at layout time, so section 13's blanket
   *{animation:none!important;transition:none!important} already covers it and
   a local restatement here would be dead code. Nothing is added on scroll, and
   nothing here can be composited or repainted by the parallax or the reveal
   observer. */

/* ===== craft-2:surface ===== */
/* ==========================================================================
   14.15 Material and depth — two materials, one light.
   Cream panels are paper on a lit table: a white highlight along the top edge,
   a cool navy shade underneath. Navy panels are slate, and the only light that
   reaches them in this building is gold, so their top edge catches gold and
   their underside goes darker still. Both share one light direction (upper
   left) and one shadow recipe — a tight contact shadow that says the surface
   is resting on something, plus a wide ambient one. That shared recipe is what
   makes them read as two materials in one system rather than two colours.
   Gold is treated as light, never as an outline: every gold line added here
   fades along its length instead of drawing a box around something.
   Additive only. No new animation, no new backdrop-filter, no new element.
   Header height is untouched: every rule here paints, none of them measures.
   Re-measured at 320/360/390/900/1024/1280 — one 66 px row at each.
   ========================================================================== */

/* 14.15.1 The shared light. These two tokens carry nearly every raised surface
   on the site, so they are the cheapest place to buy depth. One mid-distance
   blur — which is what they were — is the flat look that no amount of styling
   on top of it recovers from. They also reach the three page-level styles that
   borrow them: podcast-art, expect-list and the about photo. */
:root{
  --shadow-1:0 1px 2px rgba(10,17,40,.09),0 8px 22px rgba(10,17,40,.10);
  --shadow-2:0 2px 5px rgba(10,17,40,.18),0 18px 44px rgba(10,17,40,.22);
  --edge-lit:rgba(255,255,255,.55);       /* top edge of a cream surface */
  --edge-lit-gold:rgba(227,193,105,.22);  /* top edge of a navy surface */
  --edge-shade:rgba(10,17,40,.12);        /* underside of either */
}

/* 14.15.2 Grain. The noise layer was lightening the brightest part of the
   frame — the sky the hero stands in — where there is no banding to hide.
   Mask it back at the top of the viewport, coarsen the tile so it reads as
   film rather than television static, and take the opacity down. Same single
   fixed layer as before: a static mask, no extra per-frame work. */
body.page-glass::after{
  opacity:.24;
  background-size:220px 220px;
  -webkit-mask-image:linear-gradient(180deg,rgba(0,0,0,.30) 0%,rgba(0,0,0,.85) 40%,#000 100%);
  mask-image:linear-gradient(180deg,rgba(0,0,0,.30) 0%,rgba(0,0,0,.85) 40%,#000 100%);
}

/* 14.15.3 Glass panels, cream. The fill token stays as the background colour;
   this adds only the light falling across it, brightest at the upper left and
   spent before the foot of the panel. The four inset hairlines are the point:
   the lit side and the shaded side of one edge cannot be the same colour, or
   the panel reads as a sticker laid on the video. */
body.page-glass main>section:not(.hero){
  background-image:linear-gradient(165deg,
    rgba(255,255,255,.30) 0%,
    rgba(255,255,255,.10) 24%,
    rgba(255,255,255,0) 52%,
    rgba(22,38,77,.05) 100%);
  box-shadow:
    0 1px 2px rgba(10,17,40,.10),
    0 10px 24px rgba(10,17,40,.13),
    0 40px 88px rgba(10,17,40,.26),
    inset 0 1px 0 var(--edge-lit),
    inset 1px 0 0 rgba(255,255,255,.22),
    inset -1px 0 0 rgba(10,17,40,.05),
    inset 0 -1px 0 var(--edge-shade);
}
/* The rule along the top of a panel is where the light enters, so it is
   brightest just off the left corner and gone before the right one. A
   symmetrical stripe with a white core reads as decoration; an asymmetric one
   reads as a masthead rule. */
body.page-glass main>section:not(.hero)::before{
  opacity:1;
  background:linear-gradient(90deg,
    rgba(227,193,105,0) 0%,
    rgba(243,234,216,.75) 16%,
    rgba(227,193,105,.60) 46%,
    rgba(227,193,105,.22) 74%,
    rgba(227,193,105,0) 94%);
}

/* 14.15.4 Glass panels, navy — and the inner-page title band, which is the
   same material. The light arrives warm at the top edge and the surface falls
   away to a deeper navy at the foot. The gold border drops from .45 to .28:
   an even gold box around a panel is jewellery, and the top rule is the gold
   that is actually doing the work.
   The top-left highlight is held at .06 and spent by 40 percent of its ray.
   These panels carry cream text over a pale-sky clip, which is the whole
   reason section 14.12 exists; a highlight strong enough to see is also
   strong enough to take the heading under 4.5:1 on the brightest frames. */
body.page-glass main>section.section--dark,
body.page-glass main>section.page-hero{
  border-color:rgba(227,193,105,.28);
  background-image:
    radial-gradient(120% 140% at 18% 0%,rgba(255,255,255,.06),rgba(255,255,255,0) 40%),
    linear-gradient(180deg,rgba(227,193,105,.05) 0%,rgba(227,193,105,0) 24%,rgba(10,17,40,.18) 100%);
  box-shadow:
    0 1px 2px rgba(0,0,0,.18),
    0 10px 26px rgba(10,17,40,.22),
    0 40px 88px rgba(10,17,40,.34),
    inset 0 1px 0 var(--edge-lit-gold),
    inset 1px 0 0 rgba(255,255,255,.07),
    inset -1px 0 0 rgba(0,0,0,.10),
    inset 0 -1px 0 rgba(0,0,0,.20);
}
body.page-glass main>section.section--dark::before,
body.page-glass main>section.page-hero::before{opacity:.8}
/* The title band is centred copy, so its light is centred too — and held to
   the same budget, because the h1 and its subtitle sit inside the glow. */
body.page-glass main>section.page-hero{
  background-image:
    radial-gradient(110% 130% at 50% 0%,rgba(227,193,105,.07),rgba(227,193,105,0) 40%),
    linear-gradient(180deg,rgba(255,255,255,.05) 0%,rgba(255,255,255,0) 30%,rgba(10,17,40,.20) 100%);
}

/* 14.15.5 Header. The bar gets the same two-sided edge as a panel, plus one
   gold seam along its underside that fades out well before the rounded
   corners. Faint at rest, full once the page has moved, so it reads as the bar
   settling onto the content rather than as trim that was always there.
   The seam is absolutely positioned inside a sticky bar, so it adds no height:
   the row stays 64 px of content inside a 1 px border, 66 px in all. */
body.page-glass .site-header{
  box-shadow:
    0 1px 2px rgba(0,0,0,.16),
    0 12px 40px rgba(0,0,0,.28),
    inset 0 1px 0 rgba(255,255,255,.16),
    inset 0 -1px 0 rgba(0,0,0,.22);
}
body.is-scrolled.page-glass .site-header{
  box-shadow:
    0 1px 2px rgba(0,0,0,.22),
    0 10px 34px rgba(0,0,0,.42),
    inset 0 1px 0 rgba(255,255,255,.20),
    inset 0 -1px 0 rgba(0,0,0,.28);
}
body.page-glass .site-header::after{
  content:"";position:absolute;left:12%;right:12%;bottom:0;height:1px;
  z-index:2;pointer-events:none;opacity:.45;
  background:linear-gradient(90deg,
    rgba(227,193,105,0),
    var(--hair-gold) 26%,
    rgba(243,234,216,.55) 50%,
    var(--hair-gold) 74%,
    rgba(227,193,105,0));
  transition:opacity .35s ease;
}
body.is-scrolled.page-glass .site-header::after{opacity:1}

/* Menu separators fade before they reach the rounded edge of the phone panel,
   where a hard edge-to-edge line currently runs into the corner radius. The
   1px border keeps its place in the box and only loses its colour, so nothing
   in the header moves by a pixel.
   The give button is excluded on purpose. It is the last li, so it matches
   li + li, but its own rule sets border-top:0 — it has never had a separator
   and must not gain one, and a painted line does not care that the border
   width is zero. */
.nav li + li:not(.nav-cta){
  border-top-color:transparent;
  background-image:linear-gradient(90deg,
    rgba(227,193,105,0),var(--hair-gold-soft) 20%,var(--hair-gold-soft) 80%,rgba(227,193,105,0));
  background-size:100% 1px;background-position:left top;
  background-repeat:no-repeat;background-origin:border-box;
}
@media (min-width:900px){
  .nav li + li:not(.nav-cta){background-image:none}
}

/* 14.15.6 Footer. It carried no shadow at all, so it sat flat against the clip
   while every panel above it floated. Same recipe as a navy panel, with the
   light entering along the top edge, where the eye leaves the last panel. */
body.page-glass .site-footer{
  border-top-color:var(--hair-gold-soft);
  background-image:
    radial-gradient(120% 100% at 50% 0%,rgba(227,193,105,.06),rgba(227,193,105,0) 42%),
    linear-gradient(180deg,rgba(255,255,255,.05) 0%,rgba(255,255,255,0) 26%,rgba(10,17,40,.20) 100%);
  box-shadow:
    0 1px 2px rgba(0,0,0,.18),
    0 -4px 30px rgba(10,17,40,.16),
    0 26px 64px rgba(10,17,40,.30),
    inset 0 1px 0 var(--edge-lit-gold),
    inset 0 -1px 0 rgba(0,0,0,.20);
}
/* The closing rule sits under centred copy, so it fades from both ends. */
.footer-bottom{
  border-top-color:transparent;
  background-image:linear-gradient(90deg,
    rgba(227,193,105,0),var(--hair-gold-soft) 16%,var(--hair-gold) 50%,var(--hair-gold-soft) 84%,rgba(227,193,105,0));
  background-size:100% 1px;background-position:left top;
  background-repeat:no-repeat;background-origin:border-box;
}

/* 14.15.7 Cards. Paper: white where the light lands, settling to cream at the
   foot, a cream-lit top border and a cool shaded bottom one. --mat-rest holds
   the resting shadow so the touch restore further down can never drift from
   it. Dark cards must follow, because every one of them matches .card too. */
.card{
  --mat-rest:var(--shadow-1),inset 0 -1px 0 rgba(22,38,77,.05);
  box-shadow:var(--mat-rest);
  background-image:linear-gradient(172deg,var(--white) 0%,var(--cream) 74%,var(--cream-2) 100%);
  border-top-color:var(--cream-2);
  border-bottom-color:rgba(22,38,77,.13);
}
.card--dark{
  --mat-rest:var(--shadow-2),inset 0 1px 0 var(--edge-lit-gold),inset 0 -1px 0 rgba(0,0,0,.26);
  box-shadow:var(--mat-rest);
  background-image:
    radial-gradient(120% 130% at 20% 0%,rgba(255,255,255,.08),rgba(255,255,255,0) 52%),
    linear-gradient(176deg,rgba(34,58,110,.45) 0%,rgba(10,17,40,.30) 100%);
  border-top-color:var(--hair-gold);
  border-bottom-color:rgba(0,0,0,.28);
}

/* 14.15.8 The times plate on the home page. It was an even gold rectangle with
   no shadow — a drawn box, not a plate lying on the panel. Gold now catches
   only the top edge and the frame itself steps back. */
.infocard{
  border-color:var(--hair-gold-soft);
  background-image:
    radial-gradient(130% 120% at 50% 0%,rgba(227,193,105,.07),rgba(227,193,105,0) 55%),
    linear-gradient(180deg,rgba(255,255,255,.05) 0%,rgba(255,255,255,0) 30%,rgba(0,0,0,.14) 100%);
  box-shadow:
    0 1px 2px rgba(0,0,0,.20),
    0 14px 34px rgba(10,17,40,.28),
    inset 0 1px 0 rgba(227,193,105,.30),
    inset 0 -1px 0 rgba(0,0,0,.22);
}
/* Cell dividers fade out before the edge of the plate instead of butting into
   it. Again the border keeps its 1px and only loses its colour, so no cell
   changes width and the row does not reflow. */
.infocard .cell + .cell{
  border-top-color:transparent;
  background-image:linear-gradient(90deg,
    rgba(227,193,105,0),var(--hair-gold-soft) 22%,var(--hair-gold-soft) 78%,rgba(227,193,105,0));
  background-size:100% 1px;background-position:left top;
  background-repeat:no-repeat;background-origin:border-box;
}
@media (min-width:700px){
  .infocard .cell + .cell{
    border-left-color:transparent;
    background-image:linear-gradient(180deg,
      rgba(227,193,105,0),var(--hair-gold-soft) 20%,var(--hair-gold-soft) 80%,rgba(227,193,105,0));
    background-size:1px 100%;
  }
}

/* 14.15.9 Photographs get an edge of their own on a pale panel. It is drawn as
   a ring inside the figure rather than as an outline, because outline follows
   border-radius only from Safari 16.4 — on an older iPhone an outline would
   put four square ticks around a 22px-rounded photo. The ring is clipped by
   the figure's own overflow:hidden and inherits its radius everywhere.
   position:relative is layout-neutral here; no figure has a positioned child. */
.figure{position:relative}
.figure::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  border:1px solid rgba(10,17,40,.07);border-radius:inherit;
}
.section--dark .figure::after,.card--dark .figure::after{border-color:rgba(227,193,105,.20)}

/* Scripture bar: gold as a light source. Full strength where the quote begins,
   spent by the time it ends. The 3px border stays in the box and the gradient
   is painted into it, so the quote does not move. */
.verse-block{
  border-left-color:transparent;
  background-image:linear-gradient(180deg,var(--gold) 0%,var(--gold-deep) 42%,rgba(227,193,105,.10) 100%);
  background-size:3px 100%;background-position:left top;
  background-repeat:no-repeat;background-origin:border-box;
}
.verse-block--center{background-image:none}

/* 14.15.10 Hover belongs to pointers that can hover. On a phone each of these
   states used to stick after a tap. Nothing here moves anything — the lift in
   14.6 and 14.9 still owns the transform and 14.11 still switches it off — so
   these are material changes only: the contact shadow tightens and the ambient
   one deepens, the way a surface behaves as it comes up toward the light. */
@media (hover:hover) and (pointer:fine){
  .card:hover{
    box-shadow:0 2px 4px rgba(10,17,40,.12),0 20px 46px rgba(10,17,40,.20),inset 0 -1px 0 rgba(22,38,77,.05);
    border-color:var(--hair-gold-soft);
    border-top-color:var(--hair-gold);
    border-bottom-color:rgba(22,38,77,.13);
  }
  .card--dark:hover{
    box-shadow:0 2px 6px rgba(0,0,0,.24),0 24px 54px rgba(10,17,40,.34),inset 0 1px 0 rgba(227,193,105,.34),inset 0 -1px 0 rgba(0,0,0,.26);
    border-color:var(--hair-gold-soft);
    border-top-color:var(--hair-gold);
    border-bottom-color:rgba(0,0,0,.28);
  }
  .map-plate:hover{box-shadow:0 2px 6px rgba(10,17,40,.14),0 26px 58px rgba(10,17,40,.30)}
}
/* Touch: no hover state survives the tap, and the map plate answers the finger
   instead. hover:none tests the primary pointer, so a touchscreen laptop with
   a trackpad keeps its hovers. */
@media (hover:none){
  .card:hover,.card--dark:hover,.map-plate:hover{transform:none}
  .card:hover{
    box-shadow:var(--mat-rest);
    border-color:var(--paper-line);
    border-top-color:var(--cream-2);
    border-bottom-color:rgba(22,38,77,.13);
  }
  .card--dark:hover{
    box-shadow:var(--mat-rest);
    border-color:var(--hair-gold-soft);
    border-top-color:var(--hair-gold);
    border-bottom-color:rgba(0,0,0,.28);
  }
  .map-plate:hover{border-color:var(--hair-gold);box-shadow:0 2px 8px rgba(10,17,40,.10),0 18px 42px rgba(10,17,40,.22)}
}
/* Pressed. Focus only takes the border: on a glass page the focus ring itself
   is a box-shadow from 14.10, at a higher specificity than this rule, so a
   shadow written here would never reach the screen and must not pretend to. */
.map-plate:active{
  border-color:var(--gold);
  box-shadow:0 1px 3px rgba(10,17,40,.24),0 10px 26px rgba(10,17,40,.22);
}
.map-plate:focus-visible{border-color:var(--gold)}

/* 14.15.11 Motion. Nothing in this subsection animates; the one transition it
   adds is switched off here as well as by section 13. The selector matches the
   one that sets the transition, or it would lose to it on specificity. The
   transforms named below belong to 14.6 and 14.9 — restated so the hover rules
   above can never resurrect them for a reader who asked for stillness. */
@media (prefers-reduced-motion:reduce){
  body.page-glass .site-header::after{transition:none}
  .card:hover,.card--dark:hover,.map-plate:hover,.map-plate:active{transform:none}
}

/* 14.15.12 Print: paper does not need painted light. Every hairline that was
   moved from a border into a background is given a printable border back. */
@media print{
  body.page-glass .site-header::after{display:none!important}
  body.page-glass main>section,.card,.card--dark,.infocard,.infocard .cell + .cell,
  .site-footer,.footer-bottom,.verse-block,.nav li + li:not(.nav-cta){
    background-image:none!important;box-shadow:none!important;
  }
  .figure::after{display:none!important}
  .verse-block{border-left-color:var(--gold)!important}
  .footer-bottom,.infocard .cell + .cell,.nav li + li:not(.nav-cta){border-color:#999!important}
}

/* ===== craft-2:scroll ===== */
/* ==========================================================================
   15. Scroll choreography - arrival, descent, departure.

   14.8 already rises each panel and then staggers its container children.
   That reads as a slideshow: every kind of thing moves the same distance at
   the same speed. This block gives each kind its own arrival - the label
   slides in from the margin, the heading lands from further away on a faster
   curve, the line under it settles a beat later, pictures carry weight and
   take their time, and cards in a grid come one after another. It also gives
   the page a descent (the light falls as you go down, so the video in the
   gaps between panels reads as one falling evening rather than a repeated
   cut), a settle on the way out, and a reading seam that leads rather than
   sits. Nothing here is the only thing keeping content on screen: see 15.9.
   ========================================================================== */

/* 15.1 Three curves, so the page has a vocabulary instead of one easing.
   --cx-land arrives fast and stops without bouncing (headings, things that
   should feel placed). --cx-settle is gentler and lands late (text that
   follows a heading). --cx-glide is the curve 14.8 already uses for panels,
   so a panel and the things inside it agree with each other.              */
:root{
  --cx-land:cubic-bezier(.16,1,.3,1);
  --cx-settle:cubic-bezier(.22,.61,.36,1);
  --cx-glide:cubic-bezier(.2,.7,.2,1);
}

@keyframes cx-failsafe{to{opacity:1}}
@keyframes cx-label-in{from{opacity:0;transform:translate3d(-10px,0,0)}}
@keyframes cx-head-in{from{opacity:0;transform:translate3d(0,26px,0)}}
@keyframes cx-text-in{from{opacity:0;transform:translate3d(0,10px,0)}}
@keyframes cx-verse-in{from{opacity:0;transform:translate3d(0,14px,0)}}
@keyframes cx-media-in{from{opacity:0;transform:translate3d(0,22px,0) scale(.972)}}
@keyframes cx-media-zoom{from{transform:scale(1.055)}}
@keyframes cx-card-in{from{opacity:0;transform:translate3d(0,20px,0) scale(.985)}}

/* 15.2 The hidden state, and five ways out of it.
   Everything below is gated on JS being alive (site.js drops .no-js on its
   first line), on a glass page, and on the panel not yet carrying .is-in -
   which the observer adds, and which site.js adds to every panel after 4 s
   no matter what. The 1 ms animation on a 6 s delay is one more way back
   that needs no class at all: if the observer never fires AND that safety
   net never runs, the element still fades up on its own. It animates
   opacity only, so it can never pin a hover transform in place.

   Two groups, because they need different care. Group A is grandchildren
   that 14.8 never touches, so it must not declare transform - declaring it
   here would outrank .card:hover and kill the card lift for good. Group B is
   direct children of .container that 14.8 does transition; those need the
   blanket transform and transition switched off so one element is not driven
   by a transition and an animation at the same time.                      */
html:not(.no-js) body.page-glass main>section:not(.hero) .section-head>*,
html:not(.no-js) body.page-glass main>section:not(.hero) :is(.grid-2,.grid-3,.events-list,.infocard)>*{
  opacity:0;
  animation:cx-failsafe 1ms linear 6s forwards;
}
html:not(.no-js) body.page-glass main>section:not(.hero) .container>.eyebrow,
html:not(.no-js) body.page-glass main>section:not(.hero) .container>:is(h1,h2,h3),
html:not(.no-js) body.page-glass main>section:not(.hero) .container>:is(.figure,.video-frame,.verse-block){
  opacity:0;transform:none;transition:none;
  animation:cx-failsafe 1ms linear 6s forwards;
}

/* A wrapper whose children now arrive individually must stop arriving as a
   block, or the motion doubles up and goes mushy. These are pinned visible,
   which is strictly safer than what 14.8 does with them today. .events-list
   is listed for the day someone authors one straight into a .container; the
   events module renders it inside [data-events-upcoming] today.           */
html:not(.no-js) body.page-glass main>section:not(.hero) .container>:is(.section-head,.grid-2,.grid-3,.events-list,.infocard,.container,[data-events-upcoming],[data-events-weekly]){
  opacity:1;transform:none;transition:none;animation:none;
}

/* 15.3 Arrival. Fill mode is `backwards` everywhere on purpose: the keyframe
   holds the offset during the delay, then the element lands on its own real
   style and the animation stops owning it. Nothing is left filled forwards,
   so :hover and :active transforms keep working the moment motion ends.   */

/* Anything in a section head is accounted for, even a rule or a small line. */
html:not(.no-js) body.page-glass main>section.is-in .section-head>*{opacity:1;animation:none}

/* Heading: further, faster, lands. This is the beat the eye should catch.  */
html:not(.no-js) body.page-glass main>section.is-in .section-head>:is(h1,h2,h3),
html:not(.no-js) body.page-glass main>section.is-in .container>:is(h1,h2,h3){
  opacity:1;animation:cx-head-in .9s var(--cx-land) .12s backwards;
}
/* Supporting line: shorter, softer, late - it follows the heading in.      */
html:not(.no-js) body.page-glass main>section.is-in .section-head>p:not(.eyebrow){
  opacity:1;animation:cx-text-in .8s var(--cx-settle) .22s backwards;
}
/* Label: slides in from the margin the way a printed masthead rule does,
   and arrives first so the heading has something to land against. The gold
   seams in 14.7 are its own pseudo-elements, so they ride along with it.   */
html:not(.no-js) body.page-glass main>section.is-in .section-head>.eyebrow,
html:not(.no-js) body.page-glass main>section.is-in .container>.eyebrow{
  opacity:1;animation:cx-label-in .62s var(--cx-settle) .04s backwards;
}
/* Scripture is unhurried: least distance, longest duration.                */
html:not(.no-js) body.page-glass main>section.is-in .container>.verse-block{
  opacity:1;animation:cx-verse-in 1.2s var(--cx-settle) .18s backwards;
}
/* Cards, event rows and the three cells of the info card arrive in order
   instead of as one slab. This is the single biggest change on the page.   */
html:not(.no-js) body.page-glass main>section.is-in :is(.grid-2,.grid-3,.events-list,.infocard)>*{
  opacity:1;animation:cx-card-in .85s var(--cx-glide) backwards;
}
html:not(.no-js) body.page-glass main>section.is-in :is(.grid-2,.grid-3,.events-list,.infocard)>:nth-child(1){animation-delay:.05s}
html:not(.no-js) body.page-glass main>section.is-in :is(.grid-2,.grid-3,.events-list,.infocard)>:nth-child(2){animation-delay:.14s}
html:not(.no-js) body.page-glass main>section.is-in :is(.grid-2,.grid-3,.events-list,.infocard)>:nth-child(3){animation-delay:.23s}
html:not(.no-js) body.page-glass main>section.is-in :is(.grid-2,.grid-3,.events-list,.infocard)>:nth-child(n+4){animation-delay:.3s}

/* Media has mass: more distance, a touch of scale, and the longest ride.
   The picture inside the frame settles out of a small push-in while the
   frame itself fades up - the frame already clips, so nothing escapes it.
   This must stay BELOW the :nth-child delays above: a .figure inside a grid
   matches both at equal specificity, and the shorthand here has to win so
   the figure keeps its own .16s delay rather than a stagger step.         */
html:not(.no-js) body.page-glass main>section.is-in .container>:is(.figure,.video-frame),
html:not(.no-js) body.page-glass main>section.is-in :is(.grid-2,.grid-3)>.figure{
  opacity:1;animation:cx-media-in 1.05s var(--cx-glide) .16s backwards;
}
html:not(.no-js) body.page-glass main>section.is-in .figure>img{
  animation:cx-media-zoom 1.5s var(--cx-glide) .16s backwards;
}

/* 15.4 The handoff. The first panel under the hero is the one the scroll cue
   points at, so it comes from further away and takes longer than the rest -
   the page opens rather than starts. Home only: the inner pages open with
   section.page-hero, which has no .hero before it.                        */
html:not(.no-js) body.page-glass main>section.hero+section:not(.hero){
  transform:translateY(34px) scale(.978);
  transition:opacity 1.05s var(--cx-glide),transform 1.05s var(--cx-glide);
}
html:not(.no-js) body.page-glass main>section.hero+section.is-in{transform:none}

/* 15.5 Descent. The fixed shade gains a second veil that deepens as the page
   runs on, so the clip showing between panels gets darker as you go down
   instead of repeating the same brightness at every gap. One fixed layer,
   opacity only. Base state is fully transparent, so where scroll-driven
   animation is unavailable this costs nothing and changes nothing.

   The end stop is deliberately shallow. .page-shade is already at roughly
   .72 effective alpha by the bottom of the page, and the light panels carry
   dark navy body copy - every point added here comes straight out of the
   text contrast that 14.12 and 14.13 were written to protect. Re-check the
   foot of index.html and ofrendas.html if this is ever deepened.          */
body.page-glass .page-shade::before{
  content:"";position:absolute;inset:0;pointer-events:none;opacity:0;
  background:linear-gradient(180deg,rgba(10,17,40,0) 0%,rgba(10,17,40,.05) 42%,rgba(10,17,40,.16) 100%);
}

/* 15.6 The seam leads. The JS bar stays exactly as it is - this adds a soft
   point of light riding its leading edge, on its own fixed layer so it never
   fights the inline transform the scroll module writes. It is invisible at
   rest and invisible without a scroll timeline.                            */
.scroll-progress{border-radius:0 var(--r-pill) var(--r-pill) 0}
body.page-glass::before{
  content:"";position:fixed;top:0;left:0;right:0;height:3px;z-index:71;
  pointer-events:none;opacity:0;transform:translate3d(-100%,0,0);
  background:radial-gradient(12px 8px at 100% 50%,rgba(255,248,224,.95),rgba(227,193,105,.5) 40%,rgba(227,193,105,0) 100%);
}

/* 15.7 Scroll-driven layer. Everything in here is decoration on top of a
   state that already looks right without it, so a browser with no scroll
   timelines simply gets the JS path from site.js untouched. Note the order:
   the `animation` shorthand resets animation-timeline, so the timeline and
   range must be declared after it.                                         */
@supports (animation-timeline: scroll()){
  @media (prefers-reduced-motion:no-preference){
    /* The hero does not cut away, it releases: its copy lifts and dissolves
       over the first screen of scroll, handing off to the first panel.     */
    body.page-glass .hero-inner{
      animation:cx-hero-release auto linear both;
      animation-timeline:scroll(root block);
      animation-range:6vh 62vh;
    }
    /* .hero-inner holds two real links. Fading their container to .1 would
       leave them tabbable and invisible, so any focus inside cancels the
       release outright and the hero comes back at full strength. Dropping
       focus hands it back to the timeline at the current scroll offset.   */
    body.page-glass .hero-inner:focus-within{
      animation:none;opacity:1;transform:none;
    }
    /* The cue has done its job the moment you move.                        */
    body.page-glass .scroll-cue{
      animation:cx-cue-out auto linear both;
      animation-timeline:scroll(root block);
      animation-range:0 20vh;
    }
    body.page-glass .page-shade::before{
      animation:cx-dusk auto linear both;
      animation-timeline:scroll(root block);
      animation-range:10vh 100%;
    }
    body.page-glass::before{
      opacity:1;
      animation:cx-comet auto linear both;
      animation-timeline:scroll(root block);
    }
  }
}
@keyframes cx-hero-release{
  from{opacity:1;transform:translate3d(0,0,0)}
  to{opacity:.1;transform:translate3d(0,-26px,0)}
}
@keyframes cx-cue-out{
  from{opacity:1;transform:translate3d(-50%,0,0)}
  to{opacity:0;transform:translate3d(-50%,14px,0)}
}
@keyframes cx-dusk{from{opacity:0}to{opacity:1}}
@keyframes cx-comet{from{transform:translate3d(-100%,0,0)}to{transform:translate3d(0,0,0)}}

/* 15.8 Departure. A panel leaving through the top of the window dims from its
   top edge down, so the gap above it reads as a shadow trough instead of a
   hard cut against the clip. The gradient is top-weighted on purpose: on a
   panel taller than the window the part you are still reading is untouched.
   Held to wide viewports - this is one extra composited layer per panel, and
   a phone is already carrying a fixed video, grain and glass.

   The timeline MUST be named on the section rather than left as a bare
   view() on the pseudo-element. The panels carry overflow:hidden for their
   rounded corners, and overflow:hidden makes each section its own scroll
   container - so view() resolved from inside one measures that section's
   scrollport, which never scrolls, and the animation sits at 0 forever.
   Naming it on the section resolves it against the root instead.          */
@supports (animation-timeline: view()){
  @media (prefers-reduced-motion:no-preference) and (min-width:900px){
    body.page-glass main>section:not(.hero){
      view-timeline-name:--cx-panel;
    }
    body.page-glass main>section:not(.hero)::after{
      content:"";position:absolute;inset:0;z-index:1;pointer-events:none;opacity:0;
      background:linear-gradient(180deg,rgba(10,17,40,.50) 0%,rgba(10,17,40,.18) 34%,rgba(10,17,40,0) 62%);
      animation:cx-depart auto linear both;
      animation-timeline:--cx-panel;
      animation-range:exit 18% exit 92%;
    }
  }
}
@keyframes cx-depart{from{opacity:0}to{opacity:1}}

/* 15.9 Reduced motion, and the promise that nothing stays hidden.
   Section 13 kills every animation and transition with !important, which
   would otherwise strand an element at opacity 0 forever - so these have to
   be !important too, and they have to be unconditional: no .is-in, no
   :not(.hero), no dependence on JS having run at all.                     */
@media (prefers-reduced-motion:reduce){
  html body.page-glass main>section .section-head>*,
  html body.page-glass main>section :is(.grid-2,.grid-3,.events-list,.infocard)>*,
  html body.page-glass main>section .container>.eyebrow,
  html body.page-glass main>section .container>:is(h1,h2,h3),
  html body.page-glass main>section .container>:is(.figure,.video-frame,.verse-block){
    opacity:1!important;transform:none!important;
  }
  html body.page-glass main>section .figure>img{transform:none!important}
  html body.page-glass main>section.hero+section{opacity:1!important;transform:none!important}
  body.page-glass .hero-inner,body.page-glass .scroll-cue{opacity:1}
  body.page-glass::before,
  body.page-glass .page-shade::before,
  body.page-glass main>section:not(.hero)::after{display:none}
}

/* A panel that never scrolled into view used to print blank, because the
   reveal state is a paint state and printing does not scroll. Force the
   whole choreography off on paper.                                         */
@media print{
  html body.page-glass main>section,
  html body.page-glass main>section .section-head>*,
  html body.page-glass main>section :is(.grid-2,.grid-3,.events-list,.infocard)>*,
  html body.page-glass main>section .container>*,
  html body.page-glass main>section .figure>img{
    opacity:1!important;transform:none!important;animation:none!important;
  }
  body.page-glass::before,
  body.page-glass .page-shade::before,
  body.page-glass main>section::after{display:none!important}
}

/* 15.10 Touch. Every transform on this page is now part of a sequence, and a
   hover transform that sticks after a tap breaks the sequence - the card you
   tapped stays lifted for the rest of the session. On a device with no hover
   the lift is cancelled and replaced by a real press. Styling other than the
   transform is left alone, so this composes with any hover-capability guard
   added elsewhere rather than competing with it.                           */
@media (hover:none),(pointer:coarse){
  .btn:hover,body.page-glass .btn:hover,.card:hover,.map-plate:hover{transform:none}
  .btn:active{transform:translate3d(0,1px,0)}
  .card:active,.map-plate:active{transform:translate3d(0,-2px,0)}
}

/* ===== craft-2:content ===== */
/* ==========================================================================
   15. Content components - the things people actually came for.
   Events and the next-service block, podcast episodes and the show header,
   newsletter issues, figures, the infocard, the map plate, the Zelle tile,
   and the empty states that stand in for all of them.

   Purely additive: every rule re-skins an element that already exists or
   draws a decorative pseudo-element. No markup changes, no new strings.
   Pseudo-elements are safe on bilingual nodes - textContent replaces child
   nodes, and a pseudo is not one; it repaints after every language switch.

   Three of these lists are empty in content today, so the empty state is the
   state most visitors will meet. It is designed first here, not last.
   ========================================================================== */

/* 15.1 One chip for every row's label. A service date, an episode number and
   an issue date are the same kind of thing, so they become the same object:
   a gold-tinted tag that sits at the top-left of the row instead of reading
   as stray small text. The weekly schedule on eventos.html turns its eyebrow
   into a display-size day name, so rows in that list (events-list--rows) are
   deliberately left out. font-variant-numeric comes after the font shorthand
   because the shorthand resets it.                                         */
.events-list:not(.events-list--rows) .event>.eyebrow,
.episode>.eyebrow,
.newsletter-item>time{
  display:inline-block;align-self:flex-start;max-width:100%;
  padding:6px 13px;border-radius:var(--r-pill);
  background:var(--gold-tint);border:1px solid var(--hair-gold-soft);
  color:var(--text);
  font:640 .78rem/1.35 var(--font-display);font-stretch:88%;
  letter-spacing:.14em;text-transform:uppercase;
  font-variant-numeric:tabular-nums;
}
.event-next-card>.eyebrow{
  display:inline-block;align-self:flex-start;
  padding:6px 15px;border-radius:var(--r-pill);
  background:var(--ink-wash);border:1px solid var(--hair-gold);
  color:var(--gold);letter-spacing:.2em;
}
.event-when,.newsletter-item>time,.infocard .val,.events-next>p{
  font-variant-numeric:tabular-nums;
}

/* 15.2 Every row ends in an action strip. The buttons stop floating inside
   the card and become its footer: a hairline, a warm ground, and a bleed to
   the inner edge of the border. The negative margin equals the card padding
   exactly, so nothing sticks out and no width changes - which is why the
   weekly rows on eventos.html are excluded. That page repads .schedule .event
   to var(--s6) with a 6px gold left border, and this bleed would leave a
   visible 6-8px gutter on their strips. They keep their existing footer.   */
.events-list:not(.events-list--rows) .event>.btn-row,
.event-next-card>.btn-row,
.episode>.links{
  margin:auto calc(-1 * var(--s5)) calc(-1 * var(--s5));
  padding:var(--s4) var(--s5);
  border-top:1px solid var(--hair-gold-soft);
  background:var(--gold-tint);
  border-radius:0 0 calc(var(--r-md) - 1px) calc(var(--r-md) - 1px);
}
.event-next-card>.btn-row{background:var(--ink-wash)}

/* A row answers as one object when anything inside it takes focus - keyboard
   users see the whole card acknowledge, not just the link. Border only: a
   focus state can linger after a tap, and a lingering lift looks broken.    */
.event.card:focus-within,
.episode.card:focus-within,
.newsletter-item.card:focus-within{border-color:var(--hair-gold)}

/* 15.3 Newsletter rows. The title carries a drawn arrow rather than a glyph,
   so screen readers announce the link text and nothing else. The underline
   stays at full-strength gold-deep: it is the only affordance marking these
   headings as links, and the hover brightening below is fine-pointer-only,
   so a translucent rule would read as "not a link" for good on a phone.    */
.newsletter-item h3 a{text-decoration-color:var(--gold-deep)}
.newsletter-item h3 a::after{
  content:"";display:inline-block;position:relative;top:1px;
  width:7px;height:9px;margin-left:9px;
  background:var(--gold-deep);
  clip-path:polygon(0 0,100% 50%,0 100%);
  transition:transform .28s cubic-bezier(.2,.7,.2,1);
}

/* 15.4 Empty states. Dashed borders read as "something failed to load", so
   the plate becomes solid: a warm wash, a lit top seam like the panels in
   14.3, a small gold lozenge as a seal, the message set in the serif that
   carries scripture elsewhere, and the links as the same footer strip the
   real rows get. Nothing here says a feature is missing.
   site.js builds these with no h3 - p, then p.map-links.links - so both the
   first-of-type and the last-child hooks land on the real boxes.           */
.empty{
  position:relative;overflow:hidden;
  padding:var(--s7) var(--s5) var(--s6);
  border:1px solid var(--hair-gold-soft);border-radius:var(--r-lg);
  background:radial-gradient(120% 80% at 50% 0%,var(--gold-tint) 0%,var(--cream-2) 58%);
  box-shadow:var(--shadow-1);
}
.section--alt .empty{
  background:radial-gradient(120% 80% at 50% 0%,var(--gold-tint) 0%,var(--cream) 58%);
}
.empty::before{
  content:"";position:absolute;inset:0 14px auto;height:1px;pointer-events:none;
  background:linear-gradient(90deg,transparent,var(--gold),transparent);
}
.empty::after{
  content:"";position:absolute;top:var(--s5);left:calc(50% - 7px);
  width:14px;height:14px;pointer-events:none;
  background:linear-gradient(180deg,var(--gold),var(--gold-deep));
  clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);
  filter:drop-shadow(0 2px 5px rgba(207,169,74,.5));
}
.empty h3{font-stretch:88%;letter-spacing:-.01em}
.empty>p:first-of-type{
  font-family:var(--font-serif);font-weight:360;
  font-size:var(--fs-lead);line-height:1.5;
  max-width:46ch;margin-inline:auto;color:var(--text);
}
.empty>.btn-row:last-child,
.empty>.links:last-child,
.empty>.map-links:last-child{
  margin:var(--s5) calc(-1 * var(--s5)) calc(-1 * var(--s6));
  padding:var(--s4) var(--s5);
  border-top:1px solid var(--hair-gold-soft);
  background:var(--gold-tint);
  justify-content:center;
}
/* Defensive: an empty state dropped on navy still has to read.             */
.section--dark .empty,.card--dark .empty{
  background:var(--ink-wash);border-color:var(--hair-gold-soft);color:var(--ink-on-dark);
}
.section--dark .empty>p:first-of-type,.card--dark .empty>p:first-of-type{color:var(--muted-1)}
.section--dark .empty>.btn-row:last-child,
.section--dark .empty>.links:last-child,
.section--dark .empty>.map-links:last-child{background:var(--ink-wash)}

/* 15.5 "Next service" on the home page. site.js leaves this element empty
   when there is nothing to announce, so it must collapse - an empty bordered
   box is worse than no box. It stays a block, not a centring flex row:
   nosotros.html carries an inline text-align:left on its copy, and
   justify-content would silently override that author decision. The existing
   .events-next .btn-row rule keeps centring the buttons on both pages, which
   is exactly today's behaviour. The lozenge repeats the empty-state seal.   */
.events-next{
  margin-top:var(--s6);padding:var(--s4) var(--s5);
  border:1px solid var(--hair-gold-soft);border-radius:var(--r-lg);
  background:var(--gold-tint);
}
.section--dark .events-next,.card--dark .events-next{background:var(--ink-wash)}
.events-next:empty{display:none}
.events-next>p{margin:0;font-size:var(--fs-lead);font-weight:600}
.events-next>p::before{
  content:"";display:inline-block;width:9px;height:9px;margin-right:10px;
  position:relative;top:-1px;background:var(--gold);
  clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%);
}
.events-next>.btn-row{margin-top:var(--s3)}

/* 15.6 Podcast show header. The artwork is a placeholder until a real cover
   lands in the JSON, so the frame lives on the tile, not on the image: one
   overlay gives the dove version depth and a future cover a lit edge and a
   vignette without touching the file itself. The overlay is absolutely
   positioned, so it is not a flex item of .podcast-art's own flex box.     */
.podcast-art::after{
  content:"";position:absolute;inset:0;z-index:2;pointer-events:none;
  border-radius:inherit;
  background:
    radial-gradient(120% 82% at 18% 6%,rgba(255,255,255,.18),rgba(255,255,255,0) 54%),
    linear-gradient(180deg,rgba(10,17,40,0) 58%,rgba(10,17,40,.34) 100%);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);
}
.podcast-art .podcast-art-sub{position:relative;z-index:3}
.podcast-art .podcast-art-name{position:relative;z-index:3}
/* The episode count is a fact about the show, so it gets a tag of its own.
   Scoped one level deeper than the page block that also sets it. It ships
   with [hidden], and [hidden]{display:none!important} outranks this.       */
.podcast-show-text .podcast-count{
  display:inline-block;margin-top:var(--s4);
  padding:6px 14px;border-radius:var(--r-pill);
  background:var(--gold-tint);border:1px solid var(--hair-gold-soft);
  color:var(--text);
  font:640 .8rem/1.3 var(--font-display);font-stretch:88%;
  letter-spacing:.14em;text-transform:uppercase;
  font-variant-numeric:tabular-nums;
}
/* Media framed rather than dropped in: the native player sits on a plate.
   The inset is deliberately small - audio is width:100% inside a card that
   is about 186px wide at 320px, and the native control bar has its own
   minimum, so a wide pad would squeeze the transport controls.             */
.episode>audio{
  padding:var(--s2);border-radius:var(--r-md);
  background:var(--cream-2);
  box-shadow:inset 0 0 0 1px var(--paper-line);
}
.feed-box .feed-url{
  border-left:3px solid var(--gold);
  box-shadow:var(--shadow-1);
  font-variant-numeric:tabular-nums;
}

/* 15.7 Figures. A hairline ring holds the photo inside the plate, and the
   caption is separated from the image instead of floating under it.
   about-photo centres its caption on nosotros.html, so nothing here assumes
   a text alignment.                                                        */
.figure{position:relative;background:var(--white)}
.figure::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  border-radius:inherit;
  box-shadow:inset 0 0 0 1px var(--hair-gold-soft);
}
.figure figcaption{border-top:1px solid var(--hair-gold-soft)}
.section--dark .figure figcaption{border-top-color:var(--hair-gold-soft)}
.figure img{transition:transform .6s cubic-bezier(.2,.7,.2,1)}

/* 15.8 Infocard. Hard full-height dividers look cheap; hairlines that fade
   out at both ends look printed. The borders stay in place at zero opacity
   so the cells keep the exact widths they have today.                      */
.infocard{position:relative}
.infocard::before{
  content:"";position:absolute;inset:0 14px auto;height:1px;pointer-events:none;
  background:linear-gradient(90deg,transparent,var(--hair-gold),transparent);
}
.infocard .cell{position:relative}
.infocard .cell + .cell{border-top-color:transparent}
.infocard .cell + .cell::before{
  content:"";position:absolute;top:0;left:14%;right:14%;height:1px;
  background:linear-gradient(90deg,transparent,var(--hair-gold),transparent);
}
@media (min-width:700px){
  .infocard .cell + .cell{border-left-color:transparent}
  .infocard .cell + .cell::before{
    top:12%;bottom:12%;left:0;right:auto;width:1px;height:auto;
    background:linear-gradient(180deg,transparent,var(--hair-gold),transparent);
  }
}

/* 15.9 Map plate. A vignette lifts the gold chip off the lightest parts of
   the tiles; the chip is raised above the overlay so it stays crisp.       */
.map-plate::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  border-radius:inherit;
  background:linear-gradient(180deg,rgba(10,17,40,.22) 0%,rgba(10,17,40,0) 26%,rgba(10,17,40,0) 60%,rgba(10,17,40,.30) 100%);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.18);
}
.map-plate .map-chip{z-index:2}

/* 15.10 Zelle. The QR is the church's real bank code: it is never recoloured,
   cropped, filtered or overlaid. Everything here is the furniture around it -
   a deeper plate, a purple brand seam at the top edge, and the word Zelle as
   a badge instead of loose purple text. The tile stays white and the code
   keeps its quiet zone: 380px of tile less 48px of padding still clears the
   300px the image is capped at.                                            */
.zelle-tile{
  position:relative;overflow:hidden;
  border-radius:var(--r-lg);
  padding:var(--s6) var(--s5) var(--s5);
  box-shadow:var(--shadow-2);
}
.zelle-tile::before{
  content:"";position:absolute;inset:0 0 auto;height:6px;pointer-events:none;
  background:linear-gradient(90deg,var(--zelle),#8b4ae6 52%,var(--zelle));
}
.zelle-word{
  display:inline-block;margin-top:var(--s4);
  padding:7px 20px;border-radius:var(--r-pill);
  background:var(--zelle);color:var(--white);
  font:700 1.05rem/1.25 var(--font-display);font-stretch:90%;
  letter-spacing:.16em;text-transform:uppercase;
}

/* 15.11 Motion. Rows arrive in sequence after their panel does, using the
   site's own curve and the same is-in switch the sections use. The delays
   start at .10s so a row never races the panel it sits in. Every path back
   to visible is guaranteed: no-js keeps them visible, the IntersectionObserver
   adds is-in, its 4s timer adds is-in to everything regardless, and the
   reduced-motion block at the end of this section shows them outright.     */
html:not(.no-js) body.page-glass main>section:not(.hero) .events-list>*,
html:not(.no-js) body.page-glass main>section:not(.hero) .episode-list>*,
html:not(.no-js) body.page-glass main>section:not(.hero) .newsletters-list>*{
  opacity:0;transform:translateY(14px);
  transition:opacity .7s cubic-bezier(.2,.7,.2,1),transform .7s cubic-bezier(.2,.7,.2,1);
}
html:not(.no-js) body.page-glass main>section.is-in .events-list>*,
html:not(.no-js) body.page-glass main>section.is-in .episode-list>*,
html:not(.no-js) body.page-glass main>section.is-in .newsletters-list>*{
  opacity:1;transform:none;
}
html:not(.no-js) body.page-glass main>section.is-in .events-list>:nth-child(1),
html:not(.no-js) body.page-glass main>section.is-in .episode-list>:nth-child(1),
html:not(.no-js) body.page-glass main>section.is-in .newsletters-list>:nth-child(1){transition-delay:.10s}
html:not(.no-js) body.page-glass main>section.is-in .events-list>:nth-child(2),
html:not(.no-js) body.page-glass main>section.is-in .episode-list>:nth-child(2),
html:not(.no-js) body.page-glass main>section.is-in .newsletters-list>:nth-child(2){transition-delay:.17s}
html:not(.no-js) body.page-glass main>section.is-in .events-list>:nth-child(3),
html:not(.no-js) body.page-glass main>section.is-in .episode-list>:nth-child(3),
html:not(.no-js) body.page-glass main>section.is-in .newsletters-list>:nth-child(3){transition-delay:.24s}
html:not(.no-js) body.page-glass main>section.is-in .events-list>:nth-child(n+4),
html:not(.no-js) body.page-glass main>section.is-in .episode-list>:nth-child(n+4),
html:not(.no-js) body.page-glass main>section.is-in .newsletters-list>:nth-child(n+4){transition-delay:.30s}

/* 15.12 Hover belongs to mice. The file has 22 hover rules and no capability
   guard, so on a phone a tapped card keeps its lift until you tap elsewhere.
   Everything new here is gated; the lifts these components inherit from 14.6
   and 14.9 are cancelled outright on coarse pointers, where focus-within in
   15.2 and the browser's own tap feedback do the work instead. The dark
   "next up" card is named separately because it restores a different base
   border and shadow than the cream rows do.                                */
@media (hover:hover) and (pointer:fine){
  .figure:hover img{transform:scale(1.025)}
  .newsletter-item.card:hover h3 a::after{transform:translateX(3px)}
  .newsletter-item.card:hover h3 a{text-decoration-color:var(--gold)}
}
@media (hover:none),(pointer:coarse){
  .event.card:not(.card--dark):hover,
  .episode.card:hover,
  .newsletter-item.card:hover{
    transform:none;box-shadow:var(--shadow-1);border-color:var(--paper-line);
  }
  .event-next-card:hover{
    transform:none;box-shadow:var(--shadow-2);border-color:var(--hair-gold-soft);
  }
  /* 14.9's base plate shadow, restated because it is not tokenised.        */
  .map-plate:hover{
    transform:none;border-color:var(--hair-gold);
    box-shadow:0 2px 8px rgba(10,17,40,.10),0 18px 42px rgba(10,17,40,.22);
  }
  .figure:hover img{transform:none}
}
/* After the coarse block on purpose: same specificity as .map-plate:hover,
   so a keyboard focus on a touch device is not eaten by a sticky hover.    */
.map-plate:focus-visible{border-color:var(--gold)}

/* 15.13 Reduced motion. Neutralises every transition and transform added in
   this section, and guarantees the staggered rows are simply present.      */
@media (prefers-reduced-motion:reduce){
  html:not(.no-js) body.page-glass main>section:not(.hero) .events-list>*,
  html:not(.no-js) body.page-glass main>section:not(.hero) .episode-list>*,
  html:not(.no-js) body.page-glass main>section:not(.hero) .newsletters-list>*{
    opacity:1;transform:none;transition:none;transition-delay:0s;
  }
  .figure img{transition:none}
  .figure:hover img{transform:none}
  .newsletter-item h3 a::after{transition:none}
  .newsletter-item.card:hover h3 a::after{transform:none}
}

@media print{
  .empty::before,.empty::after,
  .podcast-art::after,.map-plate::after,.figure::after,
  .infocard::before,.infocard .cell + .cell::before,
  .zelle-tile::before,.events-next>p::before{display:none!important}
  .empty,.events-next,.zelle-tile,.episode>audio{box-shadow:none!important}
  .events-list:not(.events-list--rows) .event>.btn-row,
  .event-next-card>.btn-row,
  .episode>.links,
  .empty>.btn-row:last-child,
  .empty>.links:last-child,
  .empty>.map-links:last-child{background:none!important}
  /* 15.8 hid the cell borders behind gradients that do not print; give the
     printed infocard its dividers back.                                    */
  .infocard .cell + .cell{border-color:#999!important}
}

/* ===== craft-2:forms ===== */
/* ==========================================================================
   14.15 Forms and feedback — the five states a person actually meets.
   site.js owns the behaviour: it sets novalidate, calls checkValidity() then
   reportValidity() on submit, marks the form aria-busy while the POST is in
   flight, disables the submit button and swaps its label to "Enviando…",
   then appends .form-msg--ok (with every field hidden) or inserts
   .form-msg--error above the button. The hooks below are the hooks the
   engine already produces:
     idle       .field input
     invalid    aria-invalid on the control, .is-invalid on its wrapper
     sending    .form[aria-busy="true"] and [type="submit"]:disabled
     succeeded  .form-msg--ok
     failed     .form-msg--error
   Nothing here adds a child element to anything carrying data-es or data-en:
   every mark is a pseudo-element or a background image, so a language swap,
   which rewrites textContent, cannot erase it. Every page carries
   body.page-glass, so any rule that must beat a body.page-glass rule is
   written with that prefix beside the plain one.
   ========================================================================== */

/* 14.15.1 Resting state — a field should look like a place to write before
   it is touched: warm hairline, a darker lower edge so the box reads as a
   shallow trough, and the same 16px minimum everywhere (anything smaller and
   iOS Safari zooms the page the moment the caret lands).                  */
.field input,.field textarea,.field select{
  padding:12px 14px;
  border-color:var(--text-muted);
  border-bottom-color:var(--text);
  border-radius:var(--r-sm);
  font-size:max(1rem,16px);
  box-shadow:inset 0 1px 2px rgba(10,17,40,.07);
  transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;
}
.field textarea{line-height:1.5}
.field input::placeholder,.field textarea::placeholder{color:var(--text-muted);opacity:1}

/* Labels sit above the control and are only as wide as their words. The
   colour of a hint is left to .small, which already reads correctly on both
   cream and navy — setting it here would blind a hint inside a dark panel. */
.field label{
  width:fit-content;max-width:100%;
  font-size:1rem;font-weight:620;font-stretch:92%;letter-spacing:.005em;
  color:var(--text);cursor:pointer;
}
.field .small{margin-top:var(--s2)}

/* The character counter is filled in by contacto.js after the page paints,
   so its line is reserved here — otherwise the button jumps down on load.
   Tabular figures stop the count jittering as digits change width.         */
.form [data-msg-count]{
  display:block;
  min-height:1.35em;
  font-variant-numeric:tabular-nums;
  letter-spacing:.01em;
}

/* Chrome paints autofilled fields canary yellow and ignores background-color.
   A thick inset shadow is the one thing it does honour, and it has to outrank
   the focus rule below or the yellow returns the moment the field is focused. */
.form .field input:-webkit-autofill,
.form .field input:-webkit-autofill:hover{
  -webkit-text-fill-color:var(--text);
  caret-color:var(--text);
  box-shadow:inset 0 0 0 100px var(--white),inset 0 1px 2px rgba(10,17,40,.07);
}
.form .field input:-webkit-autofill:focus,
.form .field input:-webkit-autofill:focus-visible{
  -webkit-text-fill-color:var(--text);
  caret-color:var(--text);
  box-shadow:inset 0 0 0 100px var(--white),0 0 0 4px rgba(227,193,105,.40);
}

/* 14.15.2 Focus — obvious, not loud: the border darkens to ink, a 2px ring
   sits just off the edge, and a soft gold halo ties it to the brand. Three
   signals at once, so it survives forced colours and low-contrast screens. */
.form .field input:focus-visible,
.form .field textarea:focus-visible,
.form .field select:focus-visible{
  outline:2px solid var(--focus);
  outline-offset:2px;
  border-color:var(--focus);
  background-color:var(--white);
  box-shadow:inset 0 1px 2px rgba(10,17,40,.05),0 0 0 4px rgba(227,193,105,.40);
}

/* 14.15.3 Choice controls — the language question is two answers, so it reads
   as two selectable pills rather than two loose dots. The native input keeps
   its own focus ring and its 44px row; only the surface around it changes.
   :has() is the whole trick here, and a browser without it simply shows the
   plain radios it shows today — the state is never carried by the pill alone. */
.radio-group{gap:var(--s3)}
.radio-group .radio{
  padding:var(--s1) var(--s4) var(--s1) var(--s3);
  border:1px solid var(--paper-line);
  border-radius:var(--r-pill);
  background:var(--white);
  transition:border-color .18s ease,background-color .18s ease,box-shadow .18s ease;
}
.radio-group .radio label{flex:1 1 auto;padding-block:var(--s2);font-weight:600}
.radio-group .radio:has(input:checked){
  border-color:var(--gold-deep);
  background:var(--gold-tint);
  box-shadow:inset 0 0 0 1px var(--gold-deep);
}
.radio-group .radio:has(input:focus-visible){border-color:var(--focus)}

/* 14.15.4 Invalid — never colour alone. A failed field gets a heavier rim
   (drawn with an inset shadow so the box does not grow and shove the page),
   a tinted ground, and an alert glyph; a failed consent box gets a ring around
   the box itself and its line turns to error ink. The tint is scoped to
   controls inside a .field: painted straight onto a native checkbox or radio
   it fights the browser's own rendering. The browser's message still appears
   on top of all of it. site.js marks the nearest of .field/.checkbox/.radio/
   fieldset, so a failed radio group lands on .radio, not on the fieldset —
   both are styled here. */
.form .field [aria-invalid="true"]{
  border-color:var(--error-text);
  background-color:var(--error-bg);
  box-shadow:inset 0 0 0 1px var(--error-text);
}
.field.is-invalid input,
.field.is-invalid textarea{
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a1c1c' stroke-width='2.1' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9.2'/%3E%3Cpath d='M12 7.4v5.4'/%3E%3Cpath d='M12 16.4h.01'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right 12px center;
  background-size:20px 20px;
  padding-right:42px;
}
.field.is-invalid textarea{background-position:right 12px top 14px}
.form .field [aria-invalid="true"]:focus-visible{
  outline-color:var(--error-text);
  border-color:var(--error-text);
  background-color:var(--white);
  box-shadow:inset 0 0 0 1px var(--error-text),0 0 0 4px rgba(138,28,28,.22);
}
.checkbox.is-invalid input,.radio.is-invalid input{
  outline:2px solid var(--error-text);outline-offset:3px;accent-color:var(--error-text);
}
.checkbox.is-invalid label,.radio.is-invalid label{color:var(--error-text)}
.radio-group .radio.is-invalid{border-color:var(--error-text);background:var(--error-bg)}
fieldset.is-invalid legend{color:var(--error-text)}

/* 14.15.5 Submit and sending — the button keeps one width across "Suscribirme",
   "Subscribe" and "Enviando…", so swapping the label never moves the page, and
   min() keeps it inside the 193px card column on a phone. In flight it stays at
   full strength with a small spinner beside the word: a dimmed, greyed button
   reads as broken, not as working. The form is no longer faded either — that
   dimming forced a compositing layer over the glass panels. */
.form [type="submit"]{min-width:min(100%,240px)}
.form[aria-busy="true"]{opacity:1;cursor:progress}
.form[aria-busy="true"] [type="submit"]{opacity:1;cursor:progress}
.form[aria-busy="true"] [type="submit"]::after{
  content:"";flex:none;width:17px;height:17px;border-radius:50%;
  border:2px solid rgba(22,38,77,.30);border-top-color:var(--text);
  animation:frm-spin .7s linear infinite;
}

/* 14.15.6 The answer arrives. Success replaces the fields, so it is centred
   and carries a struck seal above the line; failure drops in above the button
   it belongs to, keeps the ink dark for legibility, and states itself with a
   bar and a glyph as well as a tint.                                        */
.form-msg{
  border-radius:var(--r-md);
  box-shadow:var(--shadow-1);
  animation:frm-arrive-down .42s cubic-bezier(.2,.7,.2,1) both;
}
.form-msg[tabindex]:focus:not(:focus-visible){box-shadow:var(--shadow-1),0 0 0 3px var(--hair-gold)}

.form-msg--ok{
  padding:var(--s6) var(--s5);
  border:1px solid var(--gold-deep);
  background:linear-gradient(180deg,var(--white),var(--gold-tint));
  text-align:center;
  animation-name:frm-arrive-up;
}
.form-msg--ok::before{
  content:"";display:block;width:44px;height:44px;margin:0 auto var(--s4);
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='11' fill='%23cfa94a'/%3E%3Cpath d='M7.3 12.5l3.1 3.1 6.3-6.5' fill='none' stroke='%2316264d' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 44px 44px no-repeat;
  animation:frm-pop .45s cubic-bezier(.2,.9,.3,1.25) .12s both;
}
.form-msg--ok p{font-size:var(--fs-lead);line-height:1.45;text-wrap:balance}

.form-msg--error{
  border:1px solid rgba(138,28,28,.32);
  border-left:4px solid var(--error-text);
  padding-left:calc(var(--s4) + 34px);
  background-color:var(--error-bg);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a1c1c' stroke-width='2.1' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9.2'/%3E%3Cpath d='M12 7.4v5.4'/%3E%3Cpath d='M12 16.4h.01'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:left 16px top 18px;
  background-size:22px 22px;
}
.form-msg--error .fb-link{align-items:flex-start}
.form-msg--error .fb-link .fb-badge{margin-top:2px}
@media (max-width:379px){
  .form-msg--error{padding-left:calc(var(--s3) + 28px);background-position:left 10px top 16px;background-size:20px 20px}
}

/* 14.15.7 Copy to clipboard — the tick replaces the clipboard glyph inside the
   same 18px box (the icon is stroked with currentColor, so hiding the stroke
   and painting a tick behind it swaps the symbol without moving anything).
   The status line beside it keeps the size ofrendas.html gives it: it is a
   3-second message, and reserving a 42px chip for it would leave a permanent
   hole under the button.                                                    */
.copy-btn svg{transition:opacity .2s ease}
.copy-btn.is-copied svg{
  stroke:transparent;
  background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2316264d' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12.8l4.6 4.6L19.5 7'/%3E%3C/svg%3E") center / 18px 18px no-repeat;
  animation:frm-pop .32s cubic-bezier(.2,.9,.3,1.25) both;
}

/* 14.15.8 Pointer capability. Every hover state in this section is gated, and
   on a touch screen the inherited button hovers are cancelled — otherwise the
   lift and the glow stick to the button after the tap that submitted the form.
   body.page-glass .btn:hover is 0-3-1, so the cancels carry the same prefix or
   they lose. Touch gets a press instead, which is the feedback that helps.   */
@media (hover:hover) and (pointer:fine){
  .form .field input:hover:not(:focus),
  .form .field textarea:hover:not(:focus),
  .form .field select:hover:not(:focus){
    border-color:var(--text);
    box-shadow:inset 0 1px 3px rgba(10,17,40,.11);
  }
  .radio-group .radio:hover{border-color:var(--gold-deep)}
}
@media (hover:none),(pointer:coarse){
  .form .btn:hover,.copy-btn:hover,
  body.page-glass .form .btn:hover,body.page-glass .copy-btn:hover{
    transform:none;box-shadow:none;
  }
  .form .btn-primary:hover,body.page-glass .form .btn-primary:hover{
    background:linear-gradient(180deg,#f2d891 0%,var(--gold) 46%,var(--gold-deep) 100%);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.62),0 8px 22px rgba(207,169,74,.34);
  }
  .copy-btn:hover:not(.is-copied),body.page-glass .copy-btn:hover:not(.is-copied){background:transparent}
}
.form .btn:active:not(:disabled),.copy-btn:active:not(:disabled){transform:translateY(1px) scale(.995)}
.radio-group .radio:active{background:var(--gold-tint)}

/* 14.15.9 Motion is decoration here, never a gate on finishing the form: every
   animation is on an element that has just appeared, and each one starts from
   its own visible resting style, so switching it off can never hide anything. */
@keyframes frm-arrive-down{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
@keyframes frm-arrive-up{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes frm-pop{from{opacity:0;transform:scale(.72)}to{opacity:1;transform:none}}
@keyframes frm-spin{to{transform:rotate(360deg)}}

@media (prefers-reduced-motion:reduce){
  .form-msg,.form-msg--ok::before,.copy-btn.is-copied svg,
  .form[aria-busy="true"] [type="submit"]::after{
    animation:none;opacity:1;transform:none;
  }
  .form[aria-busy="true"] [type="submit"]::after{border-color:var(--text);opacity:.45}
  .form .btn:active:not(:disabled),.copy-btn:active:not(:disabled),
  .field input,.field textarea,.field select,.radio-group .radio,.copy-btn svg{
    transform:none;transition:none;
  }
  /* 14.11 cancels .btn:hover at 0-2-0, which body.page-glass .btn:hover
     outranks on every page of this site. Same cancel, high enough to land. */
  body.page-glass .btn:hover,body.page-glass .card:hover,body.page-glass .map-plate:hover{transform:none}
}
@media print{
  .form-msg{box-shadow:none;break-inside:avoid}
  .form[aria-busy="true"] [type="submit"]::after{display:none}
}

/* ===== craft-2:pointer ===== */
/* ==========================================================================
   15. Pointer, press and focus - one vocabulary for everything you can touch.

   The file had 22 hover rules and no hover-capability guard, so on a phone
   every hover state stuck to whatever was tapped last: the language pill
   stayed solid gold, a tapped card floated 4px off the page, a nav row kept
   its tint until something else was tapped. This section fixes that in three
   moves - put the legacy hovers back where there is no real pointer, give
   touch a press instead, and restate hover as one designed idea for pointers
   that can actually hover.

   THE VOCABULARY - gold arrives at the leading edge, then the surface lifts.
     - a link with no underline at rest gets one that wipes in from the left
     - a link already underlined keeps it, and that underline warms to gold
     - a surface you can act on rises, its border warms, its shadow deepens
     - the gold button takes one specular pass, left to right
     - an icon advances the way it points
     - every press settles to zero and scales to .97, immediately
   One easing curve and three durations carry all of it, so the header, the
   footer and a card in the middle of a page all move like one hand made them.
   ========================================================================== */

/* 15.1 The timing vocabulary, and the accent token that resolves per ground
   so a single hover rule is correct on cream and on navy glass alike. */
:root{
  --mi-ease:cubic-bezier(.2,.7,.2,1);       /* the house curve, from 14.x   */
  --mi-spring:cubic-bezier(.34,1.4,.64,1);  /* the button overshoot, 14.5   */
  --mi-press:.1s;                           /* a press answers immediately  */
  --mi-quick:.2s;                           /* tint, border, small lift     */
  --mi-move:.3s;                            /* wipes, card lift, shadow     */
  --mi-underline:var(--gold-deep);
  --mi-halo:rgba(22,38,77,.16);
  --gold-lit:#f2d891;                       /* lit top of the gold face, mirrors 14.5 */
  /* Shadow stacks as tokens so the touch revert and the focus ring can put
     back exactly what sections 14.5 and 14.9 set. Keep these in step. */
  --mi-btn-lit:inset 0 1px 0 rgba(255,255,255,.62),0 8px 22px rgba(207,169,74,.34);
  --mi-plate-rest:0 2px 8px rgba(10,17,40,.10),0 18px 42px rgba(10,17,40,.22);
  --mi-card-lift:0 4px 10px rgba(10,17,40,.08),0 22px 48px rgba(10,17,40,.20);
  --mi-card-lift-dark:0 6px 14px rgba(10,17,40,.20),0 26px 56px rgba(10,17,40,.34);
}
.site-header,.hero,.page-hero,.section--dark,.card--dark,.infocard,.site-footer{
  --mi-underline:var(--gold);
  --mi-halo:rgba(227,193,105,.30);
}
/* A cream card inside a dark section is still a cream ground, and so is the
   dashed empty state. A figcaption is NOT in this list: section 8 repaints
   it navy-3 inside a dark section, so it keeps the dark ground it inherits. */
.section--dark .card:not(.card--dark),.section--dark .empty{
  --mi-underline:var(--gold-deep);
  --mi-halo:rgba(22,38,77,.16);
}

/* 15.2 One transition set. Declared on the resting state so the way out is
   as considered as the way in, and outside the hover guard so a press on a
   touch screen is animated too. */
.btn,body.page-glass .btn{
  transition:
    transform var(--mi-quick) var(--mi-spring),
    box-shadow var(--mi-move) var(--mi-ease),
    background-color var(--mi-quick) linear,
    border-color var(--mi-quick) linear,
    color var(--mi-quick) linear,
    opacity var(--mi-quick) linear,
    filter var(--mi-quick) linear;
}
.card,.map-plate,.zelle-tile{
  transition:transform var(--mi-move) var(--mi-ease),
             box-shadow var(--mi-move) var(--mi-ease),
             border-color var(--mi-move) var(--mi-ease);
}
.lang-toggle,.nav-toggle{
  transition:transform var(--mi-press) var(--mi-ease),
             background-color var(--mi-quick) linear,
             border-color var(--mi-quick) linear,
             color var(--mi-quick) linear;
}
.fb-badge,.brand-dove,.map-chip{
  transition:transform var(--mi-quick) var(--mi-ease),
             box-shadow var(--mi-move) var(--mi-ease),
             filter var(--mi-move) var(--mi-ease);
}
a,.map-credit{transition:text-decoration-color var(--mi-quick) linear,color var(--mi-quick) linear}

/* 15.3 The leading-edge wipe. The header already owns this gesture (14.4);
   these three lines re-aim it at the leading edge and lend it to the footer,
   so both navigations answer the pointer with the same stroke.
   The gold bar is hidden inside the nav pill - that pill is .btn-primary, so
   its one ::after is spoken for by the sheen below, and gold on gold reads as
   a smudge anyway - and inside the phone panel, where the row is the target,
   not the word. The current page keeps its 2px gold underline there (5.x). */
.nav a::after{transform-origin:0 50%}
.nav a.btn::after{display:none}
@media (max-width:899px){.nav a::after{display:none}}

.footer-nav a{position:relative}
.footer-nav a::after{
  content:"";position:absolute;left:0;right:0;top:calc(50% + .64em);height:2px;
  border-radius:2px;pointer-events:none;
  background:linear-gradient(90deg,var(--gold-deep),var(--gold));
  transform:scaleX(0);transform-origin:0 50%;
  transition:transform var(--mi-move) var(--mi-ease);
}
/* the page you are on keeps the bar, on every device */
.footer-nav a[aria-current="page"]::after,.footer-nav a:active::after{transform:scaleX(1)}

/* 15.4 The specular pass. It rests off the left edge of the gold face and
   sweeps once per hover, so leaving the button needs no unwind - the sweep
   simply ends out of frame. Transform only, and it never loops. */
.btn-primary{position:relative;overflow:hidden}
.btn-primary::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(104deg,rgba(255,255,255,0) 34%,rgba(255,255,255,.45) 50%,rgba(255,255,255,0) 66%);
  transform:translateX(-130%);
}
@keyframes mi-sheen{from{transform:translateX(-130%)}to{transform:translateX(130%)}}

/* the map chip's arrow needs a box before it can advance */
.map-chip::after{display:inline-block;transition:transform var(--mi-quick) var(--mi-ease)}

/* 15.5 The skip link arrives instead of appearing. It is parked above the
   viewport by transform, not by a negative top, so it travels on focus. */
.skip-link{
  top:var(--s3);transform:translateY(calc(-100% - var(--s5)));
  transition:transform var(--mi-quick) var(--mi-ease);
}
.skip-link:focus{transform:none}

/* 15.6 No real pointer - put every legacy hover state back. Without this a
   tap leaves the state behind, because a touch screen has no "leave". */
@media (hover:none),(pointer:coarse){
  a:hover,.map-links a:hover,.episode .links a:hover{text-decoration-thickness:auto}
  .fb-link:hover{text-decoration:none}
  .footer-nav a:hover{text-decoration:none}

  .lang-toggle:hover{background:var(--navy-3);color:var(--gold)}
  .nav-toggle:not([aria-expanded="true"]):hover{background:var(--navy-3)}
  .nav a:hover{background:transparent;text-decoration:none}
  .nav a:hover::after{transform:scaleX(0)}
  /* the line above and 14.4's current-page bar are both (0,2,2), so without
     this the tapped current page would lose its bar until something else is
     tapped. The page you are on is a state, not a hover. */
  .nav a[aria-current="page"]::after{transform:scaleX(1)}

  /* the generic revert carries the shadow too, so a bare .btn cannot keep the
     glass lift (14.x) after a tap; the specific buttons below re-set theirs */
  .btn:hover,body.page-glass .btn:hover{transform:none;box-shadow:none}
  .btn-primary:hover{background:linear-gradient(180deg,var(--gold-lit) 0%,var(--gold) 46%,var(--gold-deep) 100%)}
  .btn-primary:hover,body.page-glass .btn-primary:hover{box-shadow:var(--mi-btn-lit)}
  .btn-outline:hover,body.page-glass .btn-outline:hover{background:transparent;box-shadow:none}
  .hero .btn-outline:hover,.page-hero .btn-outline:hover,.section--dark .btn-outline:hover,.card--dark .btn-outline:hover,.site-footer .btn-outline:hover,.infocard .btn-outline:hover{background:transparent}

  .card:hover{transform:none;box-shadow:var(--shadow-1);border-color:var(--paper-line)}
  .card--dark:hover{box-shadow:var(--shadow-2);border-color:var(--hair-gold-soft)}

  .map-plate:hover{transform:none;border-color:var(--hair-gold);box-shadow:var(--mi-plate-rest)}
  .map-credit:hover{color:var(--text-muted)}
  .section--dark .map-credit:hover,.card--dark .map-credit:hover{color:var(--muted-3)}
}

/* 15.7 Press - the whole answer on a touch screen, and the bottom of the arc
   under a mouse. Fast in, fast out, gone the moment the finger lifts. */
.btn:active{transform:translateY(0) scale(.97)}
.lang-toggle:active{background:var(--gold);color:var(--text);border-color:var(--gold);transform:scale(.96)}
.nav-toggle:active{background:var(--ink-wash);transform:scale(.96)}
.map-plate:active{transform:scale(.995)}
.fb-link:active .fb-badge{transform:scale(.94)}
a:active{text-decoration-thickness:2px;text-decoration-color:var(--mi-underline)}
.map-credit:active{color:var(--text)}
@media (max-width:899px){.nav a:active{background:var(--ink-wash)}}
/* the copy button takes one beat of its own; ofrendas.html still paints the
   confirmed state gold-tint for its three seconds, and that is its to own */
.copy-btn.is-copied{animation:mi-pop .32s var(--mi-spring) 1}
@keyframes mi-pop{from{transform:scale(1)}40%{transform:scale(1.045)}to{transform:scale(1)}}

/* 15.8 Hover, for pointers that can actually hover. */
@media (hover:hover) and (pointer:fine){

  /* Content links: the underline is already drawn, so it warms and thickens.
     Nothing gains an underline it did not have, which keeps the wordmark,
     the skip link and the map plate free of stray rules. */
  a:hover{text-decoration-thickness:2px;text-decoration-color:var(--mi-underline)}
  .map-credit:hover{color:var(--text);text-decoration-color:var(--mi-underline)}
  .section--dark .map-credit:hover,.card--dark .map-credit:hover{color:var(--ink-on-dark)}

  /* Navigation: gold wipes in from the leading edge, header and footer alike. */
  .footer-nav a:hover{text-decoration:none}
  .footer-nav a:hover::after{transform:scaleX(1)}

  /* The Facebook line: the badge lifts, the name takes the gold underline. */
  .fb-link:hover{text-decoration-color:var(--mi-underline)}
  .fb-link:hover .fb-badge{transform:translateY(-1px);box-shadow:0 6px 16px rgba(24,119,242,.40)}

  /* The wordmark: the dove catches a little more light. Transform only, so
     the header stays exactly one 66px row (14.14). */
  .brand:hover .brand-dove{transform:translateY(-1px) scale(1.04);filter:drop-shadow(0 0 16px rgba(170,200,255,.65))}

  /* Header controls. */
  .lang-toggle:hover{background:var(--gold);color:var(--text);border-color:var(--gold);transform:translateY(-1px);box-shadow:0 6px 16px rgba(227,193,105,.34)}
  .nav-toggle:hover{background:var(--ink-wash);border-color:var(--gold)}

  /* Buttons: the outline warms to gold, the gold face takes its one pass. */
  .btn-outline:hover{border-color:var(--gold-deep)}
  .hero .btn-outline:hover,.page-hero .btn-outline:hover,.section--dark .btn-outline:hover,.card--dark .btn-outline:hover,.site-footer .btn-outline:hover,.infocard .btn-outline:hover{border-color:var(--gold)}
  .btn-primary:hover::after{animation:mi-sheen .62s var(--mi-ease) 1}

  /* Cards: only the ones you can act on move. A panel that does nothing
     should not answer the pointer - never promise a click you cannot cash.
     This is why "Idioma" now sits still while "Donde" rises. */
  .card:hover{transform:none;box-shadow:var(--shadow-1);border-color:var(--paper-line)}
  .card--dark:hover{box-shadow:var(--shadow-2);border-color:var(--hair-gold-soft)}
  .card:has(a[href],button:not([hidden])):hover{
    transform:translateY(-4px);border-color:var(--hair-gold);box-shadow:var(--mi-card-lift);
  }
  .card--dark:has(a[href],button:not([hidden])):hover{
    border-color:var(--gold);box-shadow:var(--mi-card-lift-dark);
  }
  /* two lifts stacked read as a glitch, not as depth: when the plate inside
     a card is the thing being pointed at, the plate rises and the card waits */
  .card:has(.map-plate:hover):hover{transform:none;box-shadow:var(--shadow-1)}

  /* The map plate lifts (14.9) and its chip advances with the arrow. */
  .map-plate:hover .map-chip{transform:translateY(-1px)}
  .map-plate:hover .map-chip::after{transform:translate(2px,-2px)}

  /* The Zelle tile is not a link. It acknowledges the pointer with light
     only, never with movement. */
  .zelle-tile:hover{box-shadow:0 12px 32px rgba(109,30,212,.20)}
}

/* 15.9 Focus. The glass pages paint one halo for everything (14.10), which
   also erases the gold button's lit edge and the map plate's shadow. These
   restate each control's own shadow with the halo added, and the halo colour
   follows the ground the same way the underline does. Each rule carries a
   body.page-glass twin because 14.10's own selector is (0,2,1) and would
   otherwise outrank the bare class on every page of this site. */
.btn:focus-visible,.lang-toggle:focus-visible,.nav-toggle:focus-visible,
body.page-glass .btn:focus-visible,body.page-glass .lang-toggle:focus-visible,body.page-glass .nav-toggle:focus-visible{
  box-shadow:0 0 0 6px var(--mi-halo);
}
.btn-primary:focus-visible,body.page-glass .btn-primary:focus-visible{box-shadow:var(--mi-btn-lit),0 0 0 6px var(--mi-halo)}
.map-plate:focus-visible,body.page-glass .map-plate:focus-visible{box-shadow:var(--mi-plate-rest),0 0 0 6px var(--mi-halo)}
.field input:focus-visible,.field textarea:focus-visible,.field select:focus-visible{
  border-color:var(--text);box-shadow:0 0 0 4px var(--mi-halo);
}

/* 15.10 A control that cannot be pressed has to look like it, and a form in
   flight has to say so. site.js disables the submit and sets aria-busy. */
.btn:disabled,.btn[aria-disabled="true"]{cursor:not-allowed;box-shadow:none;filter:saturate(.5)}
.btn:disabled:hover,.btn[aria-disabled="true"]:hover,
body.page-glass .btn:disabled:hover,body.page-glass .btn[aria-disabled="true"]:hover{transform:none;box-shadow:none}
.btn:disabled:active,.btn[aria-disabled="true"]:active{transform:none}
.btn-primary:disabled:hover::after,.btn-primary[aria-disabled="true"]:hover::after{animation:none}
form[aria-busy="true"] [type="submit"]{cursor:progress}

/* 15.11 Reduced motion. Section 13 already kills every transition and every
   animation with !important, so what is left to neutralise are the transforms
   that are applied outright. 14.11 does this for .btn:hover at (0,2,0), which
   does NOT reach the glass rule at (0,3,1) - that one is restated here.
   Two transforms are deliberately NOT listed, because in both cases the
   transform is what hides an element rather than a motion: the skip link
   parked above the viewport, and the wipe bars, which simply appear at full
   width once the transition is gone. */
@media (prefers-reduced-motion:reduce){
  .btn-primary::after,.btn-primary:hover::after{animation:none}
  .copy-btn.is-copied{animation:none}
  .btn:hover,body.page-glass .btn:hover{transform:none}
  .btn:active,.lang-toggle:active,.nav-toggle:active,.map-plate:active{transform:none}
  .lang-toggle:hover{transform:none}
  .fb-link:hover .fb-badge,.fb-link:active .fb-badge{transform:none}
  .brand:hover .brand-dove{transform:none}
  .map-plate:hover .map-chip,.map-plate:hover .map-chip::after{transform:none}
  .card:has(a[href],button:not([hidden])):hover,
  .card--dark:has(a[href],button:not([hidden])):hover{transform:none}
}

/* 15.12 Print: decoration that only exists for a pointer has no page. */
@media print{
  .btn-primary::after,.footer-nav a::after{display:none!important}
  .skip-link{display:none!important}
}

/* ===== craft-2:reconcile ===== */
/* ==========================================================================
   16. Reconciliation - six proposals, one page.
   Appended LAST. Every rule here exists because two blocks that could not
   see each other drew the same thing twice, spoke a different motion
   language for the same gesture, or pinned a transform that section 14
   still needs. Nothing new is invented: each conflict is settled in favour
   of the block that owns that idea, and the loser's duplicate is nulled.
   ========================================================================== */

/* 16.1 One motion vocabulary. The house curve is the one four of the six
   blocks already chose; the two strays (plain "ease", and a second spring)
   are re-pointed at it. The arrival curves --cx-land and --cx-settle stay
   as they are: those are deliberately different, and only for entrances. */
:root{
  --ease-house:cubic-bezier(.2,.7,.2,1);
  --ease-spring:cubic-bezier(.34,1.4,.64,1);
  --mi-ease:var(--ease-house);
  --cx-glide:var(--ease-house);
  --mi-spring:var(--ease-spring);
  --dur-press:.1s;--dur-quick:.2s;--dur-move:.3s;--dur-slow:.6s;
  --mi-press:var(--dur-press);--mi-quick:var(--dur-quick);--mi-move:var(--dur-move);
}
body.page-glass .site-header::after{transition:opacity var(--dur-move) var(--ease-house)}
.field input,.field textarea,.field select,.radio-group .radio{
  transition:border-color var(--dur-quick) var(--ease-house),
             box-shadow var(--dur-quick) var(--ease-house),
             background-color var(--dur-quick) var(--ease-house);
}
.copy-btn svg{transition:opacity var(--dur-quick) var(--ease-house)}
.newsletter-item h3 a::after{transition:transform var(--dur-move) var(--ease-house)}
.figure img{transition:transform var(--dur-slow) var(--ease-house)}
.form-msg{animation-timing-function:var(--ease-house)}
.form-msg--ok::before,.copy-btn.is-copied svg{animation-timing-function:var(--ease-spring)}

/* 16.2 One press. The pointer block says a press settles to zero and scales;
   the scroll block had presses LIFT. A gesture cannot mean two things, so
   the settle wins and the lift goes. The button press is already carried by
   source order; these two are not. */
.card:active,.map-plate:active{transform:scale(.995)}

/* 16.3 Cards on a real pointer. The pointer block owns the question of WHICH
   card moves (only one you can act on) and the surface block owns HOW the
   material answers. Neither could see the other, so the lift kept a flat
   shadow and the material was spent on panels that do not move. Here the
   surface recipe is re-applied to exactly the cards that lift. */
@media (hover:hover) and (pointer:fine){
  .card:has(a[href],button:not([hidden])):hover{
    box-shadow:0 2px 4px rgba(10,17,40,.12),0 20px 46px rgba(10,17,40,.20),inset 0 -1px 0 rgba(22,38,77,.05);
    border-color:var(--hair-gold-soft);border-top-color:var(--hair-gold);border-bottom-color:rgba(22,38,77,.13);
  }
  .card--dark:has(a[href],button:not([hidden])):hover{
    box-shadow:0 2px 6px rgba(0,0,0,.24),0 24px 54px rgba(10,17,40,.34),inset 0 1px 0 rgba(227,193,105,.34),inset 0 -1px 0 rgba(0,0,0,.26);
    border-color:var(--hair-gold-soft);border-top-color:var(--gold);border-bottom-color:rgba(0,0,0,.28);
  }
  .card:has(.map-plate:hover):hover{box-shadow:var(--mat-rest)}
  /* the tile answers with light ADDED to its plate, not instead of it */
  .zelle-tile:hover{box-shadow:var(--shadow-2),0 12px 32px rgba(109,30,212,.20)}
}

/* 16.4 The same cards after a tap. Three blocks wrote a touch revert and the
   last one to land dropped the lit top edge and the shaded underside, so a
   tapped card came back as a flat white box. --mat-rest is the surface
   block's own resting stack; the row components are named at their own
   specificity so their reverts cannot undercut it. */
@media (hover:none),(pointer:coarse){
  .card:hover{box-shadow:var(--mat-rest);border-color:var(--paper-line);border-top-color:var(--cream-2);border-bottom-color:rgba(22,38,77,.13)}
  .card--dark:hover{box-shadow:var(--mat-rest);border-color:var(--hair-gold-soft);border-top-color:var(--hair-gold);border-bottom-color:rgba(0,0,0,.28)}
  .event.card:not(.card--dark):hover,
  .episode.card:not(.card--dark):hover,
  .newsletter-item.card:not(.card--dark):hover{
    box-shadow:var(--mat-rest);border-color:var(--paper-line);border-top-color:var(--cream-2);
  }
}

/* 16.5 One reveal for the three JSON lists. The scroll block reveals grid
   children with an animation that never owns the element once it lands; the
   content block revealed the same rows with a transition that DOES own
   transform forever. Two systems drove .events-list at once. The animation
   wins, .episode-list and .newsletters-list join it, and both of them gain
   the 6 s failsafe they were missing - if the observer and site.js's 4 s
   sweep both fail, these rows still appear. */
html:not(.no-js) body.page-glass main>section:not(.hero) :is(.events-list,.episode-list,.newsletters-list)>*{
  transform:none;transition:none;animation:cx-failsafe 1ms linear 6s forwards;
}
html:not(.no-js) body.page-glass main>section.is-in :is(.events-list,.episode-list,.newsletters-list)>*{
  opacity:1;transform:none;transition:none;animation:cx-card-in .85s var(--ease-house) backwards;
}
html:not(.no-js) body.page-glass main>section.is-in :is(.events-list,.episode-list,.newsletters-list)>:nth-child(1){animation-delay:.05s}
html:not(.no-js) body.page-glass main>section.is-in :is(.events-list,.episode-list,.newsletters-list)>:nth-child(2){animation-delay:.14s}
html:not(.no-js) body.page-glass main>section.is-in :is(.events-list,.episode-list,.newsletters-list)>:nth-child(3){animation-delay:.23s}
html:not(.no-js) body.page-glass main>section.is-in :is(.events-list,.episode-list,.newsletters-list)>:nth-child(n+4){animation-delay:.3s}

/* 16.6 A reveal must not confiscate a gesture. Any revealed state that
   declares transform:none - 14.8's container children and the list rules
   above - outranks .card:hover (0,2,0) and kills the lift for the rest of
   the session. The interaction transforms are restated here, above the
   reveal layer. The :not() keeps 14.15's rule that a card holding a hovered
   map plate stays still, so two lifts never stack. */
@media (hover:hover) and (pointer:fine){
  html body.page-glass main>section .card:has(a[href],button:not([hidden])):not(:has(.map-plate:hover)):hover,
  html body.page-glass main>section .card--dark:has(a[href],button:not([hidden])):not(:has(.map-plate:hover)):hover{
    transform:translateY(-4px);
  }
  html body.page-glass main>section .map-plate:hover{transform:translateY(-3px)}
}

/* 16.7 One line per edge. The surface block and the content block each drew
   a ring around every photograph and a divider between every infocard cell,
   so both were painted twice at slightly different insets. Surface keeps the
   outer material (it is the only one that also answers on navy); content
   keeps the inner furniture (its dividers are the ones that fade correctly
   in both directions). */
.figure::after{box-shadow:none}
.infocard::before{display:none}
.infocard .cell + .cell{background-image:none}

/* 16.8 Grounds. The content block repaints the empty state navy inside a
   dark section, which the pointer block could not know - its hover tokens
   there were still the cream-ground pair. */
.section--dark .empty,.card--dark .empty{--mi-underline:var(--gold);--mi-halo:rgba(227,193,105,.30)}

/* 16.9 A form in flight is working, not broken. The pointer block desaturates
   any disabled button; the forms block deliberately keeps the sending button
   at full strength beside its spinner. The forms block is right about its own
   five states. */
.form[aria-busy="true"] [type="submit"]:disabled,
.form[aria-busy="true"] [type="submit"][aria-disabled="true"]{filter:none;opacity:1;cursor:progress}

/* 16.10 Reduced motion for everything section 16 introduces, and for the two
   transforms it moved. Section 13 kills animation and transition with
   !important, so the guarantees here carry !important too - a row that this
   section took over must never be left at opacity 0. */
@media (prefers-reduced-motion:reduce){
  .card:active,.map-plate:active,
  html body.page-glass main>section .card:has(a[href],button:not([hidden])):not(:has(.map-plate:hover)):hover,
  html body.page-glass main>section .card--dark:has(a[href],button:not([hidden])):not(:has(.map-plate:hover)):hover,
  html body.page-glass main>section .map-plate:hover{transform:none!important}
  html body.page-glass main>section :is(.events-list,.episode-list,.newsletters-list)>*{
    opacity:1!important;transform:none!important;animation:none!important;
  }
  body.page-glass .site-header::after,.field input,.field textarea,.field select,
  .radio-group .radio,.copy-btn svg,.newsletter-item h3 a::after,.figure img{transition:none!important}
  .form-msg,.form-msg--ok::before,.copy-btn.is-copied svg{animation:none!important}
}

@media print{
  html body.page-glass main>section :is(.events-list,.episode-list,.newsletters-list)>*{
    opacity:1!important;transform:none!important;animation:none!important;
  }
}

/* ===== craft-2:critic ===== */
/* ==========================================================================
   17. Cross-cut - the five things a dimension-by-dimension split cannot see.
   Appended after 16. Every rule here is a floor, not a look: it decides what
   happens when the page does not get everything it was promised - no video,
   no JavaScript, no pointer, no screen. Nothing here changes the design of a
   page that loads the way the six blocks assumed it would.
   ========================================================================== */

/* 17.1 The reveal failsafe has to be whole. 14.8 hides EVERY child of every
   .container and hands the only key to one JS class. Section 15 added a 6 s
   escape for the children it took ownership of - the eyebrow, the heading,
   the figure, the verse - and pinned the wrappers it recognised. Nothing
   covers the rest, and the rest is the site: the subscribe card on boletin,
   the contact form, the podcast show header, the episode list, the newsletter
   list, the next-service line on index and nosotros, the closing button row
   on five pages. If site.js throws anywhere after its first line - which is
   where it removes .no-js - those elements stay at opacity 0 forever.
   One rule, opacity only, deliberately below every arrival rule in 15.3 and
   16.5, and it must never declare transform: a forwards-filled transform
   would confiscate the card lift for good. */
html:not(.no-js) body.page-glass main>section:not(.hero) .container>*{
  animation:cx-failsafe 1ms linear 6s forwards;
}

/* 17.2 A keyboard can reach content the eye has not scrolled to. Tab moves
   focus into a panel and the browser scrolls it up, but focus lands before
   the observer has run, so for a frame or more the ring is drawn around an
   element at opacity 0 - and on a panel that failed to reveal, forever.
   15.7 wrote this escape for .hero-inner only, where the same risk was
   understood. Here it is for every panel. Opacity only, so a hover lift or
   an entrance transform is never cancelled by a focus that merely passed
   through, and !important because an entrance keyframe starts at zero. */
html:not(.no-js) body.page-glass main>section:focus-within .container>*,
html:not(.no-js) body.page-glass main>section:focus-within .section-head>*,
html:not(.no-js) body.page-glass main>section:focus-within :is(.grid-2,.grid-3,.events-list,.episode-list,.newsletters-list,.infocard)>*{
  opacity:1!important;
}

/* 17.3 The ground. Every panel on this site is translucent over a fixed
   video, and the legibility of the whole site is borrowed from that video
   being lit. When it is not painting - autoplay refused, Low Power Mode,
   Data Saver, a stalled connection, a reader who asked for no motion - the
   ground falls back to --navy-3, and a cream panel at .36 alpha over #0a1128
   resolves to about rgb(96,100,113). Navy body copy on that is near 2.6:1.
   Six passes each protected contrast inside their own layer and every one of
   them assumed the layer underneath would be there.
   The grounds become tokens so the fallback is one switch. .no-video is for
   site.js to set on the video's error, stalled and suspend events, and on a
   play() promise that rejects - three lines, and the CSS is already here. */
body.page-glass{
  --glass-cream:rgba(250,247,242,.36);
  --glass-alt:rgba(243,234,216,.36);
  --glass-navy:rgba(22,38,77,.48);
  --glass-bar:rgba(10,17,40,.42);
  --glass-bar-lit:rgba(10,17,40,.72);
  --glass-foot:rgba(10,17,40,.45);
}
body.page-glass main>section:not(.hero){background-color:var(--glass-cream)}
body.page-glass main>section.section--alt{background-color:var(--glass-alt)}
body.page-glass main>section.section--dark,
body.page-glass main>section.page-hero{background-color:var(--glass-navy)}
body.page-glass .site-header{background-color:var(--glass-bar)}
body.is-scrolled.page-glass .site-header{background-color:var(--glass-bar-lit)}
body.page-glass .site-footer{background-color:var(--glass-foot)}

body.page-glass.no-video{
  --glass-cream:var(--cream);
  --glass-alt:var(--cream-2);
  --glass-navy:var(--navy-2);
  --glass-bar:rgba(10,17,40,.96);
  --glass-bar-lit:rgba(10,17,40,.98);
  --glass-foot:var(--navy-3);
}
body.page-glass.no-video .hero{
  background:radial-gradient(ellipse 120% 80% at 50% 20%,var(--navy-1),var(--navy-2) 45%,var(--navy-3));
}

/* Reduced motion: a fixed, full-viewport, looping clip is the largest moving
   thing on all seven pages, and not one of the six blocks switched it off -
   each of them switched off its own 4 px transforms and trusted the others.
   Section 13 cannot reach it either: autoplay is markup, not animation. */
@media (prefers-reduced-motion:reduce){
  body.page-glass{
    --glass-cream:var(--cream);
    --glass-alt:var(--cream-2);
    --glass-navy:var(--navy-2);
    --glass-bar:rgba(10,17,40,.96);
    --glass-bar-lit:rgba(10,17,40,.98);
    --glass-foot:var(--navy-3);
  }
  body.page-glass .page-video{display:none}
  body.page-glass .hero{
    background:radial-gradient(ellipse 120% 80% at 50% 20%,var(--navy-1),var(--navy-2) 45%,var(--navy-3));
  }
}

/* 17.4 A card you type into must not move. 16.6 lifts any card containing a
   link or a button, which was written for the event, episode and newsletter
   rows - but boletin's subscribe card IS a .card and it holds the whole
   form, so the pointer travelling toward the email field lifts the field out
   from under itself, and the card sits 4 px high for as long as someone is
   typing in it. The same rule takes the full-width band cards and the
   podcast masthead, which are page furniture, not rows: a banner the width
   of the panel rising 4 px reads as the page slipping, not as depth.
   .container in the selector buys the one class needed to outrank 16.6. */
@media (hover:hover) and (pointer:fine){
  html body.page-glass main>section .container .card:has(form,fieldset,input,textarea,select,.copy-row):hover,
  html body.page-glass main>section .container .card--band:hover,
  html body.page-glass main>section .container .podcast-show:hover{
    transform:none;
    box-shadow:var(--mat-rest);
  }
}

/* 17.5 Paper. Five of the six blocks wrote a print block for their own
   decoration; the fixed clip and its shade belong to none of them, so both
   survive to paper - a position:fixed video and a dark navy gradient render
   over the first printed page, and every glass panel prints as a translucent
   wash with a backdrop-filter behind it. */
@media print{
  .page-video,.page-shade{display:none!important}
  body.page-glass{background:#fff!important}
  body.page-glass main>section:not(.hero),
  body.page-glass .site-header,
  body.page-glass .site-footer{
    -webkit-backdrop-filter:none!important;backdrop-filter:none!important;
    background-color:#fff!important;border-color:#999!important;box-shadow:none!important;
  }
  body.page-glass main>section.section--dark,
  body.page-glass main>section.page-hero{background-color:#fff!important;color:var(--text)!important}
  .map-chip{background:none!important;color:#000!important;border-color:#999!important;box-shadow:none!important}
}

/* ===== craft-2:light ===== */
/* ==========================================================================
   18. Light. Two things the workflow could not settle on its own.

   18.1 makes the no-video fallback real - section 17.3 raises every glass
   alpha under body.no-video so the site stays readable when the clip is not
   painting, but nothing was ever setting that class, so the fallback was a
   dead hook. The detection lives in site.js beside it.

   18.2 is the illumination Chris asked for: hovering a panel should read as
   light falling on it. It follows the pointer, because light with a source
   reads as light and a static wash reads as a gradient.

   The one rule it must not break: the craft review measured that a white wash
   over a navy glass panel drops heading contrast from 3.23:1 to 2.76:1,
   because those panels are translucent over a pale sky and their text is
   already the light element. So the light is applied by MATERIAL, not
   uniformly - cream takes an inner wash, navy takes an outer bloom and a
   warmed edge, and nothing pours light across text that is itself light.
   ========================================================================== */

/* 18.2.1 Where the light is, and how strong. Defaults put it off-panel so a
   card that is never hovered, or hovered before the pointer script runs, has
   its gradient centred nowhere visible rather than parked in a corner. */
body.page-glass main>section:not(.hero),
.card,.map-plate{
  --glow-x:50%;
  --glow-y:-40%;
  --glow:0;
}

@media (hover:hover) and (pointer:fine){

  /* Cream panels and cards are paper, and paper takes light on its face.
     Raising a cream panel's luminance under warm light moves its navy text
     further from the ground, so this wash improves contrast rather than
     costing it. background-color is owned by 17.3, so only the image layer
     is set here and the two compose. */
  body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero){
    background-image:radial-gradient(42rem 30rem at var(--glow-x) var(--glow-y),
      rgba(255,244,214,calc(.42 * var(--glow))) 0%,
      rgba(255,238,198,calc(.16 * var(--glow))) 38%,
      rgba(255,236,190,0) 72%);
    transition:background-image .45s cubic-bezier(.2,.7,.2,1),
               border-color .45s cubic-bezier(.2,.7,.2,1),
               box-shadow .45s cubic-bezier(.2,.7,.2,1);
  }
  body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero):hover{
    --glow:1;
    border-color:rgba(227,193,105,.62);
  }

  /* Navy panels are slate. Light does not soak into slate and it must not be
     poured over the pale text sitting on it, so the glow leaves the face
     alone: the edge warms, the top highlight lifts, and the bloom happens
     OUTSIDE the panel where there is nothing to read. */
  body.page-glass main>section.section--dark,
  body.page-glass main>section.page-hero{
    transition:border-color .45s cubic-bezier(.2,.7,.2,1),
               box-shadow .45s cubic-bezier(.2,.7,.2,1);
  }
  body.page-glass main>section.section--dark:hover,
  body.page-glass main>section.page-hero:hover{
    border-color:rgba(227,193,105,.78);
    box-shadow:
      0 2px 6px rgba(10,17,40,.10),
      0 12px 28px rgba(10,17,40,.14),
      0 40px 90px rgba(10,17,40,.30),
      0 0 34px rgba(227,193,105,.20),
      inset 0 1px 0 rgba(255,236,190,.34);
  }

  /* Cards repeat, so their light is smaller and cheaper: one image layer, no
     extra blur, and the same warm edge so a hovered card belongs to the same
     lighting as the panel behind it. .card--dark follows the navy rule. */
  .card:not(.card--dark){
    background-image:radial-gradient(22rem 16rem at var(--glow-x) var(--glow-y),
      rgba(255,246,222,calc(.55 * var(--glow))) 0%,
      rgba(255,240,205,calc(.20 * var(--glow))) 40%,
      rgba(255,238,198,0) 74%);
    transition:background-image .4s cubic-bezier(.2,.7,.2,1),
               border-color .4s cubic-bezier(.2,.7,.2,1),
               transform .35s cubic-bezier(.2,.7,.2,1),
               box-shadow .35s ease;
  }
  .card:not(.card--dark):hover{--glow:1;border-color:rgba(227,193,105,.7)}
  .card--dark:hover{
    border-color:rgba(227,193,105,.78);
    box-shadow:var(--shadow-2),0 0 26px rgba(227,193,105,.18);
  }

  /* The map plate is a photograph, not a surface: it gets the edge only. */
  .map-plate:hover{box-shadow:
    0 4px 12px rgba(10,17,40,.12),0 26px 56px rgba(10,17,40,.30),
    0 0 22px rgba(227,193,105,.22)}
}

/* 18.2.2 Touch and keyboard get the same welcome without the pointer.
   A tap cannot track a position, so the light arrives from above, which is
   where it comes from in the hero anyway. Keyboard focus inside a panel
   lights it too, so tabbing through the site is lit the same way pointing is. */
body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero):focus-within{
  --glow:.8;
  --glow-x:50%;
  --glow-y:0%;
}
.card:not(.card--dark):focus-within{--glow:.8;--glow-x:50%;--glow-y:0%}

/* 18.3 None of this is information. Under reduced motion the light stops
   moving and stops fading - the surfaces simply rest, which is also what they
   do for anyone whose browser never runs the pointer script. */
@media (prefers-reduced-motion:reduce){
  body.page-glass main>section:not(.hero),
  .card,.map-plate{
    transition:none!important;
    background-image:none!important;
    --glow:0;
  }
  body.page-glass main>section:not(.hero):hover,
  .card:hover,.map-plate:hover{box-shadow:none}
  body.page-glass main>section.section--dark:hover,
  body.page-glass main>section.page-hero:hover{box-shadow:
    0 2px 6px rgba(10,17,40,.10),0 12px 28px rgba(10,17,40,.14),0 40px 90px rgba(10,17,40,.30)}
}

@media print{
  body.page-glass main>section:not(.hero),.card,.map-plate{background-image:none!important}
}

/* ===== craft-2:halo ===== */
/* ==========================================================================
   19. The halo, and a repair.

   19.1 repairs section 18. The pointer glow set background-image on panels and
   on light cards, which REPLACED the material gradients the surface pass had
   built there - measured: a cream panel had four layers and not one of them was
   the 165deg light-fall. Light was arriving with no surface for it to fall on.
   The glow is now the top layer over that material rather than instead of it.

   19.2 is the halo Chris asked for: a light gold gradient turning slowly around
   every panel and card. Two techniques, because the two have different budgets.
   A card gets a real rim - a conic gradient masked down to the border itself,
   so the light travels around the edge. A panel gets a breathing bloom instead:
   there are far fewer of them, they are very large, and every one of them is
   already compositing a backdrop-filter, so a masked rim on a panel is paint
   the page cannot spare.

   Both are driven by registered custom properties, so they interpolate. Where
   @property is unsupported the halo still renders - it simply holds still,
   which is the correct thing to lose.
   ========================================================================== */

@property --halo-angle{
  syntax:"<angle>";
  inherits:false;
  initial-value:0deg;
}
@property --halo-lift{
  syntax:"<number>";
  inherits:false;
  initial-value:0;
}

/* 19.1 Repair: material first, then the light that falls on it. --mat-panel
   and --mat-card hold what the surface pass drew, so section 18 and anything
   after it can layer over the material by name instead of overwriting it. */
:root{
  --mat-panel:linear-gradient(165deg,
    rgba(255,255,255,.30) 0%,
    rgba(255,255,255,.10) 24%,
    rgba(255,255,255,0) 52%,
    rgba(22,38,77,.05) 100%);
  --mat-card:linear-gradient(172deg,var(--white) 0%,var(--cream) 74%,var(--cream-2) 100%);
}
@media (hover:hover) and (pointer:fine){
  body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero){
    background-image:
      radial-gradient(42rem 30rem at var(--glow-x) var(--glow-y),
        rgba(255,244,214,calc(.42 * var(--glow))) 0%,
        rgba(255,238,198,calc(.16 * var(--glow))) 38%,
        rgba(255,236,190,0) 72%),
      var(--mat-panel);
  }
  .card:not(.card--dark){
    background-image:
      radial-gradient(22rem 16rem at var(--glow-x) var(--glow-y),
        rgba(255,246,222,calc(.55 * var(--glow))) 0%,
        rgba(255,240,205,calc(.20 * var(--glow))) 40%,
        rgba(255,238,198,0) 74%),
      var(--mat-card);
  }
}

/* 19.2.1 The card rim. A conic gradient fills the element, then a two-layer
   mask subtracts the content box and leaves only the border - so what shows is
   a 1.5px ring of moving light and nothing over the text. inset:-1px sits it
   just outside the card's own border so the two do not fight for the same
   pixel, and border-radius:inherit keeps it honest on every corner radius the
   card takes at different breakpoints. */
.card{position:relative}
.card::after{
  content:"";position:absolute;inset:-1px;border-radius:inherit;
  padding:1.5px;pointer-events:none;
  background:conic-gradient(from var(--halo-angle),
    rgba(227,193,105,0)    0deg,
    rgba(227,193,105,.20)  38deg,
    rgba(243,234,216,.92)  76deg,
    rgba(255,248,224,1)    92deg,
    rgba(227,193,105,.85) 118deg,
    rgba(227,193,105,.16) 168deg,
    rgba(227,193,105,0)   232deg,
    rgba(227,193,105,0)   360deg);
  -webkit-mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
  mask:linear-gradient(#000 0 0) content-box,linear-gradient(#000 0 0);
  mask-composite:exclude;
  opacity:.5;
  animation:halo-turn 18s linear infinite;
}
/* On navy the same gold needs less of itself; on cream it needs a little more
   to survive the bright video behind the panel. */
.card--dark::after{opacity:.62}

/* A hovered card is already being lit by section 18, so its rim comes up to
   meet the light rather than staying at rest. */
@media (hover:hover) and (pointer:fine){
  .card::after{transition:opacity .4s cubic-bezier(.2,.7,.2,1)}
  .card:hover::after{opacity:.95;animation-duration:9s}
}

@keyframes halo-turn{to{--halo-angle:360deg}}

/* 19.2.2 The panel bloom. No pseudo-element is free on a panel - ::before is
   the masthead rule and ::after is the scroll-driven departure veil on desktop
   - and a panel is too large to paint a masked rim on anyway. So the halo is
   carried by the shadow stack: the existing seven layers, unchanged, plus one
   gold bloom whose spread and opacity breathe off --halo-lift.

   It runs only on .is-in, so a panel that has not been reached is not
   animating, and the whole thing stops at once under reduced motion. */
body.page-glass main>section:not(.hero){
  box-shadow:
    0 1px 2px rgba(10,17,40,.10),
    0 10px 24px rgba(10,17,40,.13),
    0 40px 88px rgba(10,17,40,.26),
    0 0 calc(16px + 26px * var(--halo-lift)) rgba(227,193,105,calc(.07 + .13 * var(--halo-lift))),
    inset 0 1px 0 var(--edge-lit),
    inset 1px 0 0 rgba(255,255,255,.22),
    inset -1px 0 0 rgba(10,17,40,.05),
    inset 0 -1px 0 var(--edge-shade);
}
html:not(.no-js) body.page-glass main>section.is-in{
  animation:halo-breathe 11s ease-in-out infinite;
}
/* the panels are staggered so the page does not pulse as one object */
html:not(.no-js) body.page-glass main>section.is-in:nth-of-type(2n){animation-delay:-3.7s}
html:not(.no-js) body.page-glass main>section.is-in:nth-of-type(3n){animation-delay:-7.3s}

@keyframes halo-breathe{
  0%,100%{--halo-lift:0}
  50%    {--halo-lift:1}
}

/* The dark panels carry gold on their edge already, so their bloom is the one
   that must stay quietest or the navy starts to look like it is glowing. */
body.page-glass main>section.section--dark,
body.page-glass main>section.page-hero{
  box-shadow:
    0 2px 6px rgba(10,17,40,.10),
    0 12px 28px rgba(10,17,40,.14),
    0 40px 90px rgba(10,17,40,.30),
    0 0 calc(14px + 18px * var(--halo-lift)) rgba(227,193,105,calc(.06 + .09 * var(--halo-lift))),
    inset 0 1px 0 rgba(255,255,255,.12);
}

/* 19.3 Touch gets the halo but not the hover response, and a tap must not
   leave a card lit for the rest of the session. */
@media (hover:none),(pointer:coarse){
  .card:hover::after{opacity:.5;animation-duration:18s}
}

/* 19.4 None of this is information, so all of it stops. A held halo still
   reads as a gold rim and a gold edge - the light simply stops travelling. */
@media (prefers-reduced-motion:reduce){
  .card::after{animation:none;--halo-angle:96deg;opacity:.42}
  html:not(.no-js) body.page-glass main>section.is-in{animation:none}
  body.page-glass main>section:not(.hero){--halo-lift:.45}
}

@media print{
  .card::after{display:none!important}
  body.page-glass main>section:not(.hero){--halo-lift:0}
}

/* ===== craft-2:timescard ===== */
/* ==========================================================================
   20. The times card: lower, and scrubbed to the scroll.

   It was sitting hard against the bottom of the hero, so the scroll cue and
   the card were competing for the same moment. Now the hero keeps the first
   screen to itself, the cue still marks the way down, and the card arrives on
   its own - panned up and settled by the scroll position rather than by a
   timer, so it tracks the wheel exactly and reverses if you scroll back.

   The existing reveal (14.8, a transition keyed on .is-in) still runs for
   everything else in the panel. For this one element it would fight the
   scroll-driven animation - two systems owning one transform - so 20.2 stands
   the transition down wherever the scroll timeline is actually driving.
   ========================================================================== */

/* 20.1 Room. The first panel on the home page starts below the fold rather
   than tucked under the hero's last line. --s9 is the existing 96px step, so
   this is the page's own spacing scale, not a new number. */
/* NOT :first-of-type - the hero IS the first section in main, so that selector
   spaced the hero instead of the card. The panel we mean is the one that
   follows the hero, and it is named that way. */
body.page-index main>section.hero + section{
  margin-top:clamp(var(--s7),9vh,calc(var(--s9) + var(--s6)))!important;
}
@media (max-height:700px){
  body.page-index main>section.hero + section{margin-top:var(--s6)!important}
}

/* 20.2 The pan. view() gives a timeline that runs as the element crosses the
   viewport, so the card is positioned by scroll offset instead of played on a
   trigger - scrub back up and it goes back. entry 5% to entry 70% means it is
   fully settled well before it reaches the middle of the screen, so nobody
   reads it mid-move.

   Guarded twice: @supports for browsers without scroll-driven animation, and
   no-preference so it never runs for someone who asked for stillness. Both
   fallbacks land on the 14.8 reveal that was already there. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    html:not(.no-js) body.page-glass main>section:not(.hero) .container>.infocard{
      /* anonymous view() rather than a named timeline: the element is its own
         subject here, so naming it buys nothing and only adds a scope that has
         to resolve. */
      /* longhand on purpose: `animation: <name> auto linear both` is thrown
         away by the shorthand parser, because `auto` is not a valid duration
         there. Written long, the duration is simply left to the timeline. */
      animation-name:times-pan;
      animation-timing-function:linear;
      animation-fill-mode:both;
      animation-timeline:view();
      animation-range:entry 5% entry 70%;
      /* the .is-in transition would be a second owner of this transform */
      opacity:1;
      transition:none;
    }
    @keyframes times-pan{
      from{
        opacity:0;
        transform:translate3d(0,52px,0) scale(.962);
        filter:blur(3px);
      }
      60%{
        opacity:1;
        filter:blur(0);
      }
      to{
        opacity:1;
        transform:none;
        filter:blur(0);
      }
    }

    /* The three cells arrive a beat after the plate they sit on, each one a
       little later than the last, so the card assembles rather than appearing.
       Same timeline, later range - no second observer, no JS. */
    html:not(.no-js) body.page-glass main>section:not(.hero) .container>.infocard .cell{
      animation-name:times-cell;
      animation-timing-function:linear;
      animation-fill-mode:both;
      animation-timeline:view();
    }
    html:not(.no-js) body.page-glass main>section:not(.hero) .container>.infocard .cell:nth-child(1){animation-range:entry 22% entry 74%}
    html:not(.no-js) body.page-glass main>section:not(.hero) .container>.infocard .cell:nth-child(2){animation-range:entry 30% entry 82%}
    html:not(.no-js) body.page-glass main>section:not(.hero) .container>.infocard .cell:nth-child(3){animation-range:entry 38% entry 90%}
    @keyframes times-cell{
      from{opacity:0;transform:translate3d(0,14px,0)}
      to  {opacity:1;transform:none}
    }
  }
}

/* 20.3 Nothing above may leave the card invisible. If the timeline never
   attaches - no support, reduced motion, or a browser that parses view() but
   does not run it - these are the values that hold. */
@media (prefers-reduced-motion:reduce){
  html:not(.no-js) body.page-glass main>section .container>.infocard,
  html:not(.no-js) body.page-glass .infocard .cell{
    animation:none;opacity:1;transform:none;filter:none;
  }
}
@media print{
  html:not(.no-js) body.page-glass main>section .container>.infocard,
  html:not(.no-js) body.page-glass .infocard .cell{
    animation:none;opacity:1;transform:none;filter:none;
  }
  body.page-index main>section:first-of-type{margin-top:0}
}

/* 20.4 The hero has to actually reach the fold, or the card shows through the
   gap underneath it. Section 7 caps the hero at min(92vh,860px), so on a tall
   desktop window - 1265px, say - the hero stops at 860 and leaves roughly 400px
   of screen below it. Adding margin to the CARD could never fix that: the space
   was already there, above it. So the home hero fills the viewport, and the card
   starts after a full screen plus the 20.1 gap.

   svh rather than vh: on a phone, vh is the LARGE viewport (URL bar retracted),
   so a vh hero is taller than the screen actually is on load and the scroll cue
   sits below the fold. svh is the small viewport, which is what is really there.
   The short-screen rule stays: a laptop in landscape at 700px or less keeps the
   compact hero from section 7 rather than a full screen of it. */
body.page-index .hero{min-height:100vh}
@supports (min-height:100svh){
  body.page-index .hero{min-height:100svh}
}
@media (max-height:700px){
  body.page-index .hero{min-height:0}
}
@media print{
  body.page-index .hero{min-height:0}
}

/* ===== craft-2:pass ===== */
/* ==========================================================================
   21. Panels breathe with the scroll.

   The ask: more transparency, and something happening per panel as you scroll.
   The constraint that shapes the whole block: transparency is paid for in
   contrast, and these panels carry the body copy over a bright sky. A flat
   drop from .36 to .20 would look wonderful and be slightly harder to read all
   day.

   So the transparency is SCROLL-DEPENDENT rather than lower everywhere. A panel
   is at its most transparent while it is entering and leaving - when it is at
   the edge of the screen and nobody is reading it - and settles to its full
   readable value through the middle third, which is where reading happens. The
   glass opens as it arrives and closes as it goes.

   17.3 wrote the glass as finished rgba() tokens, which cannot be animated a
   channel at a time, so 21.1 restates them as an RGB triplet plus an alpha the
   keyframes can drive. Every state 17.3 defined is re-asserted: no video means
   solid, reduced motion means solid.
   ========================================================================== */

@property --panel-a{
  syntax:"<number>";
  inherits:false;
  initial-value:.36;
}
@property --panel-a-navy{
  syntax:"<number>";
  inherits:false;
  initial-value:.48;
}

/* 21.1 The glass, restated so its alpha is a number rather than baked in. At
   rest these resolve to exactly the values 17.3 shipped. */
body.page-glass{
  --glass-cream-rgb:250,247,242;
  --glass-alt-rgb:243,234,216;
  --glass-navy-rgb:22,38,77;
  --panel-a:.36;
  --panel-a-navy:.48;
}
body.page-glass main>section:not(.hero){
  background-color:rgba(var(--glass-cream-rgb),var(--panel-a));
}
body.page-glass main>section.section--alt{
  background-color:rgba(var(--glass-alt-rgb),var(--panel-a));
}
body.page-glass main>section.section--dark,
body.page-glass main>section.page-hero{
  background-color:rgba(var(--glass-navy-rgb),var(--panel-a-navy));
}

/* At alpha 1 the triplets ARE --cream, --cream-2 and --navy-2, so these two
   states land on exactly the solid colours 17.3 chose for them. */
body.page-glass.no-video{--panel-a:1;--panel-a-navy:1}
@media (prefers-reduced-motion:reduce){
  body.page-glass{--panel-a:1;--panel-a-navy:1}
}

/* 21.2 The pass. cover 0% to cover 100% is the panel's whole journey across the
   viewport, so 0% is the moment its top edge touches the bottom of the screen
   and 100% is the moment its bottom edge leaves the top.

   The middle - 32% to 68% - is held flat on purpose. That is the panel sitting
   in the reading position, and nothing about it moves or changes there. All of
   the effect lives in the approach and the departure.

   Two animations on one element, so they are declared as one list: the gold
   bloom from 19.2.2 keeps its own clock, the pass runs on the scroll. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    html:not(.no-js) body.page-glass:not(.no-video) main>section:not(.hero){
      animation-name:halo-breathe,panel-pass;
      animation-duration:11s,auto;
      animation-timing-function:ease-in-out,linear;
      animation-iteration-count:infinite,1;
      animation-fill-mode:none,both;
      animation-timeline:auto,view();
      animation-range:normal,cover 0% cover 100%;
    }
    @keyframes panel-pass{
      0%{
        --panel-a:.13;
        --panel-a-navy:.24;
        -webkit-backdrop-filter:blur(34px) saturate(1.3);
        backdrop-filter:blur(34px) saturate(1.3);
      }
      32%{
        --panel-a:.36;
        --panel-a-navy:.48;
        -webkit-backdrop-filter:blur(24px) saturate(1.15);
        backdrop-filter:blur(24px) saturate(1.15);
      }
      68%{
        --panel-a:.36;
        --panel-a-navy:.48;
        -webkit-backdrop-filter:blur(24px) saturate(1.15);
        backdrop-filter:blur(24px) saturate(1.15);
      }
      100%{
        --panel-a:.15;
        --panel-a-navy:.26;
        -webkit-backdrop-filter:blur(30px) saturate(1.26);
        backdrop-filter:blur(30px) saturate(1.26);
      }
    }

    /* 21.3 The cards do NOT get the transparency - they are the reading
       surface inside the panel, and a card you can see the sky through is a
       card you cannot read. They get the other half of the effect instead: a
       small drift and a shadow that deepens as the card reaches the middle of
       the screen, so a column of cards passes rather than sits. */
    html:not(.no-js) body.page-glass main>section:not(.hero) .card{
      animation-name:card-pass;
      animation-timing-function:linear;
      animation-fill-mode:both;
      animation-timeline:view();
      animation-range:cover 4% cover 96%;
    }
    @keyframes card-pass{
      0%  {transform:translate3d(0,16px,0) scale(.988)}
      34% {transform:none}
      66% {transform:none}
      100%{transform:translate3d(0,-12px,0) scale(.992)}
    }
    /* a hovered or pressed card belongs to the pointer, not to the scroll */
    @media (hover:hover) and (pointer:fine){
      html:not(.no-js) body.page-glass main>section:not(.hero) .card:hover,
      html:not(.no-js) body.page-glass main>section:not(.hero) .card:active{
        animation-name:none;
      }
    }
  }
}

/* 21.4 Everything above is decoration over a video. With no video there is
   nothing to see through, so the glass stays shut and the cards stay still. */
body.page-glass.no-video main>section:not(.hero),
body.page-glass.no-video main>section:not(.hero) .card{
  animation-name:none!important;
}

@media (prefers-reduced-motion:reduce){
  html:not(.no-js) body.page-glass main>section:not(.hero),
  html:not(.no-js) body.page-glass main>section:not(.hero) .card{
    animation-name:none!important;
    transform:none!important;
  }
}
@media print{
  body.page-glass main>section:not(.hero),
  body.page-glass main>section:not(.hero) .card{
    animation-name:none!important;transform:none!important;
  }
  body.page-glass{--panel-a:1;--panel-a-navy:1}
}

/* ===== craft-2:glass ===== */
/* ===== craft-2:glass ===== */
/* ==========================================================================
   22. The glass palette over a bright video.

   The complaint, measured: the navy panel at its settled .48 over the clip's
   brightest frame composites to rgb(135,125,116) - hue 36 degrees, HSV
   saturation 14 percent. That is warm grey, and cream body copy on it is
   3.58:1. At the scroll-entry .24 of section 21.2 it is rgb(190,164,129) and
   2.11:1. The cream panel is fine over a bright frame (11.09:1) and fails over
   a mid frame at the entry alpha (4.00:1). The header bar is 3.48:1 on every
   page. Those four are real and are what this block repairs.

   WHICH FUNCTION DOES WHICH JOB. It matters, because the first draft of this
   block got it backwards and paid for it in transparency.

   HUE is fixed by saturate(), not by contrast(). The brightest frame's
   red-minus-blue skew is +75; the navy tint's blue-minus-red is +55. After
   saturate(30 percent) the backdrop skew is 22.5, and at alpha .48 the tint
   contributes .48 x 55 = 26.4 against .52 x 22.5 = 11.7. Blue wins on its own:
   saturate alone lands the settled panel at rgb(123,126,138), hue 228. The
   panel is navy at that point and still unreadable, because its LUMINANCE is
   wrong, not its hue.

   LUMINANCE is fixed by brightness(). And the whole cost of this block is paid
   there, because how much of the clip survives a panel is exactly
   contrast x brightness x (1 - alpha). Any darkening contrast() does is
   darkening brightness() could have done with a LARGER surviving range, so on
   the navy panels contrast is left near 1 and brightness does the placing.
   Cream is the exception and keeps a real contrast curve: a cream panel has to
   stay light over a DARK frame as well as over a bright one, and only
   contrast() compresses both ends toward the middle.

   Nothing here raises a panel's alpha. But say the real number out loud: a
   conditioned backdrop is less transparent than its alpha suggests. Effective
   opacity - the fraction of the clip's range the panel blocks - is
   0.79 settled and 0.81 at entry for navy (it is 0.48 today, at 3.58:1), and
   0.61 settled and 0.67 at entry for cream (0.36 today). This block buys
   legibility with transparency. It does not get both.
   ========================================================================== */

/* 22.1 One number for the whole pass. --pass is 0 while a panel is entering or
   leaving and 1 while it sits in the reading position, so each material writes
   its own curve off the same clock. Registered so it interpolates; the initial
   value is the settled end, which is also what a browser with no view()
   timeline gets and what the static tints are tuned for.
   inherits:false, to match --panel-a: the keyframes set it on the section
   itself and it must not leak into the section's children. */
@property --pass{
  syntax:"<number>";
  inherits:false;
  initial-value:1;
}

body.page-glass{
  /* Warm paper. The tint moves 3 points of red and 12 of blue off --cream so
     the panel still reads as the flyer's paper once the backdrop under it has
     been desaturated. The solid states below put --cream back, exactly. */
  --glass-cream-rgb:253,246,230;
  /* The bar and the foot are the brand's deepest navy and only their alpha
     moves: .42 to .46 at rest, .72 to .70 lit, .45 to .48 under the footer. */
  --glass-bar:rgba(10,17,40,.46);
  --glass-bar-lit:rgba(10,17,40,.70);
  --glass-foot:rgba(10,17,40,.48);
  /* Gold, lit. On the settled navy panel the brand gold measures 4.61:1 INSIDE
     14.15.4's six-percent top-left highlight - which is exactly where an
     eyebrow sits - so the brand gold now clears 4.5:1 on its own. This token
     exists for margin, not for rescue: it holds 5.33:1 in that highlight and
     still clears 4.5:1 on a blown-out frame the brief does not stipulate,
     where plain gold falls to 4.17:1. Buttons, rules, borders and the map chip
     keep --gold; this is for small text on dark glass only. */
  --gold-lit:#edd08a;
  /* Ink. A translucent cream panel is never as light as solid --cream, so the
     secondary ink comes down to meet it. #4a5877 on a settled cream panel over
     a mid frame measures 2.81:1. This is the same navy hue at 221 degrees, and
     it holds 5.67:1 there and 8.25:1 over a bright frame.
     Residual, stated rather than hidden: over a genuinely DARK frame the
     settled cream panel is rgb(156,152,145) and this ink is 3.93:1. The brief
     stipulates bright and mid backdrops and 14.12 records the clip as pale
     sky, so it is left here. If the clip ever carries dark frames the ink has
     to go to #273049 (4.54:1 there), at the cost of nearly all the separation
     from --text. */
  --text-muted:#2e3a55;
}

/* 22.2 Cream panels - warm paper.
   saturate 54 percent keeps a little of the sky's warmth so the paper is warm
   rather than grey. contrast runs 26 percent at the ends to 50 percent in the
   reading position, so the panel arrives as a flat frosted sheet and then
   CLEARS. brightness runs 145 percent down to 122 percent, which stops a
   13-percent tint from going dim over the darker parts of the clip - and it
   never clips, because contrast() has already pulled the sky to about 175
   before the lift is applied.

   Composited, over rgb(235,205,160):
     entry, --panel-a .13   - panel rgb(225,219,209), hue 37, saturation 7
     settled, --panel-a .36 - panel rgb(228,219,204), hue 38, saturation 11
   over rgb(135,111,94):
     entry rgb(193,188,183), settled rgb(190,182,173).
   Body copy #16264d on those four: 10.73, 10.78, 7.86, 7.41.
   Secondary --text-muted: 8.22, 8.25, 6.02, 5.67.
   --glass-alt-rgb is left at 243,234,216 and was measured too: the .section--alt
   panels land at rgb(224,217,207) and rgb(224,215,199), 10.57:1 and 10.35:1. */
body.page-glass main>section:not(.hero){
  -webkit-backdrop-filter:
    blur(calc(32px - 10px * var(--pass)))
    saturate(54%)
    contrast(calc(26% + 24% * var(--pass)))
    brightness(calc(145% - 23% * var(--pass)));
  backdrop-filter:
    blur(calc(32px - 10px * var(--pass)))
    saturate(54%)
    contrast(calc(26% + 24% * var(--pass)))
    brightness(calc(145% - 23% * var(--pass)));
  /* Paper needs an edge. A white hairline at .28 disappears against a bright
     sky now that the panel itself is bright; a warm one reads as the cut edge
     of a sheet and ties the panel to the gold rule along its top. */
  border-color:rgba(163,126,58,.30);
}

/* 22.3 Navy panels, and the inner-page title band, which is the same material.
   saturate 30 percent does the hue. contrast 70 to 96 percent is close enough
   to 1 that it is barely conditioning at all - it exists only to keep the
   entry state, where the tint is weakest, from going neutral. brightness 36 to
   42 percent does the placing.

   Every number below is measured INSIDE 14.15.4's top-left light, which paints
   rgba(227,193,105,.05) and then rgba(255,255,255,.06) over the composited
   surface. That is where the eyebrow and the verse reference sit, it is the
   worst point on the panel, and the first draft of this block measured the
   flat surface instead and shipped a 4.26:1 eyebrow.

   Over rgb(235,205,160):
     entry, --panel-a-navy .24 - flat rgb(57,59,66) hue 226; lit rgb(77,77,79)
     settled, .48              - flat rgb(57,63,79) hue 224; lit rgb(77,80,91)
   In that lit corner, settled: --ink-on-dark 7.09, --muted-1 5.65,
   --verse-on-dark 5.33, --gold 4.61, --gold-lit 5.33. On the flat surface the
   cream text is 9.34:1. Over rgb(135,111,94) the lit corner is 9.32:1 cream,
   6.06:1 gold. The page-hero band's own light (white .05 then gold .07) is
   within 0.03 of these.
   --muted-2 is the one colour that does NOT clear 4.5:1 in that corner - it is
   4.17:1 - so .small moves to --muted-1 below rather than the panel moving
   again for one 15px line. */
body.page-glass main>section.section--dark,
body.page-glass main>section.page-hero{
  -webkit-backdrop-filter:
    blur(calc(32px - 10px * var(--pass)))
    saturate(30%)
    contrast(calc(70% + 26% * var(--pass)))
    brightness(calc(36% + 6% * var(--pass)));
  backdrop-filter:
    blur(calc(32px - 10px * var(--pass)))
    saturate(30%)
    contrast(calc(70% + 26% * var(--pass)))
    brightness(calc(36% + 6% * var(--pass)));
  border-color:rgba(227,193,105,.34);
}

/* Small text on dark glass. Everything that is a shape rather than a
   letterform - rules, borders, buttons, the map chip - keeps the brand gold. */
body.page-glass main>section.section--dark .eyebrow,
body.page-glass main>section.page-hero .eyebrow,
body.page-glass main>section.section--dark .verse-ref,
body.page-glass main>section.section--dark .map-credit{
  color:var(--gold-lit);
}
/* 15px --muted-2 is 4.17:1 in the lit corner; --muted-1 is 5.65:1 and is the
   colour the leads in the same panels already use. */
body.page-glass main>section.section--dark .small,
body.page-glass main>section.page-hero .small{
  color:var(--muted-1);
}

/* 22.4 The pass, restated. Section 21.2 baked the backdrop filter into the
   keyframes, which cannot work now that the two materials want opposite
   brightness curves. The alpha steps are IDENTICAL to the ones 21.2 shipped -
   .13 / .36 / .36 / .15 and .24 / .48 / .48 / .26 - and the only addition is
   --pass, from which 22.2 and 22.3 derive their filters.

   The animation is given a NEW NAME and animation-name is re-declared, rather
   than relying on a second @keyframes panel-pass shadowing the first. Same-name
   replacement does work, but it is invisible at a glance and it is 100 lines
   away in a 3600-line file: any future reordering would silently restore
   21.2's backdrop-filter keyframes, and an animated backdrop-filter beats
   everything 22.2 and 22.3 declare. Only animation-name is overridden; the
   duration, timeline and range lists in 21.2 still apply, position for
   position. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    html:not(.no-js) body.page-glass:not(.no-video) main>section:not(.hero){
      animation-name:halo-breathe,panel-pass-lit;
    }
    @keyframes panel-pass-lit{
      0%  {--panel-a:.13;--panel-a-navy:.24;--pass:0}
      32% {--panel-a:.36;--panel-a-navy:.48;--pass:1}
      68% {--panel-a:.36;--panel-a-navy:.48;--pass:1}
      100%{--panel-a:.15;--panel-a-navy:.26;--pass:.1}
    }
  }
}

/* 22.5 Header. The bar had the same defect and no scroll animation to hide
   behind: over a bright frame it composited to rgb(144,126,104) and the
   navigation links measured 3.48:1, a failure on every page. Same division of
   labour - saturate for hue, brightness for placing, contrast left at 1 so the
   bar keeps as much of the clip as a readable bar can.
   At rest over rgb(235,205,160): bar rgb(63,64,70), hue 236.
   --ink-on-dark 9.17:1, --muted-2 on the brand sub-line 5.40:1, --gold on the
   language toggle 5.96:1. Scrolled, at .70, the bar is rgb(40,43,57) and
   nothing on it is under 7.3:1.
   Because the bar is sticky it also passes over the panels: over a settled
   cream panel it composites to rgb(65,67,76), so it is the same navy over the
   clip and over the content, which it was not before. */
body.page-glass .site-header{
  -webkit-backdrop-filter:blur(18px) saturate(34%) contrast(100%) brightness(50%);
  backdrop-filter:blur(18px) saturate(34%) contrast(100%) brightness(50%);
}

/* 22.6 Footer. The shade is already heaviest here, so it needs the least
   correction. Over rgb(235,205,160): foot rgb(59,60,67), hue 234.
   --ink-on-dark 9.74:1, --muted-1 7.77:1, --muted-2 in the closing line
   5.73:1, --gold 6.33:1. */
body.page-glass .site-footer{
  -webkit-backdrop-filter:blur(16px) saturate(34%) contrast(100%) brightness(48%);
  backdrop-filter:blur(16px) saturate(34%) contrast(100%) brightness(48%);
}

/* 22.7 Two things the first draft tried to fix that are not broken, recorded
   so nobody tries again.

   THE MAP PLATE is not glass. Its background is --navy-3 completely covered by
   an opaque JPEG, inside an opaque .card, on both index.html and contacto.html.
   It never composited the video and never washed out, its fallback is already
   navy, and its chip measures 10.8:1. It gets nothing.

   THE INFOCARD is the only one on the site and it lives inside
   section.section--dark on index.html, not on a cream panel. On the navy panel
   above it composites to rgb(31,38,57) at the --card-dark .55 it already has:
   the gold .lab is 8.70:1, the .val 13.38:1, the .sub 7.87:1. Raising it to
   .80 would buy 1.2 points of contrast nobody needs and would put an
   80-percent plate over the one part of the panel where the clip is most
   visible. It keeps .55.

   THE SCROLL CUE has never had a background - it is a gold word and a hairline
   over the clip, with a text-shadow, marked aria-hidden. Its gold is 1.85:1
   against the brightest sky, which is a visibility question and not a WCAG
   one, and the answer is a deeper shadow, not a navy plate: .scroll-cue is a
   two-item flex column with no padding and no radius, so a background on it
   paints a hard rectangle tight around the word in the middle of the hero. */
body.page-glass .scroll-cue .scroll-cue-word{
  text-shadow:0 1px 2px rgba(10,17,40,.92),0 0 14px rgba(10,17,40,.75);
}

/* 22.8 The three states where there is no video to condition.
   17.3 and 21.1 already switch the tints to solid for .no-video and for
   reduced motion, and 17.5 already strips the filter for print. Four things
   have to be added.
   First, --glass-cream-rgb goes back to --cream exactly, so 21.1's promise -
   that at alpha 1 the triplets ARE the brand colours - stays true now that the
   glass tint is warmer than the paint.
   Second, --glass-bar, --glass-bar-lit and --glass-foot have to be restored BY
   HAND under reduced motion. 17.3 sets them there on `body.page-glass`, which
   is the same specificity as 22.1's rule and LOSES to it on order: without
   these three lines a reader who asked for no motion gets a .46 bar with no
   video behind it. (Under .no-video the same tokens are safe, because 17.3
   writes them on `body.page-glass.no-video`, which outranks 22.1. They are
   repeated anyway so the two states read the same.)
   Third, the backdrop filter is switched off outright: behind a fully opaque
   panel it is a filter pass per frame that nobody can see.
   Fourth, --text-muted and the two dark-glass text colours go back, so a solid
   panel keeps the ink it was designed with. */
body.page-glass.no-video,
body.page-glass.no-video main>section:not(.hero){
  --glass-cream-rgb:250,247,242;
  --text-muted:#4a5877;
}
body.page-glass.no-video{
  --glass-bar:rgba(10,17,40,.96);
  --glass-bar-lit:rgba(10,17,40,.98);
  --glass-foot:var(--navy-3);
}
body.page-glass.no-video main>section:not(.hero),
body.page-glass.no-video main>section.section--dark,
body.page-glass.no-video main>section.page-hero,
body.page-glass.no-video .site-header,
body.page-glass.no-video .site-footer{
  -webkit-backdrop-filter:none;backdrop-filter:none;
}
body.page-glass.no-video main>section.section--dark .eyebrow,
body.page-glass.no-video main>section.page-hero .eyebrow,
body.page-glass.no-video main>section.section--dark .verse-ref,
body.page-glass.no-video main>section.section--dark .map-credit{color:var(--gold)}
body.page-glass.no-video main>section.section--dark .small,
body.page-glass.no-video main>section.page-hero .small{color:var(--muted-2)}

@media (prefers-reduced-motion:reduce){
  body.page-glass{
    --glass-cream-rgb:250,247,242;
    --text-muted:#4a5877;
    --glass-bar:rgba(10,17,40,.96);
    --glass-bar-lit:rgba(10,17,40,.98);
    --glass-foot:var(--navy-3);
  }
  body.page-glass main>section:not(.hero),
  body.page-glass main>section.section--dark,
  body.page-glass main>section.page-hero,
  body.page-glass .site-header,
  body.page-glass .site-footer{
    -webkit-backdrop-filter:none!important;backdrop-filter:none!important;
  }
  body.page-glass main>section.section--dark .eyebrow,
  body.page-glass main>section.page-hero .eyebrow,
  body.page-glass main>section.section--dark .verse-ref,
  body.page-glass main>section.section--dark .map-credit{color:var(--gold)}
  body.page-glass main>section.section--dark .small,
  body.page-glass main>section.page-hero .small{color:var(--muted-2)}
}

/* 22.9 Paper. 17.5 already forces every panel white and strips the filters,
   but it stops at the section: a child that declares its own colour does not
   inherit the section's, and !important on the section cannot reach it. So on
   paper today the infocard prints #f5f1e6 values and #e3c169 labels on a plate
   most browsers do not print at all, and the dark panels' .lead and .small
   print --muted-1 and --muted-2 on white. Every one of those is named here.
   Do NOT paint the infocard white without also naming its children - that
   turns a plate that at least printed as a dark block into cream text on white
   paper. */
@media print{
  body.page-glass{
    --glass-cream-rgb:250,247,242;
    --text-muted:#4a5877;
  }
  body.page-glass main>section:not(.hero),
  body.page-glass main>section.section--dark,
  body.page-glass main>section.page-hero,
  body.page-glass .site-header,
  body.page-glass .site-footer{
    -webkit-backdrop-filter:none!important;backdrop-filter:none!important;
  }
  body.page-glass .infocard{
    background:#fff!important;border-color:#999!important;
  }
  body.page-glass .infocard .lab,
  body.page-glass .infocard .val,
  body.page-glass .infocard .val a,
  body.page-glass .infocard .sub,
  body.page-glass main>section.section--dark .eyebrow,
  body.page-glass main>section.page-hero .eyebrow,
  body.page-glass main>section.section--dark .verse-ref,
  body.page-glass main>section.section--dark .verse,
  body.page-glass main>section.section--dark .lead,
  body.page-glass main>section.page-hero .page-hero-sub,
  body.page-glass main>section.section--dark .small,
  body.page-glass main>section.section--dark .map-credit{
    color:var(--text)!important;
  }
}

/* ===== craft-2:cards ===== */
/* ===== craft-3:cards ===== */
/* ==========================================================================
   22. The cards, brought up to date.

   The brief: modernise the cards without losing the warm-paper identity, and
   without making them transparent - section 21 left them opaque on purpose,
   because they carry the reading text over a moving video. Nothing here
   changes a panel alpha: --panel-a still runs .13 entering, .36 settled, .15
   leaving, and --panel-a-navy .24 / .48 / .26, exactly as 21.1 shipped them.

   Three things date a card, and this site has all three:

   1. UNIFORM PADDING AND A UNIFORM RHYTHM. Every card is a 24px box and every
      child is one interval from the next, so an eyebrow, a title, a date and
      two paragraphs read as one undifferentiated column. Grouping is what
      makes a card scannable, and grouping is made of unequal gaps.

   2. A FLAT FILL. A single top-to-bottom white-to-cream ramp across the whole
      face is the 2016 "subtle gradient". Light in 2026 comes from a place:
      a corner, with the body of the card holding one stable paper colour so
      the text sits on a constant ground.

   3. A GREY 1PX BORDER. This one is not a matter of taste, it is measured.
      The card's border today is --paper-line #e6dcc6, relative luminance
      .7209. A cream panel at the settled .36 over a bright frame of the clip
      lands at .7354. Edge against ground: 1.019:1 - the border is INVISIBLE
      exactly where the card most needs an edge.

   HONESTY ABOUT THE BACKDROP. The frame colours used below (bright
   235,205,160 and mid 135,111,94) are ASSUMED, not sampled - nobody measured
   the clip. What IS in the CSS and was missing from the first draft of this
   arithmetic is .page-shade: a fixed navy veil over the video at a minimum of
   about .07 and rising past .38 toward the foot of the page. Folding it in
   moves the worst-case bright panel from L .7354 to about .673, i.e. every
   "worst case" below is pessimistic by design. The panel also carries
   backdrop-filter:blur(24px), which averages the clip and removes the
   extremes outright. These numbers are a floor, not a reading.

   The fix for (3) forces a colour decision worth stating plainly, because it
   looks like a fourth brand colour and is not. A dark hairline is the only
   kind that survives a bright golden backdrop. But navy #16264d and cream
   #f3ead8 sit almost opposite each other, so navy at hairline alphas over
   cream composites to rgb(168,167,169) - DEAD NEUTRAL GREY, which is the very
   thing being fixed. So the card's shade term is warm: --card-ink 56,42,24,
   the shade cream takes under a golden sky. Navy and gold remain the only
   COLOURS on this site. This is a shadow, and shadows are not identity.

   Everything below is additive and token-driven. It changes no element, adds
   no animation and no backdrop-filter, and leaves every transform in sections
   14.6, 16.2, 16.6 and 21.3 exactly where it is.
   ========================================================================== */

/* 22.1 The system. Padding, radius, edge and shade as tokens, so the action
   strip that bleeds to the card's inner edge (15.2) and the nested radii can
   be derived from the same numbers instead of re-guessing var(--s5).       */
:root{
  --card-ink:56,42,24;                  /* the warm shade term, see above   */
  --card-paper:#fdfbf6;                 /* one stable ground for body copy  */

  --card-px:clamp(20px,1.1vw + 15px,26px);
  --card-pt:calc(var(--card-px) * .88); /* optical, see 22.2                */
  --card-pb:calc(var(--card-px) * 1.08);

  --card-r:clamp(14px,.9vw + 10px,18px);
  --card-r-in:calc(var(--card-r) - 1px);

  --card-edge-a:.16;                    /* sides; raised in 22.11           */

  --card-gap:var(--s4);                 /* the body interval                */
  --card-gap-label:6px;                 /* eyebrow to title, one unit       */
  --card-gap-meta:8px;                  /* title to its own date line       */

  /* Light now arrives from the upper-left CORNER and is spent by 62 percent;
     the body of the card is one flat paper; the foot warms into the cream
     the flyers use. 19.1 layers the pointer glow OVER this token inside its
     own (hover:hover) block at .card:not(.card--dark) - a HIGHER specificity
     than 22.3 below - so the glow survives this section untouched.        */
  --mat-card:
    radial-gradient(150% 120% at 6% -14%,
      rgba(255,255,255,.95) 0%,
      rgba(255,255,255,.34) 34%,
      rgba(255,255,255,0) 62%),
    linear-gradient(180deg,
      rgba(227,193,105,0) 46%,
      rgba(227,193,105,.13) 100%);

  /* 14.15.7's navy material, lifted to a token for one reason: section 22
     sets the light fill on bare .card, which every .card--dark also matches
     at equal specificity. Source order alone decided that before, and source
     order is not a guarantee. 22.9 restates the navy ground by name so a
     dark card can never inherit paper.                                    */
  --mat-card-dark:
    radial-gradient(120% 130% at 20% 0%,
      rgba(255,255,255,.08),rgba(255,255,255,0) 52%),
    linear-gradient(176deg,rgba(34,58,110,.45) 0%,rgba(10,17,40,.30) 100%);
}

/* 22.2 Geometry. Radius tracks the card's own size and stays inside the
   panel's. The panel is 24px (18px under 640px) - the 20px in section 13 is
   overwritten twice further down the file - and the card lands at 18px
   (about 15px at 640px), so the two are concentric rather than
   coincidentally different.

   The padding is deliberately NOT one number. The first child of almost every
   card is an uppercase eyebrow - cap height only, no descenders - which
   leaves roughly a tenth of its line box as optical air at the top, so the
   MEASURED top padding is smaller to read equal. The foot has a final
   baseline and descenders and reads tight at the same measurement, so it is
   larger. Uniform padding is the single most common tell of a dated card;
   this is the fix, and it is 3px, not a redesign.                          */
.card{
  border-radius:var(--card-r);
  padding:var(--card-pt) var(--card-px) var(--card-pb);
}
/* Band cards are page furniture the width of the panel, not rows. The max()
   floors them at the 32px they have today: the bare ratio would have taken a
   full-width band DOWN to about 25px on a phone, which is a regression the
   ratio was never meant to buy.                                            */
.card--band{
  padding:max(var(--s6),calc(var(--card-pt) * 1.4))
          max(var(--s6),calc(var(--card-px) * 1.35))
          max(var(--s6),calc(var(--card-pb) * 1.35));
}

/* 22.3 The fill. background-color carries the reading ground and the image
   layers carry the light, so the two never fight. Paper is a shade brighter
   than --cream, which is also the solid colour the panel falls back to when
   the clip is not painting - a card must never be the same value as the
   surface it is lying on.

   Kept at bare .card specificity ON PURPOSE: 19.1's hover-path rule is
   .card:not(.card--dark), which outranks this and keeps its glow layer.
   The dark card is taken back in 22.9, below.                              */
.card{
  background-color:var(--card-paper);
  background-image:var(--mat-card);
}

/* 22.4 The edge, asymmetric. A card lit from above has a bright top edge, an
   almost absent side, and a genuinely dark underside. Today all four sides
   are the same beige line, which is why the card reads as an outline drawn on
   the panel rather than an object resting on it.

   Border WIDTH never changes anywhere in this block - only colour - so no
   card ever reflows, at any state or breakpoint.

   The hierarchy this buys is the point of the whole section: the PANEL's edge
   is gold, which is light, and the CARD's edge is shade. For that to be true
   on screen and not only in the stylesheet, 22.13 has to deal with the
   travelling gold rim 19.2.1 paints over every card's border.               */
.card{
  border-color:rgba(var(--card-ink),var(--card-edge-a));
  border-top-color:rgba(255,253,246,.92);
  border-bottom-color:rgba(var(--card-ink),.48);
}

/* 22.5 The shadow. Three layers instead of one soft mid-blur: a contact
   shadow that says the card is touching something, a short near-ambient, and
   a long far-ambient pulled in hard so it does not bleed sideways. That last
   part matters more here than on a normal page - the shadow falls on
   TRANSLUCENT glass, and a wide soft shadow over a .36 alpha panel darkens
   the video through it and makes the glass look dirty.

   The contact layer carries NO negative spread. It did in the first draft,
   and `0 1px 2px -1px` puts the shadow's own bottom edge exactly at the
   card's bottom edge, so the opaque card covers every pixel above 50% of the
   blur and barely a tenth of the stated alpha ever reaches the screen. A 1px
   offset with a 2px blur is already tight; the spread only made it invisible.
   Measured honestly at .42 with no spread: about .26 effective in the first
   pixel, which is 1.62:1 bright-settled and 1.48:1 at mid.

   --mat-rest keeps its name from 14.15.7 on purpose: 16.4 and 17.4 both
   restore the resting stack by that token, so they follow this change
   automatically and cannot drift from it.                                  */
.card{
  --mat-rest:
    0 1px 2px rgba(var(--card-ink),.42),
    0 5px 12px -6px rgba(10,17,40,.22),
    0 18px 36px -16px rgba(10,17,40,.26);
  --mat-hover:
    0 2px 4px rgba(var(--card-ink),.34),
    0 10px 22px -9px rgba(10,17,40,.24),
    0 30px 56px -22px rgba(10,17,40,.32);
  box-shadow:var(--mat-rest);
}

/* 22.6 Internal hierarchy. One interval becomes three, so the eye is told
   what belongs to what: the eyebrow and the title are ONE unit, the date line
   belongs to the title, and the body is a separate block. The rows are flex
   columns, so the tight pairs are bought with a negative margin against the
   gap rather than by rewriting the gap per child.                          */
.card h2,.card h3{
  margin-bottom:var(--card-gap);
  text-wrap:balance;                    /* no 5-word-then-1-word card title */
}
.card .eyebrow + h2,.card .eyebrow + h3{margin-top:var(--card-gap-label)}
.card p:not([class]),.card .lead{text-wrap:pretty}

/* Flex sibling margins ADD to the gap rather than collapsing with it, so each
   tight pair is bought by cancelling the gap on one side. Reading down an
   event row the intervals are now 6, 8, 16, 16 and then auto to the action
   strip - four values where there was one.                                 */
.event,.episode,.newsletter-item{gap:var(--card-gap)}
.event>.eyebrow,
.episode>.eyebrow,
.newsletter-item>time{margin-bottom:calc(var(--card-gap-label) - var(--card-gap))}
.event>.eyebrow + h3,
.episode>.eyebrow + h3,
.newsletter-item>time + h3{margin-top:0}
.event>h3,.episode>h3,.newsletter-item>h3{margin-bottom:0}
.event>h3 + .event-when,
.episode>h3 + .episode-meta{margin-top:calc(var(--card-gap-meta) - var(--card-gap))}

/* 22.7 The label chip. A filled pill with its own border around a date is a
   2019 tag. Dropping the border and the pill radius, and letting the gold
   read as a wash rather than as a fill, moves it from "component" to "label"
   - which is all it ever was. This is a MATERIAL change only: 15.1's size,
   weight, width, tracking and tabular numerals are all left exactly as they
   are, and so is 14.15.2's per-level heading tracking, which is a considered
   decision this section has no business overriding.

   Chip ground = gold .16 over paper = rgb(249,242,223), L .8880. Navy on it
   is 13.22:1 and --text-muted 6.35:1, so nothing is spent. (The first draft
   quoted L .8591 here, which was the wrong composite; the ratio it printed
   was right.)                                                              */
.events-list:not(.events-list--rows) .event>.eyebrow,
.episode>.eyebrow,
.newsletter-item>time{
  border:0;
  border-radius:7px;
  padding:5px 10px;
  background:rgba(227,193,105,.16);
}

/* 22.8 The action strip. It keeps its bleed to the inner edge of the border,
   restated against the padding tokens so the two can never disagree - 15.2
   hardcoded var(--s5), and 22.2 has just moved the padding off 24px, so
   without this the strip would hang 2px proud of the card at the wide end.
   Its bottom corners take the correct nested radius rather than a hardcoded
   one.

   Two changes of substance. The ground drops from solid --gold-tint to a wash
   at .10, so the card's own paper still shows through and the strip reads as
   part of the card instead of a yellow band pasted on the bottom. Strip =
   gold .10 over the card's foot = rgb(247,238,215), L .8616; navy on it is
   12.85:1. (The first draft quoted L .8204; the ratio was right.) And the
   rule above it fades out at both ends, which is 14.15's own stated doctrine
   - "every gold line fades along its length instead of drawing a box" - and
   was the one gold line in the file still drawn straight across.
   The border keeps its 1px and only loses its colour, so nothing reflows. */
.events-list:not(.events-list--rows) .event>.btn-row,
.event-next-card>.btn-row,
.episode>.links{
  margin:auto calc(-1 * var(--card-px)) calc(-1 * var(--card-pb));
  padding:var(--s4) var(--card-px);
  border-top-color:transparent;
  border-radius:0 0 var(--card-r-in) var(--card-r-in);
  background-color:rgba(227,193,105,.10);
  background-image:linear-gradient(90deg,
    rgba(227,193,105,0) 0%,
    rgba(227,193,105,.55) 18%,
    rgba(227,193,105,.55) 72%,
    rgba(227,193,105,0) 100%);
  background-size:100% 1px;
  background-position:left top;
  background-repeat:no-repeat;
  background-origin:border-box;
}
.event-next-card>.btn-row{background-color:var(--ink-wash)}

/* Media sits on its own plate inside the card, so it takes the concentric
   inner radius rather than the card's own.                                 */
.episode>audio{border-radius:calc(var(--card-r) - 8px)}

/* 22.9 The dark card and the times plate. Same geometry, same shadow
   language, opposite light: their top edge catches gold because gold is the
   only light that reaches navy in this building, and their underside goes
   darker still.

   The background pair is the CORRECTION that makes this section shippable.
   .card--dark also matches .card, at the same specificity, so 22.3's paper
   fill reaches it on source order alone: index.html's
   <div class="card card--dark card--band"> and site.js's "event card
   card--dark event-next-card" both rendered rgb(253,251,246) under
   --ink-on-dark #f5f1e6 - 1.09:1, white on white. Restating the navy ground
   and material here, after 22.3, is what stops that.

   Measured on the dark card as it then renders: --ink-on-dark 9.11:1 at the
   lit corner rgb(46,64,105) and 14.22:1 at the foot rgb(18,32,66), --muted-2
   5.36:1, --muted-1 7.26:1, the gold eyebrow 5.92:1.                       */
.card--dark{
  background-color:var(--navy-2);
  background-image:var(--mat-card-dark);
  --mat-rest:
    0 1px 2px rgba(0,0,0,.34),
    0 6px 14px -7px rgba(10,17,40,.34),
    0 22px 44px -18px rgba(10,17,40,.40),
    inset 0 1px 0 rgba(255,236,190,.14);
  --mat-hover:
    0 2px 5px rgba(0,0,0,.30),
    0 12px 26px -10px rgba(10,17,40,.36),
    0 34px 62px -24px rgba(10,17,40,.46),
    inset 0 1px 0 rgba(255,236,190,.20);
  box-shadow:var(--mat-rest);
  border-color:rgba(227,193,105,.24);
  border-top-color:var(--hair-gold);
  border-bottom-color:rgba(0,0,0,.34);
}
.infocard{
  border-radius:var(--card-r);
  box-shadow:
    0 1px 2px rgba(0,0,0,.30),
    0 6px 14px -7px rgba(10,17,40,.30),
    0 20px 40px -16px rgba(10,17,40,.36),
    inset 0 1px 0 rgba(227,193,105,.28);
}
/* Only the wide layout takes the new padding. Section 8 already gives the
   stacked phone layout var(--s3) both ways, and an unconditional rule here
   would have overwritten the min-width:700px padding from outside a media
   query - which worked, but only because of where it sits in the file.    */
@media (min-width:700px){
  .infocard{padding:var(--card-pt) var(--s2) var(--card-pb)}
}

/* 22.10 Hover, press and touch, restated in the tokens. Nothing here changes
   WHICH card answers a pointer - 16.3 and 16.6 still own that, and the 4px
   lift and the .995 press are untouched. These only make the material answer
   in the new language, at the specificities those blocks established, so a
   hovered or tapped card can never fall back to the old flat shadow or to the
   beige border.                                                            */
@media (hover:hover) and (pointer:fine){
  /* At rest the card's edge is shade. On hover it is LIGHT - 18.2's story is
     that a pointer brings light with it, so the sides and the top warm to
     gold while the underside stays in shadow, because the card is still lit
     from above.                                                           */
  .card:hover,
  .card:has(a[href],button:not([hidden])):hover{
    box-shadow:var(--mat-hover);
    border-color:rgba(227,193,105,.70);
    border-top-color:rgba(243,234,216,.95);
    border-bottom-color:rgba(var(--card-ink),.48);
  }
  .card--dark:hover,
  .card--dark:has(a[href],button:not([hidden])):hover{
    box-shadow:var(--mat-hover);
    border-color:rgba(227,193,105,.34);
    border-top-color:var(--gold);
    border-bottom-color:rgba(0,0,0,.34);
  }
  /* 17.4's rule restores the resting stack by token, so a card holding a form
     or a hovered map plate follows this section with no extra rule.        */
}
@media (hover:none),(pointer:coarse){
  .card:hover,
  .event.card:not(.card--dark):hover,
  .episode.card:hover,
  .newsletter-item.card:hover{
    box-shadow:var(--mat-rest);
    border-color:rgba(var(--card-ink),var(--card-edge-a));
    border-top-color:rgba(255,253,246,.92);
    border-bottom-color:rgba(var(--card-ink),.48);
  }
  .card--dark:hover,.event-next-card:hover{
    box-shadow:var(--mat-rest);
    border-color:rgba(227,193,105,.24);
    border-top-color:var(--hair-gold);
    border-bottom-color:rgba(0,0,0,.34);
  }
}

/* 22.11 The states that are not the happy path.

   no-video and reduced motion both resolve the panel to SOLID --cream, which
   is within .03 of the card's own paper - the card's face stops separating it
   from the panel altogether (1.03:1), and the whole job falls on the edge.
   So the side line buys the contrast it was borrowing from the moving clip.

   .38, not the .30 of the first draft, and the difference is 22.13: with the
   halo rim held at .26 over it, .30 painted only 1.50:1 on --cream and
   1.34:1 on --cream-2. At .38 the same painted edge is 1.70:1 and 1.52:1,
   which is what .30 was claimed to deliver before the rim was counted. The
   underside line reads 2.84:1 there and the contact shadow 1.66:1.

   Only the light card is listed. .card--dark's border is a fixed gold and
   never reads --card-edge-a, so naming it here would be dead code.
   Nothing in this block animates, so there is nothing for reduced motion to
   switch off beyond what section 13 already does with !important.          */
body.page-glass.no-video .card{--card-edge-a:.38}
@media (prefers-reduced-motion:reduce){
  .card{--card-edge-a:.38}
}

/* 22.12 Paper. 14.15.12 already strips the fill and the shadow from every
   card; what it never had to strip was a border that is now four different
   colours, a chip that is now a wash, and a background-COLOUR (14.15.12 only
   kills background-image). A yellow wash prints muddy and costs ink for
   nothing, and a .16 alpha hairline prints as almost nothing at all.

   #8a8a8a rather than the file's usual #999, for the one border that has to
   carry the whole card once fill and shadow are gone: #999 on white is
   2.85:1, under the 3:1 floor for a non-text boundary. The first draft
   claimed 3.75:1 for #999, which is wrong twice over - its luminance is
   .3185, not .3277, and .3277 would have given 2.78:1, not 3.75:1.
   #8a8a8a is 3.45:1. Navy text on white paper is 14.80:1.                  */
@media print{
  .card,.card--dark,.infocard{
    background-color:#fff!important;
    border-color:#8a8a8a!important;
    border-top-color:#8a8a8a!important;
    border-bottom-color:#8a8a8a!important;
    box-shadow:none!important;
  }
  .events-list:not(.events-list--rows) .event>.eyebrow,
  .episode>.eyebrow,
  .newsletter-item>time{
    background:none!important;
    border:1px solid #8a8a8a!important;
  }
  .events-list:not(.events-list--rows) .event>.btn-row,
  .event-next-card>.btn-row,
  .episode>.links{
    background-color:transparent!important;
    background-image:none!important;
    border-top-color:#8a8a8a!important;
  }
}

/* 22.13 The rim this section forgot it was arguing with.

   19.2.1 gives every .card an ::after at inset:-1px with padding:1.5px and
   opacity:.5 - a conic gold-to-cream ring masked down to the border box. It
   does not sit beside the card's border, it sits ON it, and its brightest
   stop is rgba(255,248,224,1). So a shade hairline underneath it is not what
   reaches the eye: at .5 the side edge measured 1.05:1 bare against a bright
   panel and 1.30:1 against solid --cream where 1.76:1 was claimed.

   Worse under reduced motion, where 19.4 parks --halo-angle at 96deg - four
   degrees off the single brightest stop - and holds it at .42. That is the
   exact state where 22.11 says the edge does the whole job.

   So on the light card the rim steps back to an accent and the border is
   allowed to be the edge. The dark card keeps the rim as it is: gold on navy
   is the intended language there and there is no shade hairline to erase.
   The travel, the timing and the hover response are all left to 19.         */
.card:not(.card--dark)::after{opacity:.26}
@media (hover:hover) and (pointer:fine){
  .card:not(.card--dark):hover::after{opacity:.62}
}
@media (hover:none),(pointer:coarse){
  .card:not(.card--dark):hover::after{opacity:.26}
}
@media (prefers-reduced-motion:reduce){
  .card:not(.card--dark)::after{opacity:.18}
}

/* 22.14 Against section 21, at both ends of the pass.

   The panel behind a card is not one colour: --panel-a runs .13 to .36 and
   back, and the clip runs bright to mid underneath it, so the surface the
   card is lying on moves between relative luminance .233 and .735 - a factor
   of three - while the card itself stays put. Every value above was chosen to
   hold across that whole span rather than at the settled value, and each cue
   is deliberately strongest where the others are weakest. Nothing in section
   22 writes --panel-a, --panel-a-navy or a backdrop-filter, so the glass is
   exactly as translucent after this section as before it.

   Card face against the panel:  1.29:1 bright-settled (worst), 1.41:1
   bright-open, 2.45:1 mid-settled, 3.59:1 mid-open. The face alone is NOT
   enough at the bright end, which is the whole argument for the edge. Body
   copy is unaffected by any of it: the card is opaque, so navy on paper is a
   flat 14.31:1 wherever the clip happens to be, 13.42:1 on the foot, 13.22:1
   on the chip, 12.85:1 on the action strip, and --text-muted never drops
   below 6.17:1. The 4.5:1 floor is cleared with 37 percent headroom on the
   worst surface, at every alpha, because no alpha reaches the text.

   Underside line:  2.27:1 bright-settled, 2.09:1 bright-open - it carries the
   card exactly where the face gives up. 1.20:1 at mid, where it is not needed
   because the face is at 2.45:1 to 3.59:1 there. The rim does not cross it in
   any state that matters, because the conic is transparent from 232deg round
   to 360deg and the underside is where it spends most of its time.

   Side line as PAINTED, rim at .26:  1.03:1 bright-settled (deliberately
   invisible - a side line fighting bright light is what makes a card look
   drawn), 1.12:1 bright-open, 1.95:1 mid-settled, 2.86:1 mid-open. Solid
   states at edge-a .38: 1.70:1 on --cream, 1.52:1 on --cream-2.

   Contact shadow, honest:  1.62:1 bright-settled, 1.60:1 bright-open, 1.48:1
   and 1.38:1 at mid, 1.66:1 on solid cream. Still the flattest cue across the
   alpha animation - it is the one that must not flicker as the glass opens
   and closes, because it is what says "resting on". The first draft's
   1.91/1.88/1.69/1.53 assumed a shadow the negative spread never let render.

   And the drift: 21.3 moves the card 16px down to 12px up and scales .988 to
   .992. A 1px contact shadow travels glued to the card through that and does
   not pool underneath it at either end, which a single 0 8px 22px blur does.
   ========================================================================== */

/* ===== craft-2:palette ===== */
/* ===== craft-3:palette ===== */
/* ==========================================================================
   22. One image, not two. The palette, judged against the footage.

   The complaint is "out of touch" and the measurement agrees, but the hue was
   the symptom. Read as one picture rather than surface by surface, the page had
   four faults and only one of them was the navy.

   HOW THE NUMBERS BELOW WERE TAKEN. Ten frames of assets/video/cielo-720.mp4,
   put through .page-video's own saturate(1.12) contrast(1.04), composited under
   .page-shade at the actual alpha that gradient reaches at each height of the
   viewport, with 15.5's cx-dusk at full strength for the foot of a long page,
   then blurred at the radius the glass blurs at (sigma 24px at a 1366px
   viewport) - because a panel reads the AVERAGE under it, not single pixels.
   Percentiles are of that blurred field. The result is the real range a panel
   sits on:

     brightest anywhere        rgb(230,200,159)  L=.608
     mid-viewport median       rgb(184,161,136)  L=.374
     viewport foot, median     rgb( 90, 74, 70)  L=.075
     viewport foot, 5th pct    rgb( 61, 54, 59)  L=.039   <- the governing case
     viewport foot, floor      rgb( 61, 50, 52)  L=.035

   Note what is NOT in that list: a "blown" frame around L=.80. There isn't one.
   Once the shade and the blur are applied nothing under a panel is brighter
   than L=.61. Design the dark panels against L=.61 and the light panels
   against L=.035, and both ends are covered. Ratios are WCAG.

   22.0.1 The navy stopped being navy, and the whole page collapsed to ONE hue.
   Dark glass rgba(22,38,77,.48) over the brightest real backdrop composites to
   rgb(144,133,124) hsv(33,14,56) once the panel's own 14.15.4 highlight is
   added. A cream panel at .36 over a mid-viewport median composites to
   rgb(210,191,171) hsv(30,19,82). Paper and slate - the two materials 14.15
   built this site out of - were landing three hue degrees apart. There was no
   material distinction left, only a value step.

   22.0.2 It was a legibility defect, not a style. On the brightest real
   backdrop the dark panel gave --ink-on-dark 3.18:1, --muted-1 2.53:1,
   --muted-2 1.87:1 and --gold 2.07:1 - every one under 4.5:1 - and the header
   bar was worse at 3.70:1 for its own navigation, because its filter SATURATED
   the sky by 1.2 before tinting it. A cream panel at the foot of a long page
   gave --text 3.48:1 and --text-muted 1.67:1: the PRIMARY body colour failing,
   not just the secondary.

   22.0.3 The gold could not register as an accent on anything light. Brand gold
   #e3c169 is hsv(43,54,89); the sky it is asked to sit on is hsv(31,31,90).
   Near enough the same hue and the same value, the sky merely less saturated -
   so the gold masthead rule on a cream panel measured 1.20:1 to 1.31:1. Gold is
   not light on this page at all: its luminance .5549 is BELOW the cream panel's
   .70, so 14.15's own rule - gold is light, never an outline - was being
   applied to a surface brighter than the gold.

   22.0.4 The cream panels had not started disappearing into the clouds; they
   had already gone. Panel against sky measured 1.16:1 on the brightest frames.
   The first draft of this section concluded that a .36 veil simply cannot
   out-brighten a sky at value 90 and that the edge would have to come from
   chroma and a drawn line. That conclusion was wrong, and it is worth saying
   why: it is true of a veil, and false the moment the backdrop is CONDITIONED,
   because brightness() multiplies the ground before the veil is laid on it.
   With the settings in 22.2 the same .36 tint measures 1.45:1 against the
   brightest sky and 1.93:1 against a median one. The panels are back.

   22.0.5 What is NOT wrong, and is not touched.
   - --navy-1 / -2 / -3, --gold, --gold-deep, --cream, --cream-2, --white,
     --ink-on-dark, --muted-1 / -2 / -3, --display, --verse-on-dark,
     --gold-tint, --paper-line, --zelle, --fb, --live, --error-*. Every one of
     them measures correctly on the surface it was designed for. The problem
     was never the tokens; it was laying 36 to 48 percent of them over a lit
     sky and expecting the sky not to win.
   - The cards. They are opaque and they were always right: --text on --cream
     is 13.85:1. 21.3 was correct to keep the transparency off them.
   - Gold on navy: 5.16:1 on the new dark panel at a median backdrop, 8.53:1
     for navy text on a gold button. Gold does its job everywhere it sits on
     dark, which is exactly where 14.15 said it belonged.
   - The choreography of 21.2 - the 34 / 24 / 30 px blur, the .13 / .36 / .13
     and .24 / .48 / .24 alpha, the flat held middle from 32 to 68 percent.
     The shape was right. One number in it was pointed the wrong way.

   22.0.6 The idea. The panels stop trying to colour the sky and condition it
   first: backdrop-filter neutralises and re-levels the frame under the glass
   so the tint on top has something to tint, and the clip stays fully vivid
   everywhere a panel is not. The page then reads as a gold-lit scene with
   veiled windows cut into it, which is one image instead of two.

   22.0.7 What this COSTS, stated plainly, because it is the real trade.
   Conditioning the backdrop attenuates it. Measured as the fraction of the
   clip's own variation that still reaches the eye through the glass:

     light panels   today .64 -> .42   (reads like alpha .58, not .36)
     dark panels    today .52 -> .24   (reads like alpha .76, not .48)
     header / foot  today .58 -> .35   (reads like alpha .65, not .42)

   Nothing is opaque and the doves still travel through every surface - as
   perceptual lightness range across the real clip, a light panel carries 22.9
   of the 36.3 it carried before and the bar 24.5 of 30.5. But a dark panel
   carries 17.0 of 33.3, and that is the one number in this section somebody
   should look at on a screen before agreeing to it. It is the price of taking
   --ink-on-dark from 3.18:1 to 6.93:1, and no gentler setting buys that.

   22.0.8 KNOWN OPEN ITEM, not fixed here because it is not this section's
   property. 15.8 paints rgba(10,17,40,.50) over a panel AND its text as the
   panel exits through the top of the window. Under that veil the corrected
   light panel measures --text 4.07:1 at the top of the page and 2.84:1 at the
   foot. Everything below is measured on the panel itself; the departure veil
   defeats it independently of anything here. The fix belongs in 15.8 - take
   the veil to about .28, or mask it to the panel's own padding so it stops
   crossing the copy - and should be a separate, deliberate change.
   ========================================================================== */

/* 22.1 Tokens. Four changes, and only four.

   --glass-navy-rgb, 22,38,77 to 22,44,77. Six units of green. Identical
   saturation (71 percent) and value (30 percent); hue 223 to 216. The footage
   is hue 31 to 41 and its complement is near 216. A complementary pair reads
   as one lit space only when the dark half sits opposite the light half: 223 is
   a printed navy under neutral light, 216 is the hue a shadow actually takes
   when the light is this warm. It is invisible as a swatch beside #16264d and
   decisive across a whole panel. --navy-2 itself is NOT changed, and the three
   fallback states below re-pin the triplet to it, so no-video, reduced motion
   and print land exactly where 17.3 and 21.1 said they would.

   --gold-ink #a8832c, new. Hue 42 against --gold's 43 - the same gold, taken
   down to value 66 and up to saturation 74 so it can be a LINE on something
   light. This is 14.15's rule kept rather than broken: gold is light, so on a
   surface brighter than gold it has to be drawn as ink instead. Rules and
   hairlines only. Navy text on it is 4.19:1, so it is never a button face;
   buttons keep --gold at 8.53:1.

   --text-muted, #4a5877 to #2f3a55, on the glass pages only and put back
   inside the opaque cards where it never failed. Same hue (221 to 223), value
   47 to 33. This token is load-bearing, not cosmetic, and the conditioning
   alone does not save it: under the CORRECTED surfaces in 22.2 the old value
   still measures 3.00:1 at the worst light surface, where the new value
   measures 4.77:1.

   The light panel's border, rgba(255,255,255,.28) to --hair-gold-ink. The white
   edge measured about 1.1:1 against the panel - it was not an edge, and it said
   nothing about this church. The ink keyline measures 1.42:1 to 1.81:1. Take
   the alpha to .40 if it reads heavy. */
body.page-glass{
  --glass-navy-rgb:22,44,77;
  --gold-ink:#a8832c;
  --hair-gold-ink:rgba(168,131,44,.55);
  --text-muted:#2f3a55;
}
body.page-glass main>section:not(.hero) .card:not(.card--dark){
  --text-muted:#4a5877;
}

/* 22.2 The light panels. Condition, then tint.

   saturate(.18) takes the sky's chroma out from under the veil; contrast(.44)
   pulls the frame's range in so a cloud and a wing stop swinging the reading
   ground by 40 points of luminance between frames; brightness(1.50) then puts
   the whole thing back up where paper lives. The order matters: desaturate,
   re-level, then lift.

   The lift is the number an earlier draft got wrong, and it is worth recording
   why. Measured against a set of hand-picked "frames" the light panels looked
   safe at contrast(.65) brightness(1.27). Measured against the blurred field
   described at the head of this section - which is what a panel actually sits
   on - that setting puts --text-muted at 3.49:1 and --text itself at 4.57:1 on
   a .section--alt panel at the foot of a long page, because the governing
   backdrop there is L=.035 and not the L=.098 the draft assumed. The lift has
   to clear that floor, not an average.

   Reading position, worst surface (.section--alt, plus the --mat-panel wash at
   the panel's own foot, over the darkest blurred backdrop): panel
   rgb(171,168,164), --text 6.24:1, --text-muted 4.77:1. Over a median mid-
   viewport backdrop: rgb(229,226,223), 11.51:1 and 8.79:1. Over the brightest:
   rgb(247,243,238), 13.41:1 and 10.24:1 - and 1.45:1 against the sky beside it,
   which is the separation 22.0.4 said could not exist.

   The panel settles at saturation 3 to 6 against a scene at saturation 26. That
   chroma gap, the value gap the lift now buys, and the keyline are three
   separations where there was arguably none. */
body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero){
  border-color:var(--hair-gold-ink);
  -webkit-backdrop-filter:blur(24px) saturate(.18) contrast(.44) brightness(1.50);
  backdrop-filter:blur(24px) saturate(.18) contrast(.44) brightness(1.50);
}

/* The masthead rule, redrawn in ink. The same asymmetric shape 14.15.3 chose -
   brightest just off the left corner, gone before the right one - but as a line
   on paper rather than a highlight on glass. The peak measures 2.02:1 against a
   median panel where the old bright gold measured 1.20:1. The dark panels keep
   the original gold rule untouched: there it genuinely is light falling on
   slate, and there it measures 5.16:1. */
body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero)::before{
  background:linear-gradient(90deg,
    rgba(168,131,44,0) 0%,
    rgba(168,131,44,.72) 16%,
    rgba(168,131,44,.52) 46%,
    rgba(168,131,44,.20) 74%,
    rgba(168,131,44,0) 94%);
}

/* 22.3 The dark panels. The same technique, pointed the other way.

   Under slate the frame is darkened rather than lifted, and contrast() goes
   ABOVE one rather than below it: brightness(.39) takes the sky down far enough
   for the navy to read, and contrast(1.20) puts the structure back so the clip
   is still visibly moving through the glass instead of sitting behind a solid
   wall. saturate(.70) is deliberately the gentlest desaturation in this
   section - a dark panel does not need much help, because once the ground is
   at value 25 the tint's own hue is the loudest thing in the mix.

   Reading position over the brightest real backdrop, with 14.15.4's own
   top-left highlight included: rgb(79,82,86) hsv(217,8,34) - the brand navy's
   own value, at the scene's complementary hue. --ink-on-dark 6.93:1, --display
   6.55:1, --verse-on-dark 5.21:1, --muted-1 5.53:1, --gold 4.51:1, against
   today's 3.18 / 3.02 / 2.39 / 2.53 / 2.07. Over a median backdrop it deepens
   to rgb(70,74,80) and those become 7.94 / 5.96 / 6.33 / 5.16.

   One honest exception, left alone on purpose. --muted-2 measures 4.08:1 at
   the exact peak of 14.15.4's top-left highlight over the brightest possible
   backdrop - just under 4.5:1. It is 5.29:1 or better across the rest of the
   panel, and the only token that actually sits in that corner is the .eyebrow,
   which is --gold at 4.51:1 and passes. Crushing brightness to .32 would buy
   --muted-2 4.56:1 and cost a sixth of what is left of the clip. If that corner
   ever does carry small muted copy, fix it by taking 14.15.4's white highlight
   from .06 to .04 - not by darkening every dark panel on the site. */
body.page-glass main>section.section--dark,
body.page-glass main>section.page-hero{
  -webkit-backdrop-filter:blur(24px) saturate(.70) contrast(1.20) brightness(.39);
  backdrop-filter:blur(24px) saturate(.70) contrast(1.20) brightness(.39);
}

/* 22.4 The pass, restated. Section 21's timing, ranges and alphas are kept
   exactly as they are; what changes is that the conditioning now breathes WITH
   the alpha instead of against it.

   The rule 21.2 was missing: the thinner the tint, the harder the backdrop has
   to be conditioned, because the tint is doing less of the work. 21.2 already
   knew this for blur - 34 px at the edges of the pass, 24 px in the middle -
   and then pointed saturate the other way, up to 1.30 at precisely the moment
   the glass was most open. That one number is what let the sky through at full
   chroma and turned the panel into a warm grey.

   What breathes now is CHROMA. A light panel runs saturate .42 at the edges of
   the pass and .18 through the reading third; a dark one .60 and .70 with the
   darkening easing off at the ends. So a panel takes on the scene's colour
   while it is entering and leaving - which is when it is half off the screen
   and touching the sky - and cools into a surface in the middle, which is where
   reading happens. Luminance stays near-constant across the whole pass by
   design, and that is checked rather than asserted: at the same backdrop the
   light panel's own luminance moves by at most .022 between 0, 32 and 100
   percent, so a reader's contrast does not move under them mid-sentence.

   Both ends measured, the full blurred range, both light tints, with the
   --mat-panel foot wash. Worst ratio anywhere in the light pass: --text-muted
   4.77:1 at the reading position over the darkest ground, 4.83:1 at 0 percent,
   5.02:1 at 100 percent. Worst anywhere in the dark pass: --muted-2 4.24:1 at
   0 percent in the 14.15.4 highlight corner, 4.97:1 outside it.

   One thing NOT measured by any of this: 15.8's departure veil, which paints
   on top of the finished panel during exactly the exit this block describes.
   See 22.0.8.

   Two keyframe sets now, because paper and slate need opposite conditioning and
   one animation name cannot branch. The dark panels keep every other animation
   longhand from 21.2 and swap only the name - verified in the browser:
   .section--dark computes animation-name "halo-breathe, panel-pass-navy", so
   19.2.2's bloom survives the swap. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){

    @keyframes panel-pass{
      0%{
        --panel-a:.13;
        -webkit-backdrop-filter:blur(34px) saturate(.42) contrast(.32) brightness(1.62);
        backdrop-filter:blur(34px) saturate(.42) contrast(.32) brightness(1.62);
      }
      32%{
        --panel-a:.36;
        -webkit-backdrop-filter:blur(24px) saturate(.18) contrast(.44) brightness(1.50);
        backdrop-filter:blur(24px) saturate(.18) contrast(.44) brightness(1.50);
      }
      68%{
        --panel-a:.36;
        -webkit-backdrop-filter:blur(24px) saturate(.18) contrast(.44) brightness(1.50);
        backdrop-filter:blur(24px) saturate(.18) contrast(.44) brightness(1.50);
      }
      100%{
        --panel-a:.15;
        -webkit-backdrop-filter:blur(30px) saturate(.40) contrast(.30) brightness(1.62);
        backdrop-filter:blur(30px) saturate(.40) contrast(.30) brightness(1.62);
      }
    }

    html:not(.no-js) body.page-glass:not(.no-video) main>section.section--dark,
    html:not(.no-js) body.page-glass:not(.no-video) main>section.page-hero{
      animation-name:halo-breathe,panel-pass-navy;
    }
    @keyframes panel-pass-navy{
      0%{
        --panel-a-navy:.24;
        -webkit-backdrop-filter:blur(34px) saturate(.60) contrast(1.20) brightness(.31);
        backdrop-filter:blur(34px) saturate(.60) contrast(1.20) brightness(.31);
      }
      32%{
        --panel-a-navy:.48;
        -webkit-backdrop-filter:blur(24px) saturate(.70) contrast(1.20) brightness(.39);
        backdrop-filter:blur(24px) saturate(.70) contrast(1.20) brightness(.39);
      }
      68%{
        --panel-a-navy:.48;
        -webkit-backdrop-filter:blur(24px) saturate(.70) contrast(1.20) brightness(.39);
        backdrop-filter:blur(24px) saturate(.70) contrast(1.20) brightness(.39);
      }
      100%{
        --panel-a-navy:.26;
        -webkit-backdrop-filter:blur(30px) saturate(.60) contrast(1.20) brightness(.32);
        backdrop-filter:blur(30px) saturate(.60) contrast(1.20) brightness(.32);
      }
    }
  }
}

/* 22.5 The bar and the foot, which were the worst of it. The header is the
   first thing anyone sees, it is the one element that is ALWAYS over the
   brightest part of the frame, and its own filter was saturating that frame by
   1.2 before tinting it: rgb(132,123,106), --ink-on-dark 3.70:1, --gold 2.41:1,
   --muted-2 2.18:1. The site's navigation was failing contrast on every page.

   Same conditioning as a dark panel, but at brightness(.50) rather than the
   panel's .39, and this is a deliberate departure from an earlier draft that
   used .34 everywhere. .34 was solving a problem the bar does not have. It took
   --ink-on-dark to 11.30:1 - roughly two and a half times the requirement - and
   paid for it by crushing the bar to rgb(51,50,54) at rest and the footer to
   rgb(19,20,30) at the foot of a page, where the shade is already deep. That is
   not glass, it is a charcoal slab with a blur behind it, and the footer in
   particular gained nothing: it measured 11.31:1 there before any filter at
   all. .50 holds every token clear at the bar's worst case - --ink-on-dark
   8.27:1, --muted-1 6.60:1, --muted-2 4.87:1, --gold 5.38:1 at rest and
   12.52 / 9.98 / 7.37 / 8.14 once the page has moved - and keeps half again as
   much of the clip: perceptual range 24.5 against .34's 16.7.

   The footer is the same recipe at .45 (worst case --muted-2 5.09:1, for
   .footer-bottom .small) and the scroll cue takes it too at .34, so the pill
   still matches the bar it was drawn to match. */
body.page-glass .site-header{
  -webkit-backdrop-filter:blur(20px) saturate(.60) contrast(1.20) brightness(.50);
  backdrop-filter:blur(20px) saturate(.60) contrast(1.20) brightness(.50);
}
body.page-glass .site-footer{
  -webkit-backdrop-filter:blur(12px) saturate(.60) contrast(1.20) brightness(.50);
  backdrop-filter:blur(12px) saturate(.60) contrast(1.20) brightness(.50);
}
body.page-glass .scroll-cue{
  -webkit-backdrop-filter:blur(10px) saturate(.60) contrast(1.20) brightness(.50);
  backdrop-filter:blur(10px) saturate(.60) contrast(1.20) brightness(.50);
}

/* 22.6 The three states where none of this exists.

   22.6.1 A repair that has to come first, because without it everything above
   is measured against a fallback that has never actually fired. 21.1 switches
   the glass off with --panel-a:1 and --panel-a-navy:1 declared on BODY - but
   both are registered with inherits:false, so a section never sees them and
   falls back to the registered initial values, .36 and .48. Verified in the
   browser against the unpatched sheet: with .no-video set, --panel-a computes 1
   on body and 0.36 on the section, and a light panel still paints
   rgba(250,247,242,.36) while a dark one paints rgba(22,38,77,.48) - not the
   solid colours 17.3 and 21.1 describe. That is the exact failure 17.3 wrote
   three paragraphs to prevent: a cream panel at .36 over --navy-3 resolves to
   rgb(96,100,113), and --text on it is 2.51:1. The declaration simply has to
   land on the element that reads it. Re-verified after this fix, all three
   states, from the computed styles: .no-video gives rgb(250,247,242) and
   rgb(22,38,77) with backdrop-filter none and animation-name none; reduced
   motion gives the same; print gives rgb(255,255,255) with the keyline at #999
   and the ::before gone.

   22.6.2 With the glass opaque, a backdrop-filter is a compositing layer paid
   for and never seen, so all three states drop it; and the navy triplet goes
   back to the brand value so the solid surfaces are identical to the ones 17.3
   chose. --text-muted stays at the deeper value in the video-less states on
   purpose - it measures 10.58:1 on solid cream - because a secondary type
   colour that changed when the video failed to load would be a worse bug than
   the one this section fixes. Print gets the original back, since paper is
   white and the original hierarchy was drawn for it.

   The selectors repeat the full :not() chains from 22.2 and 22.3 deliberately.
   A shorter selector here would lose: those rules are more specific and come
   earlier in the file, so a later but weaker "none" would never apply. */
body.page-glass.no-video main>section:not(.hero){
  --panel-a:1;
  --panel-a-navy:1;
}
body.page-glass.no-video{
  --glass-navy-rgb:22,38,77;
}
body.page-glass.no-video main>section:not(.hero):not(.section--dark):not(.page-hero),
body.page-glass.no-video main>section.section--dark,
body.page-glass.no-video main>section.page-hero,
body.page-glass.no-video .site-header,
body.page-glass.no-video .site-footer,
body.page-glass.no-video .scroll-cue{
  -webkit-backdrop-filter:none;backdrop-filter:none;
}

@media (prefers-reduced-motion:reduce){
  body.page-glass{
    --glass-navy-rgb:22,38,77;
  }
  body.page-glass main>section:not(.hero){
    --panel-a:1;
    --panel-a-navy:1;
  }
  body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero),
  body.page-glass main>section.section--dark,
  body.page-glass main>section.page-hero,
  body.page-glass .site-header,
  body.page-glass .site-footer,
  body.page-glass .scroll-cue{
    -webkit-backdrop-filter:none;backdrop-filter:none;
  }
}

@media print{
  body.page-glass{
    --glass-navy-rgb:22,38,77;
    --text-muted:#4a5877;
  }
  body.page-glass main>section:not(.hero){
    --panel-a:1;
    --panel-a-navy:1;
  }
  body.page-glass main>section:not(.hero),
  body.page-glass .site-header,
  body.page-glass .site-footer,
  body.page-glass .scroll-cue{
    -webkit-backdrop-filter:none!important;backdrop-filter:none!important;
  }
  body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero){
    border-color:#999!important;
  }
  body.page-glass main>section:not(.hero)::before{background:none!important}
}

/* ===== craft-2:palette-reconcile ===== */
/* ==========================================================================
   23. The reconciled palette. Appended last, so this block is the authority.

   Three specialists wrote section 22 without seeing each other. Two of them
   set the same surfaces to different values. This block keeps ONE value for
   each, and re-asserts the three states where the video does not exist.

   WHAT WINS, AND WHY, IN ONE LINE EACH.
   - Conditioning values: the coherence block's, because its backdrop anchors
     were sampled from assets/video/cielo-720.mp4 through .page-shade and the
     blur, and the glass block's were stipulated. The governing case at the
     foot of a long page - relative luminance about .035 - only exists in the
     sampled set, and it is where the old secondary ink measures 3.48 to 1.
   - Conditioning MECHANISM: the glass block's single --pass number. Filters
     stay in ordinary rules, so the fallback states can switch them off without
     fighting an animation, and one keyframe set serves paper and slate.
   - The navy tint moves to 22,44,77 and the cream tint does NOT move. The
     cream was warmed to survive a saturate of 54 percent; at saturate .18 the
     warming only puts back the cast the whole section removes.
   - Chrome keeps the glass block's raised tints and its harder desaturation,
     with the coherence block's contrast above 1. At tint .42 and saturate .60
     the bar composites warm-neutral, which is the original complaint.
   - Every blur radius in the file is left exactly as the file had it. Only
     colour conditioning changes.
   - The card block is additive and survives whole, with one number moved:
     see 23.3.

   NOT FIXED HERE, and still open: 15.8's departure veil paints
   rgba(10,17,40,.50) over a panel AND its copy as the panel exits the top of
   the window. It defeats any panel value, and the repair belongs in 15.8.
   ========================================================================== */

@property --pass{
  syntax:"<number>";
  inherits:false;
  initial-value:1;
}

/* 23.1 Tokens. One declaration per token, in one place.
   --text-muted is the load-bearing one: on a conditioned cream panel over the
   darkest sampled backdrop the old #4a5877 measures 3.48 to 1 and #2f3a55
   measures 5.53 to 1. It is put back to #4a5877 inside the light cards, which
   are opaque and where it never failed, and on paper. */
body.page-glass{
  --glass-cream-rgb:250,247,242;
  --glass-alt-rgb:243,234,216;
  --glass-navy-rgb:22,44,77;
  --glass-bar:rgba(10,17,40,.46);
  --glass-bar-lit:rgba(10,17,40,.70);
  --glass-foot:rgba(10,17,40,.48);
  --gold-lit:#edd08a;
  --gold-ink:#a8832c;
  --hair-gold-ink:rgba(168,131,44,.55);
  --text-muted:#2f3a55;
}
body.page-glass main>section:not(.hero) .card:not(.card--dark){
  --text-muted:#4a5877;
}

/* 23.2 The light panels. saturate takes the sky's chroma out from under the
   veil, contrast pulls the frame's range in, brightness puts paper back where
   paper lives - in that order, and all three breathe off --pass so a panel
   takes the scene's colour at the edges of its pass and cools into a reading
   surface in the middle.
     --pass 1, brightest sampled frame : panel rgb(249,245,240), --text 13.6,
                                         --text-muted 10.4
     --pass 1, darkest sampled frame   : panel rgb(183,181,180), --text 7.2,
                                         --text-muted 5.5
   The fallback in every var() is the settled value, so a browser without
   @property gets the reading-position conditioning rather than an invalid
   declaration and no conditioning at all. */
body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero){
  border-color:var(--hair-gold-ink);
  -webkit-backdrop-filter:
    blur(calc(34px - 10px * var(--pass,1)))
    saturate(calc(.42 - .24 * var(--pass,1)))
    contrast(calc(.32 + .12 * var(--pass,1)))
    brightness(calc(1.62 - .12 * var(--pass,1)));
  backdrop-filter:
    blur(calc(34px - 10px * var(--pass,1)))
    saturate(calc(.42 - .24 * var(--pass,1)))
    contrast(calc(.32 + .12 * var(--pass,1)))
    brightness(calc(1.62 - .12 * var(--pass,1)));
}
/* The masthead rule as ink rather than light: gold's luminance is BELOW a
   conditioned cream panel's, so on paper it has to be drawn, not shone. */
body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero)::before{
  background:linear-gradient(90deg,
    rgba(168,131,44,0) 0%,
    rgba(168,131,44,.72) 16%,
    rgba(168,131,44,.52) 46%,
    rgba(168,131,44,.20) 74%,
    rgba(168,131,44,0) 94%);
}

/* 23.3 The cards, and the one number the card block has to move.
   The card block set the side hairline to .16 over a moving panel and .38 only
   in the states where the panel goes solid. Conditioning makes the light panel
   behave like the solid case at every frame - settled it is rgb(249,245,240)
   against a card face of #fdfbf6, which is 1.05 to 1 - so the case .38 was
   written for is now the ordinary case. The card block's own arithmetic is
   what carries it: at .38 the painted side edge reads 1.70 to 1 with the halo
   rim held at .26, and the underside line reads 2.8 to 1. Nothing else in that
   block moves; the paper, the shadow stack, the chip and the action strip are
   all backdrop-independent because the cards are opaque. */
body.page-glass .card{--card-edge-a:.38}

/* 23.4 The dark panels and the title band. Same technique pointed the other
   way: brightness takes the frame down far enough for navy to read, contrast
   ABOVE one puts the structure back so the clip is still visibly moving, and
   saturate is the gentlest in the section because at value 25 the tint's own
   hue is the loudest thing in the mix.
     --pass 1, brightest sampled frame: rgb(61,66,74) hue 214 - --ink-on-dark
     8.5 to 1, --gold 5.5 to 1, --muted-2 5.0 to 1 on the flat surface.
   The six units of green in the tint are what put the composite at hue 214
   instead of 226: the clip is hue 31 to 41 and a shadow under light that warm
   does not land on a printed navy. */
body.page-glass main>section.section--dark,
body.page-glass main>section.page-hero{
  border-color:rgba(227,193,105,.34);
  -webkit-backdrop-filter:
    blur(calc(34px - 10px * var(--pass,1)))
    saturate(calc(.60 + .10 * var(--pass,1)))
    contrast(1.20)
    brightness(calc(.31 + .08 * var(--pass,1)));
  backdrop-filter:
    blur(calc(34px - 10px * var(--pass,1)))
    saturate(calc(.60 + .10 * var(--pass,1)))
    contrast(1.20)
    brightness(calc(.31 + .08 * var(--pass,1)));
}

/* 23.5 Small text on dark glass. Inside 14.15.4's top-left highlight - which
   is exactly where an eyebrow sits - brand gold measures about 4.5 to 1 and
   --muted-2 about 4.1 to 1. The gold gets a lit variant for margin and the
   15px --muted-2 lines move up to --muted-1, which is the colour the leads in
   the same panels already use. Shapes - rules, borders, buttons, the map chip
   - keep --gold. All of this goes back in the three states below. */
body.page-glass main>section.section--dark .eyebrow,
body.page-glass main>section.page-hero .eyebrow,
body.page-glass main>section.section--dark .verse-ref,
body.page-glass main>section.section--dark .map-credit{
  color:var(--gold-lit);
}
body.page-glass main>section.section--dark .small,
body.page-glass main>section.page-hero .small{
  color:var(--muted-1);
}

/* 23.6 The pass. --panel-a and --panel-a-navy stay NUMBERS, at exactly the
   steps 21.2 shipped, so 21.1's rgba() still interpolates channel by channel.
   The only addition is --pass, from which 23.2 and 23.4 derive their filters.

   @keyframes panel-pass is REDEFINED rather than renamed, deliberately. 21.2's
   version bakes a backdrop-filter into the keyframes, and an animated
   backdrop-filter beats every rule in this file; redefining the name removes
   that declaration outright instead of routing around it, and it is the reason
   no animation-name has to be swapped for the dark panels - one --pass serves
   both materials, and each material reads its own alpha. animation-name is
   re-asserted anyway, because two earlier blocks pointed it at keyframe sets
   of their own. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    html:not(.no-js) body.page-glass:not(.no-video) main>section:not(.hero),
    html:not(.no-js) body.page-glass:not(.no-video) main>section.section--dark,
    html:not(.no-js) body.page-glass:not(.no-video) main>section.page-hero{
      animation-name:halo-breathe,panel-pass;
    }
    @keyframes panel-pass{
      0%  {--panel-a:.13;--panel-a-navy:.24;--pass:0}
      32% {--panel-a:.36;--panel-a-navy:.48;--pass:1}
      68% {--panel-a:.36;--panel-a-navy:.48;--pass:1}
      100%{--panel-a:.15;--panel-a-navy:.26;--pass:.1}
    }
  }
}

/* 23.7 The bar, the foot and the cue - one recipe, three blurs the file
   already had. The header is always over the brightest part of the frame and
   its own filter used to SATURATE that frame by 1.2 before tinting it, which
   is why the navigation measured 3.5 to 1 on every page.
   Measured at the bar's worst case, brightest sampled frame, tint .46:
   bar rgb(66,66,72) - --ink-on-dark 8.3 to 1, --gold 5.4 to 1, --muted-2 4.9
   to 1. Scrolled, at .70, nothing on it is under 12 to 1.
   The cue is already a pill with padding and a radius - 14.13 made it one - so
   it takes the same conditioning; its tint goes .34 to .42 because at .34 the
   pill composites warm-neutral, which is the defect this section exists for. */
body.page-glass .site-header{
  -webkit-backdrop-filter:blur(20px) saturate(.34) contrast(1.20) brightness(.48);
  backdrop-filter:blur(20px) saturate(.34) contrast(1.20) brightness(.48);
}
body.page-glass .site-footer{
  -webkit-backdrop-filter:blur(12px) saturate(.34) contrast(1.20) brightness(.48);
  backdrop-filter:blur(12px) saturate(.34) contrast(1.20) brightness(.48);
}
body.page-glass .scroll-cue{
  background-color:rgba(10,17,40,.42);
  -webkit-backdrop-filter:blur(10px) saturate(.34) contrast(1.20) brightness(.48);
  backdrop-filter:blur(10px) saturate(.34) contrast(1.20) brightness(.48);
}
body.page-glass .scroll-cue .scroll-cue-word{
  text-shadow:0 1px 2px rgba(10,17,40,.92),0 0 14px rgba(10,17,40,.75);
}

/* 23.8 No video. THE REPAIR THAT HAS TO COME FIRST: --panel-a and
   --panel-a-navy are registered inherits:false, so 21.1 declaring them on BODY
   never reaches a section - the section falls back to the registered initial
   values .36 and .48 and stays translucent with nothing behind it. A cream
   panel at .36 over --navy-3 is rgb(96,100,113), and --text on that is about
   2.6 to 1. The declaration has to land on the element that reads it.
   The navy triplet goes back to the brand value so the solid surfaces are the
   ones 17.3 chose. --text-muted does NOT go back: it measures 10.6 to 1 on
   solid cream, and a secondary type colour that changed when the video failed
   to load would be a worse bug than the one being fixed. */
body.page-glass.no-video,
body.page-glass.no-video main>section:not(.hero){
  --glass-cream-rgb:250,247,242;
  --glass-navy-rgb:22,38,77;
  --text-muted:#2f3a55;
}
body.page-glass.no-video main>section:not(.hero){
  --panel-a:1;--panel-a-navy:1;--pass:1;
}
body.page-glass.no-video{
  --glass-bar:rgba(10,17,40,.96);
  --glass-bar-lit:rgba(10,17,40,.98);
  --glass-foot:var(--navy-3);
}
body.page-glass.no-video main>section:not(.hero):not(.section--dark):not(.page-hero),
body.page-glass.no-video main>section.section--dark,
body.page-glass.no-video main>section.page-hero,
body.page-glass.no-video .site-header,
body.page-glass.no-video .site-footer,
body.page-glass.no-video .scroll-cue{
  -webkit-backdrop-filter:none;backdrop-filter:none;
}
body.page-glass.no-video main>section.section--dark .eyebrow,
body.page-glass.no-video main>section.page-hero .eyebrow,
body.page-glass.no-video main>section.section--dark .verse-ref,
body.page-glass.no-video main>section.section--dark .map-credit{color:var(--gold)}
body.page-glass.no-video main>section.section--dark .small,
body.page-glass.no-video main>section.page-hero .small{color:var(--muted-2)}

/* 23.9 Reduced motion. Same solid states, and the bar and foot tokens have to
   be restored BY HAND: 17.3 sets them on body.page-glass, which is the same
   specificity as 23.1 and loses to it on order. Without these three lines a
   reader who asked for no motion gets a .46 bar with no video behind it. */
@media (prefers-reduced-motion:reduce){
  body.page-glass{
    --glass-cream-rgb:250,247,242;
    --glass-navy-rgb:22,38,77;
    --text-muted:#2f3a55;
    --glass-bar:rgba(10,17,40,.96);
    --glass-bar-lit:rgba(10,17,40,.98);
    --glass-foot:var(--navy-3);
  }
  body.page-glass main>section:not(.hero){
    --panel-a:1;--panel-a-navy:1;--pass:1;
  }
  body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero),
  body.page-glass main>section.section--dark,
  body.page-glass main>section.page-hero,
  body.page-glass .site-header,
  body.page-glass .site-footer,
  body.page-glass .scroll-cue{
    -webkit-backdrop-filter:none!important;backdrop-filter:none!important;
  }
  body.page-glass main>section.section--dark .eyebrow,
  body.page-glass main>section.page-hero .eyebrow,
  body.page-glass main>section.section--dark .verse-ref,
  body.page-glass main>section.section--dark .map-credit{color:var(--gold)}
  body.page-glass main>section.section--dark .small,
  body.page-glass main>section.page-hero .small{color:var(--muted-2)}
}

/* 23.10 Paper. The original ink hierarchy comes back, because paper is white
   and it was drawn for white. Every hairline that has to carry a card or a
   panel once fill and shadow are gone is #8a8a8a, not #999: #999 on white is
   2.85 to 1, under the 3 to 1 floor for a non-text boundary, and #8a8a8a is
   3.45 to 1. The infocard's CHILDREN are named as well as the plate - a child
   that declares its own colour does not inherit the plate's, and !important on
   the plate cannot reach it. */
@media print{
  body.page-glass{
    --glass-cream-rgb:250,247,242;
    --glass-navy-rgb:22,38,77;
    --text-muted:#4a5877;
  }
  body.page-glass main>section:not(.hero){
    --panel-a:1;--panel-a-navy:1;--pass:1;
  }
  body.page-glass main>section:not(.hero),
  body.page-glass main>section.section--dark,
  body.page-glass main>section.page-hero,
  body.page-glass .site-header,
  body.page-glass .site-footer,
  body.page-glass .scroll-cue{
    -webkit-backdrop-filter:none!important;backdrop-filter:none!important;
  }
  body.page-glass main>section:not(.hero):not(.section--dark):not(.page-hero){
    border-color:#8a8a8a!important;
  }
  body.page-glass main>section:not(.hero)::before{background:none!important}
  body.page-glass .card,
  body.page-glass .card--dark,
  body.page-glass .infocard{
    background-color:#fff!important;
    border-color:#8a8a8a!important;
    border-top-color:#8a8a8a!important;
    border-bottom-color:#8a8a8a!important;
    box-shadow:none!important;
  }
  body.page-glass .infocard .lab,
  body.page-glass .infocard .val,
  body.page-glass .infocard .val a,
  body.page-glass .infocard .sub,
  body.page-glass main>section.section--dark .eyebrow,
  body.page-glass main>section.page-hero .eyebrow,
  body.page-glass main>section.section--dark .verse-ref,
  body.page-glass main>section.section--dark .verse,
  body.page-glass main>section.section--dark .lead,
  body.page-glass main>section.page-hero .page-hero-sub,
  body.page-glass main>section.section--dark .small,
  body.page-glass main>section.page-hero .small,
  body.page-glass main>section.section--dark .map-credit{
    color:var(--text)!important;
  }
}

/* ==========================================================================
   24. Audit fixes. Each one is a defect an adversarial reviewer reproduced,
   not a preference. Numbers are theirs, re-derived, not asserted.
   ========================================================================== */

/* 24.1 THE GIVING BUTTON WAS UNREADABLE ON EVERY PAGE.
   `.nav a{color:var(--ink-on-dark)}` (0,1,1) beats `.btn-primary{color:var(--text)}`
   (0,1,0), so the gold Ofrendas / Give pill in the header rendered CREAM text on
   gold: 1.24:1, 1.54:1 and 1.97:1 against the three gradient stops, where navy
   gives 6.65:1 to 10.58:1. On hover `.nav a:hover` replaced the gold background
   with transparent, leaving navy-on-dark-glass at 1.43:1 - an empty outlined
   pill. Every visitor, every width, both languages, all seven pages.

   Fixed by lifting ONLY colour and background back, at a specificity that wins
   outright. The `.nav a` block's display and padding are deliberately NOT
   touched: they currently beat `.btn` for this element and are what make the
   pill full-width in the phone menu. */
.site-header .nav a.btn-primary,
.site-header .nav a.btn-primary:hover,
.site-header .nav a.btn-primary:focus-visible{
  color:var(--text);
  background-image:linear-gradient(180deg,#f2d891 0%,var(--gold) 46%,var(--gold-deep) 100%);
  background-color:var(--gold);
  border-color:var(--gold-deep);
  text-decoration:none;
}
@media (hover:hover) and (pointer:fine){
  .site-header .nav a.btn-primary:hover{
    background-image:linear-gradient(180deg,#f7e3a8 0%,#e8ca78 46%,var(--gold-deep) 100%);
  }
}

/* 24.2 THE ZELLE QR WAS UNSCANNABLE ON A PHONE.
   `.zelle-tile img{max-width:300px}` (0,1,1) beats the reset `img{max-width:100%}`
   (0,0,1), giving the tile a hard 352px floor (300 + 48 padding + 4 border) inside
   a grid track that is 238px at 320px wide and 278px at 360px. The panel's
   overflow:hidden then CLIPPED it: 68px gone at 320px - the whole top-right finder
   pattern, so a decoder cannot locate the symbol at all - and 28px at 360px.
   The reviewer decoded the asset both ways to confirm: intact it resolves to the
   church's Zelle number, cropped it does not resolve at all. There is no
   horizontal scroll to reach the missing part, so the failure is silent.

   min-width:0 is load-bearing, not belt-and-braces: without it the grid item's
   intrinsic contribution is still computed from the image's 300px and the tile
   stays 352px wide even with a percentage max-width on the image. */
.zelle-tile{min-width:0;max-width:100%}
.zelle-tile img{max-width:min(300px,100%)}
@media (max-width:389px){
  .zelle-tile{padding:var(--s4) var(--s3) var(--s3)}
}

/* 24.3 THE HERO COPY FAILED CONTRAST AT EVERY FRAME.
   Measured per-glyph against real extracted frames rather than an average: the
   gold "Espíritu Santo" ran 1.06-1.27:1 against a 3:1 requirement, failing on
   100% of its glyph area; "Bienvenido" 1.62-1.99:1; the eyebrow 2.28-3.28:1
   against 4.5:1. On a 390px phone the motto clamps to 16.8px, which RAISES the
   requirement to 4.5:1, and all three motto lines then fail on 100% of area.

   Deepening the vignette alone cannot fix it - the reviewer calculated that
   clearing 4.5:1 on a phone needs about a 0.67 navy floor, which would black out
   the clip Chris built the page around. So the hero text block gets the same
   conditioning recipe the header and the scroll cue already use and that measures
   8.3:1 and 10.2:1 there: darken and desaturate the backdrop behind the copy,
   then let the type sit on it. The clip still moves through; it is simply no
   longer allowed to be brighter than the words. */
/* A backdrop-filter CLIPS at its own element box, so a filtered ellipse draws a
   visible hard-edged oval on the sky no matter how soft its gradient is. The
   filter has to be masked, not just faded: the mask feathers the conditioning
   itself out to nothing, so what the eye sees is the sky getting quieter behind
   the words with no boundary anywhere. */
body.page-glass .hero-inner::before{
  inset:-18% -12%;
  border-radius:0;
  background:radial-gradient(ellipse 66% 80% at 50% 47%,
    rgba(10,17,40,.80) 0%,
    rgba(10,17,40,.72) 48%,
    rgba(10,17,40,.34) 72%,
    rgba(10,17,40,0) 92%);
  -webkit-backdrop-filter:blur(6px) saturate(.55) brightness(.66);
  backdrop-filter:blur(6px) saturate(.55) brightness(.66);
  -webkit-mask-image:radial-gradient(ellipse 66% 80% at 50% 47%,#000 0%,#000 52%,rgba(0,0,0,.55) 74%,transparent 92%);
  mask-image:radial-gradient(ellipse 66% 80% at 50% 47%,#000 0%,#000 52%,rgba(0,0,0,.55) 74%,transparent 92%);
}
/* The accent is the single worst glyph run on the page, so it also stops being
   the palest gold available. --gold-lit is the variant 23.5 introduced for small
   text on dark glass. */
body.page-glass .hero .motto .motto-accent{color:var(--gold-lit)}
body.page-glass .hero .hero-eyebrow{color:var(--gold-lit)}

@media (prefers-reduced-motion:reduce){
  body.page-glass .hero-inner::before{
    -webkit-backdrop-filter:none;backdrop-filter:none;
    background:radial-gradient(ellipse at center,rgba(10,17,40,.88) 0%,rgba(10,17,40,.72) 46%,rgba(10,17,40,0) 86%);
  }
}
@media print{
  body.page-glass .hero-inner::before{display:none}
  .zelle-tile img{max-width:300px}
}

/* 24.4 The motto's own size was half the contrast problem. At 390px it clamped
   to 16.8px, and 16.8px sits just under the 18.66px bold threshold for WCAG
   large text - so the phone required 4.5:1 where the desktop required 3:1, for
   the same words. Raising the floor to 18.9px bold puts it in the large-text
   band at every width, which is the honest fix: the requirement follows the
   type size, so the type size is what should move. */
.motto{font-size:clamp(1.18rem,3.4vw,1.5rem)}
