/* Lifeline — app shell styles.
 *
 * Two layers of custom properties, on purpose:
 *
 *   1. BRAND tokens (--brand-*) are the church's fixed palette. They are
 *      declared once on :root and never redeclared anywhere.
 *   2. SURFACE tokens (--surface, --ink, --line, --gloss, --elev-*) are
 *      *relative to whatever you are sitting on*. Every component styles itself
 *      with these and only these, so a container can re-declare the surface —
 *      `.paper` turns a dark navy region into the warm reading page — and
 *      everything inside it, including components written months later, adapts
 *      without a single component-level override.
 *
 * DESIGN.md §5: dark navy shell, warm light reading surface, ONE red accent.
 * Red is load-bearing (the lifeline pulse). It is never used for decoration.
 *
 * ---------------------------------------------------------------------------
 * DEPTH BUDGET — read before adding an effect
 * ---------------------------------------------------------------------------
 * The congregation is on cheap Android phones over church wifi. Depth here is
 * bought with things the compositor already does for free:
 *
 *   layered gradients · static box-shadows · transform · opacity
 *
 * Nothing animates width, height, top/left, box-shadow or background-position.
 * `filter`/`backdrop-filter` appear exactly twice (top bar, tab bar) on small
 * fixed strips, never on a scrolling list. Every animation is escapable through
 * prefers-reduced-motion at the bottom of this file.
 */

/* ===================================================================
   Tokens
   =================================================================== */
:root {
  /* Brand — fixed. */
  --brand-shell: #0B1A3F;
  --brand-navy:  #13275C;
  --brand-blue:  #1871AD;
  --brand-sky:   #8DCEF9;
  --brand-pulse: #B03020;
  --brand-paper: #F6F2EA;

  /* Derived depth anchors. Not new brand colours — the shell navy pushed
     further down and further up, so a gradient has somewhere to fall off to. */
  --brand-void:  #050E27;
  --brand-lift:  #1B3474;

  /* Surface — the dark chrome is the default surface. */
  --surface:     var(--brand-shell);
  --surface-2:   var(--brand-navy);
  --surface-3:   #1A3172;
  --ink:         #EAF1FB;
  --ink-dim:     #A9BBD8;
  --ink-faint:   #7288AE;
  --line:        rgba(141, 206, 249, 0.16);
  --accent:      var(--brand-sky);
  --accent-ink:  var(--brand-shell);
  --focus:       var(--brand-sky);

  /* Dimensional kit. --gloss is the "light from above" that makes a flat panel
     read as a physical surface; --hairline is the lit top edge of that surface;
     the --elev ramp is a close contact shadow plus a wide ambient one, which is
     what separates a real object from a drop-shadowed rectangle. */
  --gloss:       linear-gradient(180deg, rgba(255,255,255,.075), rgba(255,255,255,0) 46%);
  --hairline:    rgba(255, 255, 255, .10);
  --elev-1:      0 1px 2px rgba(0,0,0,.30), 0 4px 12px -6px rgba(0,0,0,.50);
  --elev-2:      0 2px 4px rgba(0,0,0,.28), 0 16px 32px -16px rgba(0,0,0,.62);
  --elev-3:      0 6px 14px rgba(0,0,0,.30), 0 34px 64px -26px rgba(0,0,0,.72);
  --shadow:      var(--elev-2);      /* legacy alias */

  --radius:      16px;
  --radius-sm:   11px;
  --radius-xs:   8px;
  --tap:         44px;              /* minimum touch target */
  --pad:         clamp(14px, 4vw, 22px);
  --maxw:        720px;

  /* Rhythm. One scale, used everywhere, so vertical spacing is a decision
     instead of whatever the last person typed. */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 22px; --s6: 30px; --s7: 44px;

  /* Type scale. Ratio ~1.18 at the body end, opening up at the display end so a
     heading has somewhere to be big without a jump. */
  --t-xs:  .74rem;
  --t-sm:  .86rem;
  --t-md:  1rem;
  --t-lg:  1.09rem;
  --t-xl:  1.3rem;
  --t-2xl: clamp(1.45rem, 1.16rem + 1.5vw, 1.85rem);
  --t-3xl: clamp(1.72rem, 1.3rem + 2.2vw, 2.3rem);

  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial,
          "Noto Sans", sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --ease:     cubic-bezier(.2, .8, .3, 1);
  --ease-in:  cubic-bezier(.4, 0, .8, .3);

  --safe-top:    env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);

  /* The pulse, as artwork. One SVG, two tints — the app's signature mark shows
     up as a section rule, a card watermark and the splash trace, so the same
     line is doing the same job everywhere instead of three lookalike shapes.
     Inline data URIs: no extra request, works offline, no build step. */
  --ekg-red: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 104 16'%3E%3Cpath d='M0 8h28l3.5-5.4 3 11.4 4.6-14 3.6 19 3.4-11h4.4L54 8h50' fill='none' stroke='%23B03020' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --ekg-sky: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 104 16'%3E%3Cpath d='M0 8h28l3.5-5.4 3 11.4 4.6-14 3.6 19 3.4-11h4.4L54 8h50' fill='none' stroke='%238DCEF9' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* The reading surface. Anything inside `.paper` becomes light-on-warm without
   knowing it moved — including its light, its edge and its shadow. */
.paper {
  --surface:    var(--brand-paper);
  --surface-2:  #FFFFFF;
  --surface-3:  #ECE5D8;
  --ink:        #14203C;
  --ink-dim:    #4E5A73;
  --ink-faint:  #7C879B;
  --line:       rgba(11, 26, 63, 0.14);
  --accent:     var(--brand-blue);
  --accent-ink: #FFFFFF;
  --focus:      var(--brand-blue);
  --gloss:      linear-gradient(180deg, rgba(255,255,255,.9), rgba(255,255,255,0) 54%);
  --hairline:   rgba(255, 255, 255, .95);
  --elev-1:     0 1px 2px rgba(11,26,63,.10), 0 6px 14px -8px rgba(11,26,63,.22);
  --elev-2:     0 2px 5px rgba(11,26,63,.10), 0 18px 34px -18px rgba(11,26,63,.34);
  --elev-3:     0 8px 18px rgba(11,26,63,.14), 0 36px 60px -28px rgba(11,26,63,.42);
  --shadow:     var(--elev-2);
}

/* ===================================================================
   Base
   =================================================================== */
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  background: var(--brand-shell);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
  overscroll-behavior-y: none;
}

h1, h2, h3 {
  margin: 0 0 .4em;
  line-height: 1.16;
  font-weight: 700;
  letter-spacing: -.014em;
  text-wrap: balance;          /* Spanish runs long; break it where it reads */
}
h1 { font-size: var(--t-2xl); letter-spacing: -.02em; }
h2 { font-size: var(--t-xl); }
h3 { font-size: var(--t-lg); }
p  { margin: 0 0 .85em; }

a { color: var(--accent); }

/* The one structural device that repeats: a small caps-tracked label naming the
   context a block belongs to. It is never decorative — if it does not name
   something true, it does not get one. */
.eyebrow {
  margin: 0 0 var(--s2);
  font-size: var(--t-xs);
  font-weight: 700;
  /* .12em, not .15em: "IGLESIA BAUTISTA LIFELINE" is 25 characters and at the
     wider tracking it reached the exact edge of a 360px screen. The label that
     names the church is the last thing that should be flirting with a clip. */
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

.noscript {
  position: relative;
  z-index: 2;
  max-width: 34rem;
  margin: 3rem auto;
  padding: var(--pad);
  background: var(--brand-paper);
  color: #14203C;
  border-radius: var(--radius);
}

/* ===================================================================
   Ambient background — the depth field the whole app sits in
   ===================================================================
   Three stacked layers, all fixed, all behind everything, none of them
   interactive:

     .ambient        static layered radial gradients (the "room")
     .ambient-drift  two soft lights that drift on transform only
     #ambient-canvas an optional low-density constellation (app.js decides)
     .ambient-veil   a navy scrim that guarantees text contrast no matter what
                     the canvas happens to draw underneath a paragraph

   The veil is not optional. Without it, a bright particle passing under body
   copy costs contrast at exactly the moment somebody is reading a prayer
   request on a phone in sunlight.
*/
.ambient {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  background-color: var(--brand-shell);
  background-image:
    radial-gradient(90% 55% at 50% -8%,  rgba(27, 52, 116, .85) 0%, rgba(27, 52, 116, 0) 68%),
    radial-gradient(70% 45% at 8% 22%,   rgba(24, 113, 173, .30) 0%, rgba(24, 113, 173, 0) 70%),
    radial-gradient(85% 50% at 98% 76%,  rgba(19, 39, 92, .85) 0%,  rgba(19, 39, 92, 0) 72%),
    radial-gradient(120% 90% at 50% 112%, rgba(5, 14, 39, .95) 0%,  rgba(5, 14, 39, 0) 60%);
}

.ambient-drift {
  position: absolute;
  top: -30%;
  left: -25%;
  width: 150%;
  height: 150%;
  opacity: .55;
  background-image:
    radial-gradient(28% 22% at 30% 34%, rgba(141, 206, 249, .16) 0%, rgba(141, 206, 249, 0) 68%),
    radial-gradient(24% 20% at 72% 62%, rgba(24, 113, 173, .22) 0%, rgba(24, 113, 173, 0) 70%);
  will-change: transform;
  animation: drift 68s var(--ease) infinite alternate;
}

/* A second, slower light going the other way. Two crossing drifts read as a
   living room; one reads as a slider. */
.ambient-drift-b {
  opacity: .4;
  background-image:
    radial-gradient(30% 24% at 66% 24%, rgba(176, 48, 32, .10) 0%, rgba(176, 48, 32, 0) 66%),
    radial-gradient(26% 22% at 22% 78%, rgba(141, 206, 249, .12) 0%, rgba(141, 206, 249, 0) 70%);
  animation: drift-b 94s var(--ease) infinite alternate;
}

#ambient-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: .9;
}

.ambient-veil {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(11, 26, 63, .55) 0%, rgba(11, 26, 63, .18) 30%,
                            rgba(11, 26, 63, .22) 70%, rgba(11, 26, 63, .62) 100%);
}

@keyframes drift {
  from { transform: translate3d(-3%, -2%, 0) scale(1); }
  to   { transform: translate3d(4%, 3%, 0) scale(1.12); }
}
@keyframes drift-b {
  from { transform: translate3d(3%, 2%, 0) scale(1.08); }
  to   { transform: translate3d(-4%, -3%, 0) scale(1); }
}

/* ===================================================================
   Splash — the EKG reveal
   ===================================================================
   The identity moment, and the only place the app is allowed to be theatrical.
   Depth comes from four parallax planes that move at different rates:

     rings   → slowest, furthest back
     trace   → the pulse drawing itself under the plate
     plate   → the logo on its white plate, the subject
     bloom   → the glow the pulse throws as it passes, closest to the viewer

   Everything is clip-path / transform / opacity. No width, no left, no
   box-shadow keyframes: those relayout or repaint every frame and stutter on
   the cheap Android phones a lot of this congregation carries.
*/
.splash {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  overflow: hidden;
  background-color: var(--brand-shell);
  /* Identical gradient stack to .ambient, so when the splash fades the field
     behind it is already the same room — a dissolve, not a cut. */
  background-image:
    radial-gradient(90% 55% at 50% -8%,  rgba(27, 52, 116, .85) 0%, rgba(27, 52, 116, 0) 68%),
    radial-gradient(70% 45% at 8% 22%,   rgba(24, 113, 173, .30) 0%, rgba(24, 113, 173, 0) 70%),
    radial-gradient(85% 50% at 98% 76%,  rgba(19, 39, 92, .85) 0%,  rgba(19, 39, 92, 0) 72%),
    radial-gradient(120% 90% at 50% 112%, rgba(5, 14, 39, .95) 0%,  rgba(5, 14, 39, 0) 60%);
  opacity: 1;
  transform: scale(1);
  transition: opacity 420ms ease, transform 420ms var(--ease-in);   /* FADE_MS */
}
/* The hand-off: the splash does not blink out, it opens up and lets the app
   through — it grows very slightly as it fades, so the eye reads "moved
   closer", not "disappeared". */
.splash.done {
  opacity: 0;
  transform: scale(1.06);
  pointer-events: none;
}

.splash-field {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.splash-ring {
  position: absolute;
  width: min(88vw, 460px);
  aspect-ratio: 1;
  border: 1px solid rgba(141, 206, 249, .13);
  border-radius: 50%;
  opacity: 0;
  animation: ring-out 2600ms var(--ease) both;
}
.splash-ring-2 { animation-delay: 320ms; border-color: rgba(141, 206, 249, .10); }
.splash-ring-3 { animation-delay: 640ms; border-color: rgba(176, 48, 32, .16); }

.splash-stage {
  position: relative;
  display: grid;
  justify-items: center;
  gap: clamp(14px, 4vw, 22px);
  animation: stage-in 520ms var(--ease) both;
}

.splash-plate {
  position: relative;
  z-index: 2;
  overflow: hidden;
  width: min(78vw, 420px);
  padding: clamp(16px, 5vw, 26px);
  background: #FFFFFF;              /* the white plate. Never recolour the logo. */
  background-image: linear-gradient(170deg, #FFFFFF 0%, #FFFFFF 58%, #F1EDE6 100%);
  border-radius: 20px;
  box-shadow:
    0 1px 0 rgba(255,255,255,.9) inset,
    0 2px 6px rgba(0,0,0,.30),
    0 26px 60px -18px rgba(0,0,0,.62),
    0 0 0 1px rgba(11,26,63,.06);
  animation: plate-in 620ms var(--ease) both;
}

.splash-logo {
  display: block;
  width: 100%;
  height: auto;
  /* Hidden at the start, revealed left-to-right as the pulse passes over it,
     as if the logo's own EKG line were drawing it. */
  clip-path: inset(0 100% 0 0);
  animation: ekg-reveal 1100ms cubic-bezier(.32, .06, .28, 1) 180ms both;
}

/* Full-width element carrying a thin red line at its LEFT edge, so a single
   translateX(100%) walks that line across the whole plate. Translating a 3px
   box by a percentage of 3px would go nowhere — this is the trick that keeps
   the sweep on the compositor. */
.splash-sweep {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 100%;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    rgba(176, 48, 32, 0)   0,
    rgba(176, 48, 32, .18) 0,
    rgba(176, 48, 32, .18) 3px,
    var(--brand-pulse)     3px,
    var(--brand-pulse)     6px,
    rgba(176, 48, 32, 0)   7px
  );
  transform: translateX(-8px);
  opacity: 0;
  animation: ekg-sweep 1100ms cubic-bezier(.32, .06, .28, 1) 180ms both;
}

/* The bloom the pulse throws. A *static* radial gradient being translated on
   the compositor — never an animated box-shadow. It sits behind the plate and
   overhangs it top and bottom, so as the sweep crosses the logo the glow spills
   out around the plate's edges onto the navy. Inside the plate it would be
   invisible; the plate is white. */
.splash-bloom {
  position: absolute;
  z-index: 1;
  top: -55%;
  bottom: -30%;
  left: -30%;
  width: 60%;
  pointer-events: none;
  background: radial-gradient(closest-side,
    rgba(255, 128, 102, .55) 0%,
    rgba(196, 62, 44, .34) 38%,
    rgba(176, 48, 32, 0) 76%);
  transform: translateX(0);
  opacity: 0;
  animation: ekg-bloom 1100ms cubic-bezier(.32, .06, .28, 1) 180ms both;
}

/* The pulse continues past the plate: a hairline EKG under the mark that draws
   itself in step with the sweep. This is the moment that says "lifeline". */
.splash-trace {
  width: min(78vw, 420px);
  height: 18px;
  background: var(--ekg-red) center / auto 18px repeat-x;
  opacity: .5;
  clip-path: inset(0 100% 0 0);
  animation: ekg-reveal 1400ms cubic-bezier(.32, .06, .28, 1) 260ms both;
}

@keyframes plate-in {
  from { opacity: 0; transform: scale(.9) translateY(10px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes stage-in {
  from { transform: scale(.98); }
  to   { transform: scale(1); }
}
@keyframes ring-out {
  0%   { opacity: 0; transform: scale(.55); }
  22%  { opacity: 1; }
  100% { opacity: 0; transform: scale(1.5); }
}
@keyframes ekg-reveal {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}
@keyframes ekg-sweep {
  0%   { transform: translateX(-8px);  opacity: 0; }
  6%   { opacity: 1; }
  94%  { opacity: 1; }
  100% { transform: translateX(100%);  opacity: 0; }
}
@keyframes ekg-bloom {
  0%   { transform: translateX(0);    opacity: 0; }
  12%  { opacity: 1; }
  86%  { opacity: .85; }
  100% { transform: translateX(215%); opacity: 0; }
}

/* Vestibular safety: show the finished state instantly. The splash still
   dismisses on the same timer, so nothing about the flow changes. */
@media (prefers-reduced-motion: reduce) {
  .splash, .splash-plate, .splash-logo, .splash-sweep,
  .splash-bloom, .splash-trace, .splash-stage, .splash-ring { animation: none !important; }
  .splash { transition: opacity 1ms linear; transform: none; }
  .splash.done { transform: none; }
  .splash-logo, .splash-trace { clip-path: none; }
  .splash-sweep, .splash-bloom, .splash-field { display: none; }
}

/* ===================================================================
   Shell layout
   =================================================================== */
.shell {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}
/* The app arrives rather than appears — it takes over the motion the splash was
   already carrying instead of starting from a standstill. Runs once, when boot
   unhides the shell. */
.shell:not([hidden]) { animation: shell-in 520ms var(--ease) both; }
@keyframes shell-in {
  from { opacity: 0; transform: translateY(10px) scale(.99); }
  to   { opacity: 1; transform: none; }
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 50;
  padding: 10px 14px;
  background: var(--brand-sky);
  color: var(--brand-shell);
  font-weight: 700;
  border-radius: 0 0 var(--radius-sm) 0;
}
.skip-link:focus {
  left: 0;
  top: var(--safe-top);
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: calc(var(--safe-top) + 10px) var(--pad) 10px;
  background: linear-gradient(180deg, rgba(9, 21, 51, .92) 0%, rgba(11, 26, 63, .74) 100%);
  -webkit-backdrop-filter: blur(14px) saturate(1.2);
  backdrop-filter: blur(14px) saturate(1.2);
  box-shadow: 0 1px 0 var(--hairline) inset;
}
/* The bar's bottom edge is the pulse rule in miniature: a short red segment,
   then a hairline. Same device as `.pulse-rule`, same meaning. */
.topbar::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: linear-gradient(90deg,
    var(--brand-pulse) 0, var(--brand-pulse) 46px,
    var(--line) 46px, var(--line) 100%);
}

/* The logo's navy script is invisible on navy — it always rides a white plate. */
.brand-plate {
  position: relative;
  display: flex;
  align-items: center;
  padding: 6px 10px;
  background: #FFFFFF;
  background-image: linear-gradient(170deg, #FFFFFF 0%, #FFFFFF 60%, #F1EDE6 100%);
  border-radius: var(--radius-xs);
  box-shadow:
    0 1px 0 rgba(255,255,255,.85) inset,
    0 1px 3px rgba(0,0,0,.34),
    0 8px 18px -10px rgba(0,0,0,.6);
}
.brand-plate img { display: block; width: auto; height: 26px; }

/* Signed out, the auth screen shows its own bigger plate — the bar keeps only
   the language toggle, which must stay reachable BEFORE anyone signs in. */
.shell.is-auth .topbar { background: transparent; -webkit-backdrop-filter: none; backdrop-filter: none; box-shadow: none; }
.shell.is-auth .topbar::after { display: none; }
.shell.is-auth .topbar > .brand-plate { display: none; }

.topbar-actions { margin-left: auto; display: flex; gap: var(--s2); }

.lang-toggle {
  min-width: var(--tap);
  min-height: 36px;
  padding: 0 14px;
  font: 700 var(--t-sm)/1 var(--font);
  letter-spacing: .06em;
  color: var(--brand-sky);
  background: linear-gradient(180deg, rgba(141,206,249,.14), rgba(141,206,249,.04));
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
  cursor: pointer;
  transition: transform 140ms var(--ease), background-color 140ms linear;
}
.lang-toggle:active { transform: scale(.95); background-color: rgba(141,206,249,.18); }

.status {
  min-height: 0;
  padding: 0 var(--pad);
  color: var(--ink-dim);
  font-size: var(--t-sm);
}
.status:not(:empty) {
  padding-top: var(--s2);
  padding-bottom: var(--s2);
  animation: status-in 260ms var(--ease) both;
}
.status.warn { color: #FFD9A8; }
@keyframes status-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* ===================================================================
   Security notice (#notice) — "somebody else changed your passcode"
   ===================================================================
   This is the one place the app raises its voice, so it gets the accent that
   means "attention" everywhere else in the palette: the pulse red, as a bar
   down the leading edge. It sits between the chrome and the view at every
   route, so it is styled as a band rather than a card — a card here would read
   as content and be scrolled past.
   Deliberately tolerant of markup: heading, paragraphs and a dismiss button
   are all optional, in any combination. */
.notice {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: var(--maxw);
  margin: var(--s3) auto 0;
  padding: var(--s4) var(--pad) var(--s4) calc(var(--pad) + 6px);
  border: 1px solid rgba(176, 48, 32, .48);
  border-radius: var(--radius);
  background-color: rgba(58, 20, 22, .55);
  background-image: linear-gradient(180deg, rgba(176, 48, 32, .26), rgba(176, 48, 32, .08));
  box-shadow: 0 1px 0 rgba(255,255,255,.10) inset, var(--elev-2);
  color: #FFE2DC;
  font-size: var(--t-sm);
  line-height: 1.5;
  animation: notice-in 300ms var(--ease) both;
}
.notice::before {
  content: "";
  position: absolute;
  left: 0;
  top: 10px;
  bottom: 10px;
  width: 4px;
  border-radius: 0 4px 4px 0;
  background: var(--brand-pulse);
  box-shadow: 0 0 12px rgba(176, 48, 32, .8);
}
.notice > :last-child { margin-bottom: 0; }
/* The urgent variant (the leader's claim ticket is still live) burns brighter
   and cannot be dismissed — app.js omits the button. */
.notice-urgent {
  border-color: rgba(224, 92, 70, .7);
  background-image: linear-gradient(180deg, rgba(196, 62, 44, .38), rgba(176, 48, 32, .14));
}
.notice-body { display: flow-root; }
.notice-body > :last-child { margin-bottom: 0; }
.notice h1, .notice h2, .notice h3, .notice .notice-title {
  margin: 0 0 var(--s2);
  font-size: var(--t-md);
  font-weight: 700;
  letter-spacing: .01em;
  color: #FFFFFF;
}
.notice p { margin: 0 0 var(--s3); }
.notice-text { overflow-wrap: anywhere; }
/* The line that asks for an action, not just the one that reports a fact. */
.notice-act { font-weight: 600; color: #FFFFFF; }
.notice-dismiss { margin-top: var(--s1); }
.notice button {
  min-height: 36px;
  padding: 0 16px;
  font: 700 var(--t-sm) var(--font);
  color: #FFE2DC;
  background: rgba(255, 255, 255, .10);
  border: 1px solid rgba(255, 226, 220, .34);
  border-radius: 999px;
  cursor: pointer;
  transition: transform 140ms var(--ease), background-color 140ms linear;
}
.notice button:active { transform: scale(.96); background-color: rgba(255,255,255,.18); }

/* The exact shape app.js emits: a text block, then an optional dismiss button.
   Side by side where there is room, stacked on a narrow phone — a 44px pill
   crushed against a three-line sentence on a 320px screen is how a banner ends
   up being tapped away by accident. */
.notice-body { flex: 1 1 260px; min-width: 0; }
.notice:has(.notice-dismiss) {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--s3);
}
.notice-dismiss { flex: 0 0 auto; align-self: center; }

/* While `claimOpen` is true the code the leader generated still opens the
   account. That is not a notification, it is a live credential in somebody
   else's hands, so it is louder and it carries a call to action. */
.notice-urgent {
  border-color: rgba(176, 48, 32, .78);
  background-image: linear-gradient(180deg, rgba(176, 48, 32, .40), rgba(176, 48, 32, .14));
}
.notice-act { font-weight: 700; color: #FFFFFF; }

.view {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  /* Decoration may bleed, the PAGE may not. `clip` rather than `hidden`: it
     clips without creating a scroll container, so nothing inside starts
     scrolling on its own and position:sticky keeps working. A 360px phone must
     never scroll sideways — that is the single most broken-feeling thing a
     layout can do, and a glow or a tilted tile is not worth it. */
  overflow-x: clip;
  padding: var(--pad);
  padding-bottom: calc(var(--pad) + 84px + var(--safe-bottom));
  outline: none;
}
.view:focus { outline: none; }

/* ===================================================================
   Tab bar
   =================================================================== */
.tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;
  display: flex;
  justify-content: center;
  gap: 2px;
  padding: 7px 6px calc(7px + var(--safe-bottom));
  background: linear-gradient(180deg, rgba(11, 26, 63, .82) 0%, rgba(7, 17, 43, .96) 100%);
  -webkit-backdrop-filter: blur(16px) saturate(1.25);
  backdrop-filter: blur(16px) saturate(1.25);
  box-shadow:
    0 1px 0 var(--hairline) inset,
    0 -12px 30px -18px rgba(0,0,0,.9);
  border-top: 1px solid var(--line);
}

.tab {
  position: relative;
  flex: 1 1 0;
  max-width: 120px;
  min-height: var(--tap);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 6px 2px;
  color: var(--ink-faint);
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
  font: 600 var(--t-xs)/1.1 var(--font);
  text-decoration: none;
  cursor: pointer;
  transition: color 160ms linear, transform 160ms var(--ease);
}
.tab svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 200ms var(--ease);
}
.tab-label {
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 100%;
}
.tab:active { transform: scale(.94); }

/* The selected tab is a lit panel, not just tinted text. */
.tab[aria-current="page"] {
  color: var(--brand-sky);
  background: linear-gradient(180deg, rgba(141,206,249,.16), rgba(141,206,249,.03));
  box-shadow: 0 1px 0 rgba(255,255,255,.10) inset;
}
.tab[aria-current="page"] svg { transform: translateY(-1px) scale(1.06); }
.tab[aria-current="page"] .tab-dot { opacity: 1; transform: scaleX(1); }
.tab-dot {
  width: 16px;
  height: 2px;
  border-radius: 2px;
  background: var(--brand-pulse);
  box-shadow: 0 0 8px rgba(176, 48, 32, .9);
  opacity: 0;
  transform: scaleX(.3);
  transition: opacity 160ms linear, transform 200ms var(--ease);
}

/* ===================================================================
   Dimensional surfaces
   ===================================================================
   A card is three things at once: a body colour, a light falling on it from
   above (--gloss on ::before), and a lit top edge (--hairline, inset). Take any
   one away and it flattens.
*/
.card {
  position: relative;
  isolation: isolate;
  background-color: var(--surface-2);
  background-image: linear-gradient(180deg, rgba(255,255,255,.045), rgba(0,0,0,.06));
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--pad);
  margin: 0 0 var(--s4);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-2);
}
.card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: var(--gloss);
  pointer-events: none;
}
.card > :last-child { margin-bottom: 0; }
.paper.card { background-image: linear-gradient(180deg, rgba(255,255,255,.6), rgba(11,26,63,.03)); }

.muted { color: var(--ink-dim); }
.faint { color: var(--ink-faint); font-size: var(--t-sm); }

.section-title {
  margin: 0 0 var(--s3);
}
.section-title .eyebrow { margin-bottom: var(--s1); }
.section-title h1 { margin: 0; }

.section-head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  margin: var(--s6) 0 var(--s3);
}
.section-head h2 { margin: 0; font-size: var(--t-lg); }
.section-head .rule {
  flex: 1 1 auto;
  height: 1px;
  background: var(--line);
}

/* The signature rule: the church's own EKG, then a hairline. It marks a
   section the way the logo marks the app. */
/* The pulse, then a HAIRLINE — the second layer is sized to 1px tall and
   offset past the mark. A 90deg colour-stop gradient here painted the full
   16px height instead, which reads as a grey slab bolted to the end of the
   heartbeat. */
.pulse-rule {
  height: 16px;
  border: 0;
  margin: 0 0 var(--s5);
  background-image:
    var(--ekg-red),
    linear-gradient(var(--line), var(--line));
  background-position: left center, 104px center;
  background-size: auto 16px, calc(100% - 104px) 1px;
  background-repeat: no-repeat, no-repeat;
}

.badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: .04em;
  background: var(--surface-3);
  color: var(--ink-dim);
  box-shadow: 0 1px 0 var(--hairline) inset;
}
.badge-role {
  background: linear-gradient(180deg, #C4432F, var(--brand-pulse));
  color: #fff;
  box-shadow: 0 1px 0 rgba(255,255,255,.25) inset, 0 2px 8px -3px rgba(176,48,32,.9);
}

/* ===================================================================
   Tilt — the "physical" press
   ===================================================================
   app.js writes --rx/--ry from where the finger landed; the element leans away
   from the touch by a few degrees and settles back. Transform only, so it costs
   the compositor one matrix and nothing else. The tilt is deliberately small:
   the goal is "this moved under my thumb", not a carnival.
*/
[data-tilt] {
  transform: perspective(760px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)) scale(var(--ts, 1));
  transition: transform 340ms var(--ease), box-shadow 340ms var(--ease);
  will-change: transform;
}
[data-tilt].is-pressed {
  transition-duration: 90ms;
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
}

/* ===================================================================
   Home — hero and tiles
   =================================================================== */
.hero {
  position: relative;
  overflow: hidden;
  padding: var(--s6) var(--pad) calc(var(--s6) + 14px);
  margin: 0 0 var(--s5);
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background-color: var(--brand-navy);
  background-image:
    radial-gradient(120% 90% at 12% 0%, rgba(24,113,173,.55) 0%, rgba(24,113,173,0) 62%),
    radial-gradient(90% 80% at 100% 100%, rgba(11,26,63,.9) 0%, rgba(11,26,63,0) 70%),
    linear-gradient(180deg, rgba(255,255,255,.06), rgba(0,0,0,.10));
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-3);
}
.hero h1 { font-size: var(--t-3xl); margin-bottom: var(--s3); }
.hero p { margin-bottom: 0; max-width: 46ch; }
/* The mark, running out of the card's bottom edge. Watermark weight — it is
   texture, not a second logo. */
.hero-trace {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -2px;
  height: 26px;
  background: var(--ekg-sky) left center / auto 26px repeat-x;
  opacity: .16;
  pointer-events: none;
}

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s3);
  margin: 0 0 var(--s4);
}
/* Three tiles in a two-column grid leaves a hole. The odd one out takes the
   whole row instead, so the block ends on a deliberate edge rather than a gap.
   Scoped to the two-column range — at three columns there is no leftover. */
@media (max-width: 779px) {
  .tiles > :last-child:nth-child(odd) { grid-column: 1 / -1; min-height: 0; }
  .tiles > :last-child:nth-child(odd) .tile-note { max-width: 34ch; }
}

.tile {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s4);
  min-height: 132px;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background-color: var(--surface-2);
  background-image: linear-gradient(165deg, rgba(255,255,255,.07), rgba(0,0,0,.10) 70%);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-2);
  color: var(--ink);
}
.tile::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: var(--gloss);
  pointer-events: none;
}
.tile-icon {
  width: 30px;
  height: 30px;
  fill: none;
  stroke: var(--brand-sky);
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 2px 6px rgba(141, 206, 249, .35));
}
.tile-name {
  font-size: var(--t-lg);
  font-weight: 700;
  letter-spacing: -.012em;
}
.tile-note {
  margin: 0;
  font-size: var(--t-sm);
  line-height: 1.4;
  color: var(--ink-dim);
}
.tile-flag {
  margin-top: auto;
  align-self: flex-start;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: var(--t-xs);
  font-weight: 700;
  color: var(--brand-sky);
  background: rgba(141, 206, 249, .12);
  border: 1px solid var(--line);
}

/* ===================================================================
   Forms / auth
   ===================================================================
   Signed out is the plainest screen in most apps and the first one anybody
   sees. Here it gets a place to stand: a halo behind the plate, the pulse
   under it, and the church's own name and town closing the page.
*/
.auth {
  position: relative;
  max-width: 440px;
  margin: 0 auto;
  padding-top: var(--s3);
}

.auth-head {
  position: relative;
  text-align: center;
  margin-bottom: var(--s5);
  padding-bottom: var(--s4);
}
/* The halo. A single static radial gradient, scaled and faded in — no filter,
   no repaint. It is what stops the white plate looking pasted onto navy.
   Sized to the container, never wider: a 130%-wide decoration here made the
   whole 360px page 395px wide and scroll sideways. `closest-side` means it has
   already faded to nothing well before the edges, so it reads as a halo
   without ever asking the page for room it should not have. */
.auth-head::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: -16%;
  aspect-ratio: 1.35;
  background: radial-gradient(closest-side,
    rgba(141, 206, 249, .26) 0%,
    rgba(24, 113, 173, .15) 45%,
    rgba(24, 113, 173, 0) 78%);
  pointer-events: none;
  z-index: 0;
  animation: halo-in 900ms var(--ease) both;
}
.auth-head > * { position: relative; z-index: 1; }
@keyframes halo-in {
  from { opacity: 0; transform: scale(.85); }
  to   { opacity: 1; transform: scale(1); }
}

.auth-head .brand-plate {
  display: inline-flex;
  margin-bottom: var(--s4);
  padding: 14px 20px;
  border-radius: 14px;
  box-shadow:
    0 1px 0 rgba(255,255,255,.9) inset,
    0 2px 6px rgba(0,0,0,.3),
    0 22px 46px -18px rgba(0,0,0,.66);
}
.auth-head .brand-plate img { height: 38px; }
.auth-head h1 { font-size: var(--t-2xl); margin-bottom: var(--s2); }
.auth-head p { margin: 0 auto; max-width: 34ch; }

/* The pulse closes the header block — the same mark as the splash trace, at
   rest. */
.auth-trace {
  height: 14px;
  margin: var(--s4) auto 0;
  width: 70%;
  background: var(--ekg-red) center / auto 14px no-repeat;
  opacity: .42;
}

.auth-foot {
  margin: var(--s6) 0 0;
  text-align: center;
  font-size: var(--t-xs);
  letter-spacing: .06em;
  color: var(--ink-faint);
}
.auth-foot span { display: block; }
.auth-foot span + span { margin-top: 3px; opacity: .75; }

.switcher {
  display: flex;
  gap: 4px;
  padding: 4px;
  margin: 0 0 var(--s4);
  background: rgba(0, 0, 0, .18);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: inset 0 2px 6px rgba(0,0,0,.30);
}
.paper .switcher { background: rgba(11,26,63,.05); box-shadow: inset 0 2px 5px rgba(11,26,63,.10); }
.switcher button {
  flex: 1;
  min-height: 38px;
  padding: 0 10px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  font: 600 var(--t-sm) var(--font);
  cursor: pointer;
  transition: color 160ms linear, transform 160ms var(--ease);
}
.switcher button:active { transform: scale(.97); }
/* Auth tabs use aria-selected (they are tabs); the language chooser uses
   aria-pressed (it is a pair of toggle buttons, not a tablist). Both are
   "the chosen one" to a reader, so both get the same paint. */
.switcher button[aria-selected="true"],
.switcher button[aria-pressed="true"] {
  background: linear-gradient(180deg, #2585C4, var(--brand-blue));
  color: #fff;
  box-shadow: 0 1px 0 rgba(255,255,255,.28) inset, 0 4px 12px -5px rgba(24,113,173,.95);
}

.field { margin: 0 0 var(--s4); }
.field label {
  display: block;
  margin-bottom: var(--s2);
  font-size: var(--t-sm);
  font-weight: 600;
  letter-spacing: .01em;
  color: var(--ink-dim);
}
.field input {
  width: 100%;
  min-height: var(--tap);
  padding: 11px 13px;
  font: 500 var(--t-md) var(--font);
  color: var(--ink);
  background-color: rgba(6, 15, 40, .45);
  background-image: linear-gradient(180deg, rgba(0,0,0,.16), rgba(255,255,255,.02));
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 2px 5px rgba(0,0,0,.32);
  transition: border-color 160ms linear, box-shadow 160ms linear;
}
.paper .field input {
  background-color: #fff;
  background-image: none;
  box-shadow: inset 0 2px 4px rgba(11,26,63,.08);
}
.field input:focus {
  border-color: rgba(141, 206, 249, .55);
  box-shadow: inset 0 2px 5px rgba(0,0,0,.32), 0 0 0 3px rgba(141, 206, 249, .18);
}
.field input::placeholder { color: var(--ink-faint); }
.hint { margin: var(--s2) 0 0; font-size: var(--t-xs); line-height: 1.45; color: var(--ink-faint); }
.field-code input {
  font-family: var(--mono);
  letter-spacing: .28em;
  text-transform: uppercase;
}
/* The wide tracking is for a six-character code, not for the hint text. At
   .28em, "CÓDIGO DE LA IGLESIA" runs off the end of its own field. */
.field-code input::placeholder {
  letter-spacing: .02em;
  text-transform: none;
}

/* Show/hide is an ICON, not a word.
   It used to be a text button, and "Show passcode" / "Mostrar la clave" simply
   does not fit beside a passcode field on a 360px phone — it rendered as
   "Show pas…" in English and "Mostrar l…" in Spanish, which is an ellipsis
   where a control's name should be. The sentence still exists and is still
   translated: it is the button's aria-label and its tooltip, so a screen
   reader and a long-press both read the real words in the member's own
   language. A fixed square also means neither language can ever change the
   input's usable width. */
.field-secret { position: relative; }
.field-secret input { padding-right: 52px; }
.field-secret .reveal {
  position: absolute;
  right: 6px;
  bottom: 6px;
  display: grid;
  place-items: center;
  width: 38px;
  height: 32px;
  padding: 0;
  color: var(--accent);
  background: rgba(141, 206, 249, .10);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  cursor: pointer;
}
.field-secret .reveal svg {
  width: 19px;
  height: 19px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.field-secret .reveal .eye-slash { opacity: 0; transition: opacity 120ms linear; }
.field-secret .reveal[aria-pressed="true"] .eye-slash { opacity: 1; }
.paper .field-secret .reveal { background: rgba(24,113,173,.08); }

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  width: 100%;
  min-height: var(--tap);
  padding: 13px 18px;
  font: 700 var(--t-md) var(--font);
  letter-spacing: .005em;
  color: #fff;
  background-color: var(--brand-blue);
  background-image: linear-gradient(180deg, #2A8DCC 0%, var(--brand-blue) 62%, #14639A 100%);
  border: 1px solid rgba(255, 255, 255, .12);
  border-radius: var(--radius-sm);
  box-shadow:
    0 1px 0 rgba(255,255,255,.30) inset,
    0 2px 4px rgba(0,0,0,.22),
    0 12px 26px -14px rgba(24, 113, 173, 1);
  cursor: pointer;
  transition: transform 150ms var(--ease), box-shadow 150ms var(--ease), filter 150ms linear;
}
.btn:active {
  transform: translateY(1px) scale(.99);
  box-shadow: 0 1px 0 rgba(255,255,255,.18) inset, 0 1px 3px rgba(0,0,0,.3);
  filter: brightness(.96);
}
.btn:disabled { opacity: .55; cursor: default; transform: none; }
.btn-ghost {
  color: var(--ink-dim);
  background-color: transparent;
  background-image: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,0));
  border-color: var(--line);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
}
.btn-danger {
  background-color: var(--brand-pulse);
  background-image: linear-gradient(180deg, #C74936 0%, var(--brand-pulse) 60%, #8E2417 100%);
  box-shadow:
    0 1px 0 rgba(255,255,255,.24) inset,
    0 2px 4px rgba(0,0,0,.22),
    0 12px 26px -14px rgba(176, 48, 32, 1);
}
.btn-row { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s2); }
.btn-row .btn { width: auto; flex: 1 1 150px; }

.form-error, .form-ok {
  position: relative;
  margin: 0 0 var(--s4);
  padding: 11px 13px 11px 15px;
  border-radius: var(--radius-sm);
  font-size: var(--t-sm);
  line-height: 1.45;
  animation: notice-in 240ms var(--ease) both;
}
.form-error::before, .form-ok::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  bottom: 8px;
  width: 3px;
  border-radius: 3px;
}
.form-error {
  background: linear-gradient(180deg, rgba(176,48,32,.22), rgba(176,48,32,.12));
  border: 1px solid rgba(176, 48, 32, .5);
  color: #FFD7D0;
}
.form-error::before { background: var(--brand-pulse); }
.paper .form-error { color: #7A1D12; }
.form-ok {
  background: linear-gradient(180deg, rgba(24,113,173,.22), rgba(24,113,173,.12));
  border: 1px solid rgba(141, 206, 249, .42);
}
.form-ok::before { background: var(--brand-sky); }
@keyframes notice-in {
  from { opacity: 0; transform: translateY(-5px); }
  to   { opacity: 1; transform: none; }
}

.alt-line { margin-top: var(--s4); text-align: center; color: var(--ink-dim); font-size: var(--t-sm); }
.linkish {
  background: none;
  border: 0;
  padding: 4px;
  color: var(--accent);
  font: 600 var(--t-sm) var(--font);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

/* One-time secret (a generated passcode shown once and never again). */
.secret-slip {
  margin: var(--s3) 0;
  padding: var(--s4);
  text-align: center;
  background: #fff;
  background-image: linear-gradient(170deg, #fff 0%, #fff 60%, #F1EDE6 100%);
  color: #14203C;
  border-radius: var(--radius-sm);
  box-shadow: 0 1px 0 rgba(255,255,255,.9) inset, var(--elev-3);
  font: 700 1.6rem/1.2 var(--mono);
  letter-spacing: .18em;
  word-break: break-all;
}

/* ===================================================================
   Profile
   =================================================================== */
.identity {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin-bottom: var(--s4);
}
/* The member's initial on a lit disc. Nobody has uploaded a photo (there is no
   upload yet), so an empty circle would be a hole — this is a real object made
   from data we already have. */
.identity-mark {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 58px;
  height: 58px;
  border-radius: 50%;
  background-image:
    radial-gradient(90% 90% at 30% 22%, rgba(141,206,249,.55) 0%, rgba(141,206,249,0) 60%),
    linear-gradient(180deg, #2A8DCC, #12558A);
  color: #fff;
  font: 700 1.5rem/1 var(--font);
  box-shadow:
    0 1px 0 rgba(255,255,255,.4) inset,
    0 10px 22px -10px rgba(24,113,173,.95),
    0 0 0 1px rgba(141,206,249,.22);
}
.identity-body { min-width: 0; }
.identity-body h2 {
  margin: 0 0 var(--s2);
  font-size: var(--t-xl);
  overflow-wrap: anywhere;
}
.chip-row { display: flex; flex-wrap: wrap; gap: var(--s2); }

/* ===================================================================
   Stubs and empty states (Phase 2+ tabs)
   ===================================================================
   An empty screen is an invitation, not an apology. It gets the same
   dimensional treatment as a full one so a member never lands on something
   that looks broken.
*/
.stub { text-align: left; overflow: hidden; }
.stub-medallion {
  position: relative;
  display: grid;
  place-items: center;
  width: 62px;
  height: 62px;
  margin-bottom: var(--s4);
  border-radius: 18px;
  background-image:
    radial-gradient(85% 85% at 28% 20%, rgba(141,206,249,.30) 0%, rgba(141,206,249,0) 62%),
    linear-gradient(180deg, rgba(26,49,114,.95), rgba(11,26,63,.95));
  box-shadow:
    0 1px 0 rgba(255,255,255,.16) inset,
    0 14px 28px -14px rgba(0,0,0,.85),
    0 0 0 1px var(--line);
}
.paper .stub-medallion {
  background-image:
    radial-gradient(85% 85% at 28% 20%, rgba(255,255,255,.9) 0%, rgba(255,255,255,0) 62%),
    linear-gradient(180deg, #FFFFFF, #ECE5D8);
}
.stub-icon {
  width: 30px;
  height: 30px;
  color: var(--brand-sky);
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.paper .stub-icon { color: var(--brand-blue); }
.stub-flag {
  display: inline-block;
  margin-bottom: var(--s2);
  padding: 3px 10px;
  border-radius: 999px;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--brand-sky);
  background: rgba(141, 206, 249, .12);
  border: 1px solid var(--line);
}
.stub-trace {
  height: 20px;
  margin-top: var(--s4);
  background: var(--ekg-sky) left center / auto 20px repeat-x;
  opacity: .14;
}
.stub-list { margin: var(--s3) 0 0; padding-left: 18px; color: var(--ink-dim); }
.stub-list li { margin: var(--s1) 0; }

.spinner {
  width: 18px;
  height: 18px;
  border: 2px solid rgba(255, 255, 255, .35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ===================================================================
   Element defaults — the ones the browser would otherwise decide
   ===================================================================
   `.field input` was the only styled input in the file, so every bare
   <textarea>, <input type=number|search> and <select> fell through to the UA
   sheet — which on Chrome means monospace at 13.3px. That is why the board's
   compose box rendered in a different typeface than the app around it.

   These are surface-relative, so the same textarea is dark inside the shell
   and warm inside `.paper` without anybody writing a variant. */
textarea,
select,
input[type="text"], input[type="number"], input[type="search"],
input[type="password"], input[type="email"], input[type="tel"] {
  font: 500 var(--t-md) var(--font);
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: 0 1px 0 var(--hairline) inset;
  padding: 12px 14px;
  width: 100%;
  max-width: 100%;
  -webkit-appearance: none;
  appearance: none;
}
textarea {
  display: block;
  resize: vertical;
  min-height: 84px;
  line-height: 1.5;
}
textarea:focus,
select:focus,
input[type="text"]:focus, input[type="number"]:focus, input[type="search"]:focus,
input[type="password"]:focus, input[type="email"]:focus, input[type="tel"]:focus {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: transparent;
}
textarea::placeholder, input::placeholder { color: var(--ink-faint); }

/* A number input's spinner is a 20px-wide tap hazard next to a Save button. */
input[type="number"] { -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

button { font-family: inherit; }

/* Screen-reader-only label. Present for every input that has no visible one. */
.sr-label {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ===================================================================
   Small shared parts
   =================================================================== */
.lead { font-size: var(--t-lg); line-height: 1.55; color: var(--ink-dim); max-width: 52ch; }

.loading {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s5) var(--s2);
  color: var(--ink-dim);
  font-size: var(--t-sm);
}

.btn-small {
  width: auto;
  min-height: 36px;
  padding: 7px 14px;
  font-size: var(--t-sm);
  border-radius: var(--radius-xs);
}
.btn-row .btn-small { flex: 0 0 auto; }

.badge-alert {
  background: linear-gradient(180deg, #C4432F, var(--brand-pulse));
  color: #fff;
  box-shadow: 0 1px 0 rgba(255,255,255,.25) inset;
}
.badge-pin {
  background: rgba(141, 206, 249, .18);
  color: var(--brand-sky);
  box-shadow: 0 1px 0 var(--hairline) inset;
}
.paper .badge-pin { background: rgba(24, 113, 173, .12); color: var(--brand-blue); }
.badge-link { text-decoration: none; cursor: pointer; }
.badge-link:active { transform: scale(.96); }

.chip-note {
  display: inline-flex;
  align-items: center;
  min-height: 30px;
  padding: 0 12px;
  border-radius: 999px;
  font-size: var(--t-xs);
  color: var(--ink-faint);
  background: var(--surface-3);
}

/* A checkbox and its words are ONE tap target, not two. */
.check {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  min-height: var(--tap);
  padding: var(--s2) 0;
  font-size: var(--t-sm);
  line-height: 1.45;
  color: var(--ink-dim);
  cursor: pointer;
}
.check input[type="checkbox"] {
  flex: 0 0 auto;
  width: 20px; height: 20px;
  margin-top: 1px;
  accent-color: var(--brand-blue);
}

/* The one place a caution is not an error: "this will be visible to the youth
   app too". Amber, never red — red is the pulse and stays load-bearing. */
.warn-box {
  position: relative;
  margin: 0 0 var(--s4);
  padding: var(--s4) var(--s4) var(--s4) calc(var(--s4) + 4px);
  border-radius: var(--radius-sm);
  border: 1px solid rgba(255, 190, 110, .45);
  background: rgba(255, 176, 74, .17);
  color: var(--ink);
  font-size: var(--t-sm);
  line-height: 1.5;
}
/* This is the only thing between a leader and an upload that surfaces in the
   youth app's library. It has to survive being glanced at. */
.warn-box::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 4px;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
  background: #E2A33C;
}
.warn-box b, .warn-box strong { color: #FFD9A8; }
.paper .warn-box b, .paper .warn-box strong { color: #7A4A0A; }
.warn-box > :last-child { margin-bottom: 0; }

.form-error-card {
  border-color: rgba(176, 48, 32, .45);
  background-image: linear-gradient(180deg, rgba(176,48,32,.16), rgba(176,48,32,.04));
}
.reasons { margin: var(--s3) 0 0; padding-left: 18px; color: var(--ink-dim); font-size: var(--t-sm); }
.reasons li { margin: var(--s1) 0; }

/* Home tiles — the meta line under a tile name. */
.tile-meta { display: block; margin-top: var(--s1); font-size: var(--t-xs); color: var(--ink-faint); }
.tile-flags { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s2); }

/* Splash ring 1 is the leader of the three — no delay. Its siblings carry
   their own offsets; without this rule the base class was doing that job by
   accident and a future edit to `.splash-ring` would have moved it. */
.splash-ring-1 { animation-delay: 0ms; }

/* ===================================================================
   Trivia
   ===================================================================
   The whole screen was unstyled markup, which is why "Level 1" and "1 of 10"
   read as "Level 11 of 10" and the choice keys read as "AActs": adjacent
   inline spans with no layout between them.

   Difficulty is the organising idea. `.tier-N` is on the chip INSIDE the card,
   so `:has()` hoists it to the card as --tier / --tier-soft and the top edge,
   the chip and the key badges all inherit one colour. Without :has() support
   the card simply stays sky — it degrades, it does not break.
   =================================================================== */
.trivia-bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  margin: 0 0 var(--s2);
}
.trivia-count { font-size: var(--t-sm); font-weight: 700; color: var(--ink-dim); letter-spacing: .02em; }
.trivia-points { font-size: var(--t-sm); color: var(--ink-faint); text-align: right; }

.progress {
  height: 6px;
  margin: 0 0 var(--s5);
  border-radius: 999px;
  background: rgba(11, 26, 63, .55);
  box-shadow: 0 1px 0 var(--hairline) inset;
  overflow: hidden;
}
/* The inline width is set by the renderer. Deliberately NOT transitioned —
   the depth budget forbids animating width. */
.progress > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--brand-blue), var(--brand-sky));
}

.trivia-card {
  --tier: var(--brand-sky);
  --tier-soft: rgba(141, 206, 249, .16);
  position: relative;
  overflow: hidden;
}
/* The lit top edge, in the difficulty colour. */
.trivia-card::after {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 3px;
  background: var(--tier);
  opacity: .9;
}
.trivia-card:has(.tier-1), .trivia-card:has(.tier-2) {
  --tier: #2E9BD6; --tier-soft: rgba(46, 155, 214, .14);
}
.trivia-card:has(.tier-3), .trivia-card:has(.tier-4) {
  --tier: #1871AD; --tier-soft: rgba(24, 113, 173, .14);
}
.trivia-card:has(.tier-5), .trivia-card:has(.tier-6) {
  --tier: #2F5AA8; --tier-soft: rgba(47, 90, 168, .14);
}
.trivia-card:has(.tier-7), .trivia-card:has(.tier-8) {
  --tier: #6B4A9E; --tier-soft: rgba(107, 74, 158, .14);
}
.trivia-card:has(.tier-9), .trivia-card:has(.tier-10) {
  --tier: var(--brand-pulse); --tier-soft: rgba(176, 48, 32, .12);
}

.trivia-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  margin: 0 0 var(--s3);
}
.tier {
  display: inline-flex;
  align-items: center;
  padding: 4px 12px;
  border-radius: 999px;
  font-size: var(--t-xs);
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--tier, var(--brand-blue));
  background: var(--tier-soft, rgba(24, 113, 173, .12));
  box-shadow: 0 0 0 1px var(--tier-soft, transparent) inset;
}
.trivia-meta .faint { flex: 0 0 auto; font-variant-numeric: tabular-nums; }

.trivia-q {
  margin: 0 0 var(--s5);
  font-size: var(--t-xl);
  line-height: 1.35;
  letter-spacing: -.01em;
  text-wrap: balance;
}

.choices { display: grid; gap: var(--s3); }

.choice {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  min-height: var(--tap);
  padding: 12px 14px;
  text-align: left;
  font: 600 var(--t-md) var(--font);
  color: var(--ink);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
  cursor: pointer;
  transition: transform 140ms var(--ease), border-color 140ms linear, opacity 140ms linear;
}
.choice:active:not(:disabled) { transform: translateY(1px) scale(.995); }
.choice:disabled { cursor: default; }

.choice-key {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px; height: 30px;
  border-radius: 9px;
  font-size: var(--t-sm);
  font-weight: 800;
  color: var(--tier, var(--brand-blue));
  background: var(--tier-soft, rgba(24, 113, 173, .12));
}
.choice-text { flex: 1 1 auto; min-width: 0; line-height: 1.4; }

/* Answered states. Colour AND a mark, never colour alone. */
.choice.is-right {
  border-color: rgba(37, 140, 80, .5);
  background: rgba(52, 168, 96, .14);
  color: #14602F;
}
.choice.is-right .choice-key { background: rgba(37, 140, 80, .2); color: #14602F; }
.choice.is-right::after { content: "✓"; margin-left: auto; font-weight: 800; color: #1B7A41; }
.choice.is-wrong {
  border-color: rgba(176, 48, 32, .5);
  background: rgba(176, 48, 32, .12);
  color: #7A1D12;
}
.choice.is-wrong .choice-key { background: rgba(176, 48, 32, .18); color: #7A1D12; }
.choice.is-wrong::after { content: "✕"; margin-left: auto; font-weight: 800; color: #A32A19; }
.choice.is-dim { opacity: .5; box-shadow: none; }
/* The optimistic state: pressed, answer not back from the server yet. */
.choice.is-picked { border-color: var(--tier); transform: scale(.995); }

.trivia-result {
  margin-top: var(--s5);
  padding: var(--s4);
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  box-shadow: 0 1px 0 var(--hairline) inset;
}
.trivia-result.is-right { background: rgba(52, 168, 96, .12); }
.trivia-result.is-wrong { background: rgba(176, 48, 32, .10); }
.trivia-verdict {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: 0 0 var(--s2);
  font-size: var(--t-lg);
  font-weight: 800;
}
.trivia-award {
  display: inline-flex;
  align-items: center;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: var(--t-sm);
  font-weight: 800;
  color: #fff;
  background: linear-gradient(180deg, #C4432F, var(--brand-pulse));
}
.trivia-key { margin: 0 0 var(--s2); font-weight: 700; }
.trivia-fact { margin: 0 0 var(--s2); color: var(--ink-dim); line-height: 1.55; }
.trivia-result .btn-row { margin-top: var(--s4); }

.trivia-done { text-align: center; }
.trivia-done h2 { margin: 0 0 var(--s3); font-size: var(--t-2xl); }
.trivia-done p { margin: 0 0 var(--s2); }

.review { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--s3); }
.review-row {
  position: relative;
  padding: var(--s4) var(--s4) var(--s4) calc(var(--s4) + 4px);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
}
.review-row::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 4px;
  border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}
.review-row.is-right::before { background: #34A860; }
.review-row.is-wrong::before { background: var(--brand-pulse); }
.review-q { margin: 0 0 var(--s1); font-weight: 700; line-height: 1.4; }
.review-fact { margin: var(--s2) 0 0; font-size: var(--t-sm); color: var(--ink-dim); line-height: 1.5; }

/* ===================================================================
   Board
   =================================================================== */
.board-bar {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: 0 0 var(--s4);
}
.board-bar p { flex: 1 1 auto; margin: 0; font-size: var(--t-sm); }

.compose .post-input { margin-bottom: var(--s2); }

.post-input {
  font: 500 var(--t-md) var(--font);
  line-height: 1.55;
}

.posts { display: grid; gap: var(--s4); }

.post {
  position: relative;
  padding: var(--pad);
  border-radius: var(--radius);
  background-color: var(--surface-2);
  background-image: var(--gloss);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
}
.post-reply {
  margin-left: var(--s5);
  border-left: 2px solid var(--line);
  border-radius: var(--radius-sm);
}

.post-head { display: flex; align-items: center; gap: var(--s3); margin: 0 0 var(--s3); }
.post-mark, .member-mark {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px; height: 38px;
  border-radius: 50%;
  font-size: var(--t-md);
  font-weight: 800;
  color: var(--brand-sky);
  background: linear-gradient(180deg, var(--brand-lift), var(--brand-navy));
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
}
.paper .post-mark, .paper .member-mark {
  color: #fff;
  background: linear-gradient(180deg, #2A8DCC, var(--brand-blue));
}
.post-who, .member-who { flex: 1 1 auto; min-width: 0; }
.post-author, .member-name {
  margin: 0;
  font-weight: 700;
  line-height: 1.25;
  overflow-wrap: anywhere;
}
.post-meta { margin: 2px 0 0; font-size: var(--t-xs); color: var(--ink-faint); }
.post-chips { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: var(--s1); justify-content: flex-end; }

.post-body {
  margin: 0;
  line-height: 1.62;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
}
/* Showing the author's own words instead of the translation. Marked, because
   the member asked for it and should be able to see that they got it. */
.post-body.is-original {
  padding-left: var(--s3);
  border-left: 2px solid var(--accent);
  color: var(--ink-dim);
  font-style: italic;
}

.post-note {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2);
  margin: 0 0 var(--s3);
  font-size: var(--t-xs);
  color: var(--ink-faint);
}
.post-note-text { flex: 1 1 200px; min-width: 0; line-height: 1.45; }
/* A machine translation says so, every time, in the reader's language. */
.post-note-machine {
  padding: 6px 10px;
  border-radius: var(--radius-xs);
  background: var(--surface-3);
}

.post-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  margin-top: var(--s4);
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
.post-form:not([hidden]) { display: block; margin-top: var(--s3); }
.post-form .btn-row { margin-top: var(--s2); }

.post-replies { margin-top: var(--s4); display: grid; gap: var(--s3); }
.post-replies-head {
  margin: 0;
  font-size: var(--t-xs);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Removed by a leader. The slot stays — a post that vanishes silently reads
   like a bug to the person who wrote it. */
.post-gone {
  background-image: none;
  border: 1px dashed var(--line);
  box-shadow: none;
  color: var(--ink-faint);
  font-style: italic;
}

/* Waiting on a leader's approval: the author's own copy, nobody else's. */
.post-waiting {
  margin-top: var(--s3);
  padding: var(--s3) var(--s4);
  border-radius: var(--radius-sm);
  background: rgba(255, 176, 74, .10);
  border: 1px solid rgba(255, 190, 110, .30);
  font-size: var(--t-sm);
  color: var(--ink-dim);
}
.post-waiting-title { margin: 0 0 var(--s1); font-weight: 700; color: var(--ink); }
.post-waiting > :last-child { margin-bottom: 0; }

/* ===================================================================
   Library
   =================================================================== */
.books {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--s4);
}
.book {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  text-decoration: none;
  color: inherit;
}
.book-cover {
  display: block;
  width: 100%;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  margin-bottom: var(--s2);
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-2);
}
/* No cover art: the spine treatment, not a grey box. */
.book-cover-blank {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s4);
  text-align: center;
  font-size: var(--t-lg);
  font-weight: 800;
  line-height: 1.25;
  color: var(--brand-sky);
  background:
    linear-gradient(90deg, rgba(0,0,0,.28) 0 6px, rgba(255,255,255,.06) 6px 9px, transparent 9px),
    linear-gradient(180deg, var(--brand-lift), var(--brand-navy));
  overflow: hidden;
}
.book-title { font-weight: 700; line-height: 1.3; overflow-wrap: anywhere; }
.book-author { font-size: var(--t-sm); color: var(--ink-dim); }
.book-meta { font-size: var(--t-xs); color: var(--ink-faint); }

.book-detail-top { display: flex; gap: var(--s4); margin-bottom: var(--s4); }
.book-detail-top .book-cover { flex: 0 0 108px; width: 108px; margin-bottom: 0; }
.book-facts { flex: 1 1 auto; min-width: 0; display: grid; gap: var(--s1); align-content: start; }

.page-row { display: flex; align-items: center; gap: var(--s3); margin-top: var(--s3); }
.page-input { flex: 0 0 96px; width: 96px; text-align: center; font-variant-numeric: tabular-nums; }

/* ===================================================================
   Points
   =================================================================== */
.big-number {
  margin: 0 0 var(--s1);
  font-size: clamp(2.6rem, 1.9rem + 3.4vw, 3.6rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
/* The security record on Profile: "somebody else changed your passcode, and
   you already dismissed the alert". A quiet left edge in the pulse, so it
   still reads as a security line weeks later — not a centred banner. */
.standing {
  border-left: 3px solid rgba(176, 48, 32, .55);
}
.standing .eyebrow { color: var(--brand-pulse); }

.ranks { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--s2); }
.rank-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: var(--tap);
  padding: var(--s2) var(--s4);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
}
.rank-no {
  flex: 0 0 auto;
  min-width: 2.6ch;
  font-size: var(--t-sm);
  font-weight: 800;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.rank-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.rank-pts {
  flex: 0 0 auto;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
/* The member's own row, wherever it lands. */
.rank-row.is-me {
  background-image: linear-gradient(90deg, var(--tier-soft, rgba(141,206,249,.16)), transparent 70%);
  box-shadow: 0 0 0 1px var(--accent) inset, var(--elev-1);
}
.rank-row.is-me .rank-name { font-weight: 700; }
/* "···" — the gap between the top of the board and where you actually are. */
.rank-gap {
  padding: var(--s1) var(--s4);
  text-align: center;
  letter-spacing: .3em;
  color: var(--ink-faint);
}
.ranks-me { margin-top: var(--s2); }

/* ===================================================================
   Admin — members, moderation, library
   ===================================================================
   The three-tier role model is visible here or it is not real: a leader sees
   set/not-set, never a passcode. The dot is the whole affordance, so it gets a
   shape as well as a colour. */
.admin-tabs { flex-wrap: wrap; }

.members { margin: 0; padding: 0; list-style: none; display: grid; gap: var(--s3); }
.member {
  padding: var(--s4);
  border-radius: var(--radius-sm);
  background-color: var(--surface-2);
  background-image: var(--gloss);
  box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-1);
}
.member-head { display: flex; align-items: center; gap: var(--s3); }
.member-chips { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: var(--s1); justify-content: flex-end; }
.member-acts { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s3); }
.member-slip:not([hidden]) { display: block; margin-top: var(--s3); }

.pc-dot {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--t-xs);
  font-weight: 700;
  color: var(--ink-dim);
}
.pc-dot::before {
  content: "";
  width: 9px; height: 9px;
  border-radius: 50%;
  background: currentColor;
}
.pc-set { color: #34A860; }
.pc-unset { color: #D9762F; }

.mod-post { border-left: 3px solid var(--brand-pulse); }

/* ===================================================================
   Per-route accents
   ===================================================================
   `#view` carries data-route (set in renderRoute). One hook, so a screen can
   have its own character without every component learning where it lives.
   Kept deliberately thin: an accent shift, not a second theme. */
#view[data-route="trivia"] { --accent: #2E9BD6; }
#view[data-route="points"] { --accent: #E2A33C; }
#view[data-route="board"]  { --accent: var(--brand-sky); }

@media (min-width: 780px) {
  .books { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
}

@media (max-width: 380px) {
  .book-detail-top { flex-direction: column; }
  .book-detail-top .book-cover { flex: 0 0 auto; width: 132px; }
  .trivia-bar { flex-direction: column; align-items: flex-start; gap: var(--s1); }
  .trivia-points { text-align: left; }
}

/* ===================================================================
   Motion preferences — one place, applies to everything above
   =================================================================== */
@media (prefers-reduced-motion: reduce) {
  .ambient-drift { animation: none !important; }
  #ambient-canvas { display: none !important; }
  .shell:not([hidden]) { animation: none !important; }
  .auth-head::before { animation: none !important; }
  .status:not(:empty), .form-error, .form-ok, .notice { animation: none !important; }
  .notice button:active { transform: none; }
  [data-tilt] { transform: none !important; transition: none !important; }
  .tab, .tab svg, .tab-dot, .btn, .lang-toggle, .switcher button {
    transition-duration: 1ms !important;
  }
  .btn:active, .tab:active, .lang-toggle:active, .switcher button:active { transform: none; }
  .choice { transition-duration: 1ms !important; }
  .choice:active, .choice.is-picked { transform: none !important; }
  .badge-link:active { transform: none; }
  .spinner { animation-duration: 2.4s; }
}

/* Hover states belong to devices that can hover. On a phone :hover sticks after
   a tap and leaves a tile looking selected. */
@media (hover: hover) and (pointer: fine) {
  .tile:hover, .card.is-interactive:hover {
    box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-3);
  }
  .tab:hover { color: var(--ink-dim); }
  .tab[aria-current="page"]:hover { color: var(--brand-sky); }
  .btn:hover { filter: brightness(1.06); }
  .lang-toggle:hover { background-color: rgba(141,206,249,.14); }
  .switcher button:hover { color: var(--ink); }
  .choice:hover:not(:disabled) { border-color: var(--tier); }
  .book:hover .book-cover { box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-3); }
  .rank-row:hover { box-shadow: 0 1px 0 var(--hairline) inset, var(--elev-2); }
}

/* Wider screens: the app is a phone app first, but a desk browser should not
   look broken on Sunday afternoon. */
@media (min-width: 780px) {
  .tabbar {
    position: sticky;
    max-width: var(--maxw);
    margin: 0 auto;
    border-radius: var(--radius) var(--radius) 0 0;
  }
  .view { padding-bottom: var(--pad); }
  .tiles { grid-template-columns: repeat(3, 1fr); }
}

/* Very narrow phones (360px and under is the target device). Nothing may clip:
   Spanish labels run ~20% longer than English and this is where they land. */
@media (max-width: 380px) {
  .tab { font-size: .64rem; }
  .tab svg { width: 20px; height: 20px; }
  .field-secret input { padding-right: 88px; }
  .identity-mark { width: 50px; height: 50px; font-size: 1.3rem; }
}

/* ---- Study (KERYGMA, proxied) -------------------------------------------
   The frame hosts a page this app does not own and cannot restyle, so the
   job here is only to give it a calm, correctly-sized hole to live in. */
.study-lede { margin: 0 0 14px; max-width: 46ch; }

.study-frame-wrap {
  position: relative;
  border: 1px solid var(--hairline, rgba(141, 206, 249, .22));
  border-radius: 14px;
  overflow: hidden;
  background: var(--paper, #F6F2EA);
  /* Fill what is left of the viewport under the header and above the tab bar.
     A fixed pixel height leaves either a stranded gap on a tall phone or a
     two-inch letterbox on a short one, and the proxied page scrolls itself. */
  height: calc(100dvh - var(--study-chrome, 260px));
  min-height: 380px;
}

.study-frame {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.study-loading {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 24px;
  text-align: center;
  background: var(--paper, #F6F2EA);
  color: var(--navy, #13275C);
}
.study-loading[hidden] { display: none; }
.study-loading p { margin: 0; max-width: 34ch; }
.tiny { font-size: .82rem; }
