/* ============================================================
   MyChristiana — shared stylesheet (used by /af/ and /en/)
   ============================================================ */

@view-transition { navigation: auto; } /* smooth cross-page fade, incl. AF/EN switch — no-ops where unsupported */

/* Cross-document view transition: swap the UA's flat 250ms linear
   crossfade for the site's own easing/timing, plus a small directional
   drift so the incoming page reads as arriving rather than just
   replacing the old one. ::view-transition-* are generated pseudo-
   elements outside the normal tree, so the blanket reduced-motion rule
   above (`* { animation-duration: 0.001ms !important }`) doesn't reach
   them — they need their own opt-out. */
::view-transition-group(root) {
  animation-duration: var(--motion-base);
  animation-timing-function: var(--ease);
}
::view-transition-old(root) {
  animation: mc-view-out var(--motion-base) var(--ease) both;
}
::view-transition-new(root) {
  animation: mc-view-in var(--motion-base) var(--ease) both;
}
@keyframes mc-view-out {
  to { opacity: 0; transform: translateY(-8px); }
}
@keyframes mc-view-in {
  from { opacity: 0; transform: translateY(8px); }
}
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation: none !important;
  }
}

/* Any element with its own author-origin `display:` rule (.btn, .comment-guest,
   .profile-form, etc.) beats the UA stylesheet's [hidden]{display:none} at
   equal specificity, because author rules always win over UA rules — the
   hidden attribute silently does nothing and the element stays visible.
   This bit repeatedly across the site (login button, comment prompts, a
   profile form, individual editor buttons) before each was patched one at
   a time; this global rule is the actual fix, restoring what [hidden] is
   supposed to mean everywhere, permanently. The !important is intentional
   and correct here — nothing should ever want a hidden element to show. */
[hidden] { display: none !important; }

:root {
  --bg: oklch(100% 0 0);
  --surface: oklch(100% 0 0);
  --surface-sunken: oklch(96.5% 0.006 60);
  --ink: oklch(20% 0.014 60);
  --ink-soft: oklch(40% 0.018 60);
  --label: oklch(48% 0.018 60);
  --border: oklch(90% 0.012 60);

  --accent: oklch(58% 0.13 35);
  --accent-dark: oklch(46% 0.13 35);
  --accent-soft: oklch(58% 0.13 35 / 0.12);
  --accent-soft-strong: oklch(58% 0.13 35 / 0.22);
  --on-accent: oklch(99% 0.006 60);

  --danger: oklch(56% 0.19 25);
  --danger-soft: oklch(56% 0.19 25 / 0.1);
  --success: oklch(58% 0.14 150);
  --success-soft: oklch(58% 0.14 150 / 0.12);

  /* Deliberately a distinct, punchier orange from --accent — the
     Supporter star needs to read as its own "verified" signal
     wherever it appears, not blend in as just more brand chrome. */
  --supporter-gold: oklch(72% 0.18 55);
  --supporter-gold-dark: oklch(56% 0.17 50);

  --font-logo: 'Fredoka', system-ui, sans-serif;
  --font-display: 'Fraunces', Georgia, serif;
  --font-body: 'Inter', system-ui, -apple-system, sans-serif;

  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 20px;
  --shadow-card: 0 1px 2px oklch(20% 0.014 60 / 0.05), 0 8px 24px oklch(20% 0.014 60 / 0.06);
  --shadow-modal: 0 20px 60px oklch(20% 0.014 60 / 0.28);
  --container: 1120px;
  --header-h: 76px;

  --motion-fast: 140ms;
  --motion-base: 220ms;
  --ease: cubic-bezier(.2, .7, .3, 1);
}

* { box-sizing: border-box; }
/* Belt-and-suspenders against sideways scroll: any element that ever
   creeps past the viewport edge (an absolutely positioned dropdown, a
   flex child that doesn't wrap, a fixed-width embed) grows the page's
   scrollable width instead of just clipping — which reads as "the
   page rubber-bands right and doesn't come back." Nothing on the site
   is meant to scroll horizontally, so cutting it off here is safe.
   Only on html, deliberately not body too: setting overflow-x on an
   element forces its overflow-y to a used value of auto if it isn't
   already something other than visible (per the overflow spec), so
   setting this on BOTH would turn both html and body into their own
   independent vertical scroll containers — two nested scrollbars, one
   per element, which is exactly the double-scrollbar bug this used to
   cause. One element owning it is enough to block sideways scroll. */
html { overflow-x: hidden; }
html { color-scheme: light; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; display: block; }

a { color: var(--accent-dark); text-decoration-thickness: 1.5px; text-underline-offset: 2px; }
a:hover { color: var(--accent); }

.icon {
  width: 15px; height: 15px; flex-shrink: 0;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}

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

h1, h2, h3, h4 {
  font-family: var(--font-display); font-weight: 600;
  color: var(--ink); letter-spacing: -0.01em; text-wrap: balance; margin: 0 0 0.4em;
}
h1 { font-size: clamp(32px, 4.6vw, 48px); }
h2 { font-size: clamp(23px, 3vw, 30px); }
h3 { font-family: var(--font-body); font-weight: 700; font-size: 18px; }
p { margin: 0 0 1em; max-width: 62ch; }
.lede { font-size: 18px; color: var(--ink-soft); max-width: 58ch; }

.eyebrow {
  display: inline-block; font-family: var(--font-body);
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.11em; text-transform: uppercase;
  color: var(--accent-dark);
}

.container { width: 100%; max-width: var(--container); margin: 0 auto; padding: 0 24px; }
@media (max-width: 640px) { .container { padding: 0 20px; } }

:focus-visible { outline: 2.5px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
button { font-family: inherit; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

/* ---------------- Buttons ---------------- */
.btn {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-body); font-weight: 600; font-size: 15px;
  padding: 11px 20px; border-radius: 999px; border: 1.5px solid transparent;
  cursor: pointer; text-decoration: none; line-height: 1;
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-dark); color: var(--on-accent); }
.btn-ghost { background: transparent; border-color: var(--border); color: var(--ink); }
.btn-ghost:hover { border-color: var(--accent); color: var(--accent-dark); }
.btn-sm { padding: 7px 14px; font-size: 13.5px; }
.btn-hero { min-width: 200px; justify-content: center; }

/* Arrow glyph hidden by default, slides + fades in on hover/focus so
   the hero CTAs feel like they're inviting a click rather than just
   sitting there. Animating width (not just opacity/transform) is what
   lets it push in from zero instead of overlapping the label text. */
.btn-hero .btn-arrow {
  display: inline-flex; align-items: center; overflow: hidden;
  width: 0; opacity: 0; transform: translateX(-4px);
  transition: width 260ms var(--ease), opacity 200ms var(--ease), transform 260ms var(--ease);
}
.btn-hero .btn-arrow svg { width: 16px; height: 16px; flex-shrink: 0; }
.btn-hero:hover .btn-arrow, .btn-hero:focus-visible .btn-arrow {
  width: 16px; opacity: 1; transform: translateX(0);
}

/* ---------------- Loader (paint-drop logo reveal) ---------------- */
#page-loader {
  position: fixed; inset: 0; z-index: 200; background: var(--bg);
  display: flex; align-items: center; justify-content: center;
  transition: opacity 420ms var(--ease), visibility 420ms var(--ease);
}
#page-loader.is-hidden { opacity: 0; visibility: hidden; pointer-events: none; }
#page-loader .stage { width: min(70vw, 340px); transform-origin: center; }
#page-loader .stage.impact { animation: loaderImpact 300ms ease-out; }
@keyframes loaderImpact {
  0%   { transform: scale(1); }
  25%  { transform: scale(1.02) translateY(1px); }
  100% { transform: scale(1); }
}
#page-loader svg { width: 100%; height: auto; display: block; overflow: visible; }
#page-loader #wordmark { font-family: var(--font-logo); font-weight: 600; fill: var(--ink); }
#page-loader #dot { fill: var(--accent); }
#page-loader #puddle { fill: var(--accent); opacity: 0; }

/* ---------------- Header (glass, fixed look — no scroll state) ----------------
   Same floating three-pill glass treatment on every page, no
   scroll-triggered state change. */
.site-header {
  position: sticky; top: 0; z-index: 40;
  background: none; border-bottom: none; box-shadow: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
}

/* Three separate floating glass pills (logo, nav links, actions)
   instead of one continuous bar — the gaps between them stay
   transparent over whatever's behind the header (hero photo, or once
   the page scrolls past it, the page's own background), no
   scroll-triggered state. Same treatment on every page — this used to
   be homepage-only with a plain dark bar everywhere else, but that
   split is gone: nav and footer now look identical page to page. */
.site-header .brand,
.site-header .header-actions {
  /* Positioned with a z-index above .main-nav's mobile fullscreen
     overlay (z-index:41) — without this, these two pills are
     non-positioned (auto stacking level), which paints *behind* any
     positioned+z-indexed sibling regardless of where it sits on
     screen, so opening the mobile menu blanked them out even though
     the overlay starts below the header row. */
  position: relative; z-index: 42;
  height: 54px; border-radius: 999px;
  background: linear-gradient(180deg, oklch(97% 0.014 55 / 0.28), oklch(89% 0.026 50 / 0.36));
  backdrop-filter: blur(13px) saturate(200%);
  -webkit-backdrop-filter: blur(13px) saturate(200%);
  border: 1px solid oklch(100% 0 0 / 0.4);
  box-shadow: 0 8px 22px -14px oklch(20% 0.03 40 / 0.3), inset 0 1px 0 oklch(100% 0 0 / 0.45);
}
.site-header .brand { padding: 0 16px 0 10px; }
.site-header .header-actions { padding: 0 8px; }

/* .main-nav only gets the floating-pill treatment at desktop widths,
   where it's the horizontal link row. Below 900px it becomes the
   fullscreen menu (see the max-width:900px block further down) —
   leaving this unscoped clamped it to the pill's 54px height there
   too, so the open menu could only ever show its first row. */
@media (min-width: 901px) {
  .site-header .main-nav {
    height: 54px; border-radius: 999px; padding: 0 6px;
    background: linear-gradient(180deg, oklch(97% 0.014 55 / 0.28), oklch(89% 0.026 50 / 0.36));
    backdrop-filter: blur(13px) saturate(200%);
    -webkit-backdrop-filter: blur(13px) saturate(200%);
    border: 1px solid oklch(100% 0 0 / 0.4);
    box-shadow: 0 8px 22px -14px oklch(20% 0.03 40 / 0.3), inset 0 1px 0 oklch(100% 0 0 / 0.45);
  }
}

.site-header .brand-dot { width: 18px; height: 18px; }
.site-header .brand-word { font-size: 18px; color: var(--ink); }
.site-header .main-nav > a,
.site-header .nav-item > .dropdown-trigger { color: var(--ink); }
.site-header .main-nav > a:hover,
.site-header .nav-item > .dropdown-trigger:hover { color: var(--ink); }
.site-header .main-nav > a[aria-current="page"],
.site-header .nav-item > .dropdown-trigger[data-active="true"] { color: var(--accent-dark); }
.site-header .header-actions .btn-ghost {
  border-color: transparent; color: var(--ink);
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease),
    color var(--motion-fast) var(--ease), transform var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease);
}
.site-header .header-actions .btn-ghost:hover {
  background: oklch(0% 0 0 / 0.07); border-color: oklch(0% 0 0 / 0.1); color: var(--ink);
  transform: translateY(-1px); box-shadow: 0 4px 10px -6px oklch(20% 0.03 40 / 0.4);
}
.site-header .header-actions .btn-ghost:active { transform: translateY(0); box-shadow: none; }

/* Divider between the login button and the language switch, since the
   pill's own background is too transparent to separate them on its own. */
.site-header .lang-switch { border-color: transparent; margin-left: 8px; position: relative; }
.site-header .lang-switch::before {
  content: ''; position: absolute; left: -9px; top: 50%; transform: translateY(-50%);
  width: 1px; height: 20px; background: oklch(0% 0 0 / 0.14);
}
.site-header .lang-switch a:not(.is-active) { color: var(--ink); }
.site-header .lang-switch a:not(.is-active):hover { background: oklch(0% 0 0 / 0.06); }
.site-header .lang-switch a.is-active {
  background: var(--ink); color: var(--surface); box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.25);
}
.site-header .nav-toggle { border-color: oklch(0% 0 0 / 0.18); }
.site-header .nav-toggle svg { stroke: var(--ink); }

@media (max-width: 900px) {
  .site-header .main-nav > a,
  .site-header .nav-item > .dropdown-trigger { color: var(--ink-soft); }
  .site-header .main-nav > a:hover,
  .site-header .nav-item > .dropdown-trigger:hover { background: var(--surface-sunken); color: var(--ink); }

  /* Below the grid breakpoint the header is a plain flex row (brand
     pill on the left, actions pill on the right) with nothing between
     them to absorb slack. header-actions keeps flex-shrink:0 so its
     tappable controls (profile chip, language switch, hamburger) never
     get squeezed — so on narrow phones the brand pill has to be the
     one that gives, shrinking and ellipsis-truncating its wordmark
     instead of forcing the whole row wider than the viewport. */
  .site-header .brand { flex-shrink: 1; min-width: 0; }
  .site-header .brand-word { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

  /* No room for the display name or the dropdown chevron once the
     profile chip has to share the row with the brand pill, language
     switch and hamburger — dropping both is what keeps the brand
     wordmark from getting squeezed down to an ellipsis. The
     initials-avatar alone still carries the identity and stays tappable
     to open the dropdown. */
  .user-chip-name, .user-chip-trigger .chev { display: none; }

  /* The actions pill (avatar chip, lang switch, hamburger) is fixed-width
     and always wins the fight for space against the brand pill, which is
     the one side allowed to shrink — so every bit of slack trimmed here
     is slack given back to the wordmark instead of being lost to
     ellipsis. Nothing below drops under 44px of total tap target. */
  .site-header .container { gap: 10px; }
  .site-header .brand { padding: 0 12px 0 8px; gap: 8px; }
  .site-header .header-actions { padding: 0 6px; gap: 6px; }
  .user-chip > .user-chip-trigger { padding: 7px 9px; }
  .site-header .lang-switch { margin-left: 6px; }
}
.site-header .container {
  display: flex; align-items: center; justify-content: space-between;
  height: var(--header-h); gap: 16px;
}
/* Grid only once the full nav is on-screen (see the 900px breakpoint
   below) — it centers the nav between the logo and sign-in button.
   Below that, main-nav is hidden/absolute anyway, so plain flex keeps
   the logo and sign-in/hamburger from colliding on narrow phones. */
@media (min-width: 901px) {
  .site-header .container { display: grid; grid-template-columns: 1fr auto 1fr; }
  .site-header .brand { justify-self: start; }
  .site-header .main-nav { justify-self: center; }
  .site-header .header-actions { justify-self: end; }
}
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; flex-shrink: 0; }
.brand-dot { width: 24px; height: 24px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
.brand-word { font-family: var(--font-logo); font-weight: 600; font-size: 22px; color: var(--ink); letter-spacing: -0.01em; white-space: nowrap; }

.main-nav { position: relative; display: flex; align-items: center; gap: 2px; }
.main-nav > a, .nav-item > .dropdown-trigger {
  position: relative;
  display: inline-flex; align-items: center; gap: 5px;
  color: var(--ink-soft); text-decoration: none; font-weight: 600; font-size: 14.5px;
  padding: 9px 13px 13px; border-radius: 999px; background: transparent; border: none; cursor: pointer;
  transition: color 260ms var(--ease);
}
.main-nav > a:hover, .nav-item > .dropdown-trigger:hover { color: var(--ink); }
.main-nav > a[aria-current="page"], .nav-item > .dropdown-trigger[data-active="true"] { color: var(--accent-dark); }

/* One dot glides between items on hover and settles under the current
   page — a single flowing indicator instead of a per-item fill. */
.nav-indicator {
  position: absolute; bottom: 5px; left: 0; width: 5px; height: 5px; border-radius: 999px;
  background: var(--accent); opacity: 0; pointer-events: none; will-change: transform;
}

/* The dot travels via left/width/height/bottom, animated frame-by-frame
   by JS (not a CSS transition) so it can stretch into a thin flowing
   droplet spanning both items before easing back into a round dot at
   its destination — like surface tension pulling it back together
   rather than a bar sliding across. The press feedback (scaleX) still
   gets its own CSS transition below so squeezing on click eases in and
   out instead of snapping. */
.nav-indicator--stretch {
  width: 5px; height: 5px; bottom: 5px; transform: none;
  transition: opacity 220ms var(--ease), transform 160ms var(--ease);
}
.nav-indicator--stretch.is-pressed { width: 5px; transform: scaleX(1.6); }
.dropdown-trigger .chev { width: 13px; height: 13px; transition: transform var(--motion-fast) var(--ease); }
.nav-item.is-open .dropdown-trigger .chev { transform: rotate(180deg); }

.nav-item { position: relative; }
/* .dropdown-menu handles position/background/border/shadow and the
   open/close transition; .dropdown-menu-inner handles the actual link
   stack. Splitting them is what lets the mobile accordion (further
   down) animate its height via a single collapsing grid row instead of
   height:0 → auto, which can't transition. */
.dropdown-menu {
  position: absolute; top: calc(100% + 8px); left: 0; min-width: 190px; z-index: 30;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
  box-shadow: var(--shadow-card);
  opacity: 0; transform: translateY(-6px); visibility: hidden;
  transition: opacity var(--motion-fast) var(--ease), transform var(--motion-fast) var(--ease), visibility var(--motion-fast);
}
.nav-item.is-open .dropdown-menu { opacity: 1; transform: translateY(0); visibility: visible; }
.dropdown-menu-inner { display: flex; flex-direction: column; gap: 2px; padding: 6px; }
.dropdown-menu a,
.dropdown-menu .dropdown-menu-item {
  display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 12px; border-radius: var(--radius-sm);
  color: var(--ink); text-decoration: none; font-weight: 600; font-size: 14.5px; text-align: left;
  background: none; border: none; font-family: var(--font-body); cursor: pointer;
  transition: background var(--motion-fast) var(--ease);
}
.dropdown-menu a svg,
.dropdown-menu .dropdown-menu-item svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--ink-soft); }
.dropdown-menu a:hover,
.dropdown-menu .dropdown-menu-item:hover { background: var(--surface-sunken); }
.dropdown-menu a[aria-current="page"] { color: var(--accent-dark); background: var(--accent-soft); }

.header-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

/* ---------------- Notification bell ---------------- */
.notif-bell-trigger {
  position: relative; width: 40px; height: 40px; justify-content: center;
  border-radius: 50%; border: 1.5px solid oklch(20% 0.014 60 / 0.14); background: transparent; color: var(--ink);
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease),
    color var(--motion-fast) var(--ease), transform var(--motion-fast) var(--ease);
}
/* .nav-item > .dropdown-trigger's own padding (9px 13px 13px — extra
   bottom padding tuned for text+chevron triggers) beats a plain
   .notif-bell-trigger { padding: 0 } on specificity (two classes vs
   one) even though this rule comes later, which left the icon sitting
   2px above true centre inside the circle. Matching that selector's
   specificity here is what actually wins the override. */
.notif-bell .notif-bell-trigger { padding: 0; }
.notif-bell-trigger:hover { border-color: var(--accent); color: var(--accent-dark); background: var(--accent-soft); }
.notif-bell-trigger:active { transform: scale(0.93); }
.notif-bell-trigger svg { width: 19px; height: 19px; }
/* A little swing on the bell itself whenever unread arrives, so the
   badge isn't the only thing announcing new activity. Re-triggered by
   JS toggling the class off/on (see notifications.js) rather than a
   plain CSS animation, since it should only play once per new batch,
   not loop forever while unread sits there. */
@keyframes notifBellRing {
  0%, 100% { transform: rotate(0); }
  15% { transform: rotate(-13deg); } 30% { transform: rotate(10deg); }
  45% { transform: rotate(-7deg); } 60% { transform: rotate(4deg); } 75% { transform: rotate(-2deg); }
}
.notif-bell-trigger.is-ringing svg { animation: notifBellRing 620ms var(--ease); transform-origin: 50% 20%; }
@media (prefers-reduced-motion: reduce) { .notif-bell-trigger.is-ringing svg { animation: none; } }

.notif-badge {
  position: absolute; top: 4px; right: 5px; min-width: 9px; height: 9px; border-radius: 999px;
  background: var(--danger); border: 2px solid var(--surface); box-shadow: 0 0 0 1px oklch(56% 0.19 25 / 0.35);
}
/* Soft pulsing halo behind the plain dot (no count yet fetched / small
   unread count) — a quiet "something's new" cue that doesn't shout via
   motion the way the bell-ring does. Skipped once the badge carries a
   number since the halo would just look like a smudge behind text. */
.notif-badge::after {
  content: ''; position: absolute; inset: -5px; border-radius: inherit;
  background: var(--danger); opacity: 0.45; z-index: -1;
  animation: notifBadgePulse 2.2s var(--ease) infinite;
}
@keyframes notifBadgePulse {
  0% { transform: scale(0.6); opacity: 0.45; }
  70%, 100% { transform: scale(1.6); opacity: 0; }
}
.notif-badge.has-count {
  min-width: 17px; height: 17px; padding: 0 3px; font-size: 10px; font-weight: 800; color: #fff;
  display: flex; align-items: center; justify-content: center; top: 1px; right: 0px;
  background: linear-gradient(180deg, oklch(62% 0.2 25), var(--danger));
}
.notif-badge.has-count::after { content: none; }
@media (prefers-reduced-motion: reduce) { .notif-badge::after { animation: none; opacity: 0; } }

/* overflow:hidden is what keeps .notif-menu-head's flat top edge (it
   has its own background via border-bottom, not the menu's own
   rounded corner) from squaring off the dropdown's rounded corners —
   scoped to just this menu since the plain link-list dropdowns
   elsewhere don't have an internal header needing this. Bumped to the
   larger radius and a deeper, softer shadow than a plain nav dropdown
   so the panel reads as its own small surface, not just another menu. */
.notif-bell .dropdown-menu {
  right: 0; left: auto; top: calc(100% + 12px); width: 380px; max-width: calc(100vw - 32px); padding: 0; overflow: hidden;
  border-radius: var(--radius-lg); border-color: oklch(20% 0.014 60 / 0.08);
  box-shadow: 0 24px 60px -20px oklch(20% 0.03 40 / 0.35), 0 4px 16px -6px oklch(20% 0.03 40 / 0.18);
}
.notif-menu-head {
  display: flex; align-items: center; gap: 10px; padding: 16px 18px;
  background: linear-gradient(180deg, var(--surface-sunken), var(--surface));
  border-bottom: 1px solid var(--border);
}
.notif-menu-head span { font-weight: 700; font-size: 15px; color: var(--ink); letter-spacing: -0.01em; }
.notif-menu-head .notif-unread-count {
  min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px; background: var(--accent-soft-strong);
  color: var(--accent-dark); font-size: 11.5px; font-weight: 800; display: inline-flex; align-items: center; justify-content: center;
}
.notif-mark-all-btn {
  flex-shrink: 0; margin-left: auto; width: 32px; height: 32px; border-radius: 50%; border: none; background: transparent;
  color: var(--label); display: flex; align-items: center; justify-content: center; cursor: pointer;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease), transform var(--motion-fast) var(--ease);
}
.notif-mark-all-btn:hover { background: var(--accent-soft); color: var(--accent-dark); }
.notif-mark-all-btn:active { transform: scale(0.9); }
.notif-mark-all-btn svg { width: 16px; height: 16px; }
.notif-list { max-height: 380px; overflow-y: auto; padding: 6px; }

/* Rows read as soft cards on hover rather than a flat table — no
   divider lines; a slice of breathing room between them does the same
   job without the visual clutter, and it matches the dropdown's other
   menu items (.dropdown-menu a) more closely than the old bordered-row
   look did. */
.notif-item {
  position: relative;
  display: flex; gap: 12px; align-items: flex-start; padding: 12px 14px 12px 20px; text-decoration: none; color: inherit;
  border-radius: var(--radius-md); cursor: pointer;
  transition: background var(--motion-fast) var(--ease), transform var(--motion-fast) var(--ease);
  animation: notifRowIn 340ms var(--ease) backwards;
}
@media (prefers-reduced-motion: reduce) { .notif-item { animation: none; } }
@keyframes notifRowIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.notif-item:hover { background: var(--surface-sunken); }
.notif-item:active { transform: scale(0.99); }
/* Unread state is a small dot on the rail instead of tinting the whole
   row — the old full-row accent wash didn't age well once there were
   several unread rows stacked (a solid block of colour), and it fought
   with the per-type icon colour below for attention. */
.notif-item.is-unread::before {
  content: ''; position: absolute; left: 7px; top: 20px; width: 6px; height: 6px; border-radius: 999px; background: var(--accent);
}
.notif-item.is-unread .notif-item-message { font-weight: 600; }
.notif-item-icon {
  width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0;
  background: var(--surface-sunken); color: var(--ink-soft);
  display: flex; align-items: center; justify-content: center;
}
.notif-item-icon svg { width: 16px; height: 16px; }
/* Per-type tint so the icon rail gives a quick visual read of what
   happened before you even read the message — likes warm/accent,
   replies cooler, everything else stays neutral. */
.notif-item-icon.is-type-comment_like { background: var(--accent-soft); color: var(--accent-dark); }
.notif-item-icon.is-type-comment_reply { background: oklch(58% 0.13 240 / 0.14); color: oklch(46% 0.14 240); }
.notif-item-body { flex: 1; min-width: 0; padding-top: 2px; }
.notif-item-message { margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--ink); }
.notif-item-time { margin: 4px 0 0; font-size: 12px; color: var(--label); }

.notif-list .comments-empty {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 40px 20px 36px; text-align: center; color: var(--label); font-size: 13.5px; margin: 0;
}
.notif-list .comments-empty::before {
  content: ''; width: 44px; height: 44px; border-radius: 50%; background: var(--surface-sunken);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a8a196' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9'/%3E%3Cpath d='M13.73 21a2 2 0 0 1-3.46 0'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: center; background-size: 19px;
}
@media (max-width: 640px) {
  .notif-bell .dropdown-menu {
    position: fixed; top: calc(var(--header-h) + 8px); right: 10px; left: 10px; width: auto;
    max-height: calc(100vh - var(--header-h) - 24px); display: flex; flex-direction: column;
  }
  .notif-list { max-height: none; flex: 1; min-height: 0; }
}

/* Logged-in header chip: same look as the Community/About nav
   dropdowns (reuses .dropdown-trigger/.dropdown-menu), just with an
   avatar in place of a chevron-only label. The trigger gets its own
   even padding rather than inheriting the nav link's bottom-heavy
   9/13/13 (tuned for a text baseline) — with an avatar instead of a
   label that asymmetry just pushes the image visibly off-center
   against the language switch and hamburger next to it. */
.user-chip > .user-chip-trigger { padding: 8px 12px; }
.user-chip-avatar { width: 26px; height: 26px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: var(--surface-sunken); }
.user-chip-name { max-width: 110px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.supporter-star { width: 14px; height: 14px; color: var(--supporter-gold); flex-shrink: 0; margin-left: -2px; filter: drop-shadow(0 1px 1px oklch(20% 0.02 60 / 0.25)); }
.comment-author .supporter-star { width: 12px; height: 12px; margin-left: 3px; vertical-align: -1px; }

/* Corner badge on the avatar picture itself, for the spots where a
   name isn't sitting right next to it (profile page's own avatar,
   author page hero, admin author rows). */
.avatar-wrap { position: relative; display: inline-flex; flex-shrink: 0; }
.avatar-badge-star {
  position: absolute; right: -2px; bottom: -2px; width: 20px; height: 20px; border-radius: 50%;
  background: var(--supporter-gold); color: #fff; display: flex; align-items: center; justify-content: center;
  border: 2px solid var(--surface); box-shadow: 0 1px 3px oklch(20% 0.02 60 / 0.35);
}
.avatar-badge-star svg { width: 11px; height: 11px; }
.profile-avatar-wrap .avatar-badge-star { right: 28px; bottom: -2px; }
.user-chip .dropdown-menu { left: auto; right: 0; }

/* Plays once, the moment you sign in on a page that started out
   logged-out — a page load where the session was already there skips
   this entirely (see partials.js's authSettled flag) so the chip
   doesn't replay it on every navigation. scaleX from the right edge
   (not width, and not centered) is what makes it read as growing left
   out from where the login button used to be, and since transform
   doesn't affect layout, the lang switch/hamburger beside it never
   jump around while it plays. */
.user-chip--entering {
  transform: scaleX(0.4); transform-origin: right center; opacity: 0;
  transition: transform 420ms var(--ease), opacity 320ms var(--ease);
}
.user-chip--entering.is-visible { transform: scaleX(1); opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .user-chip--entering { transform: none; opacity: 1; transition: none; }
}

/* The shared mobile dropdown-trigger hover (var(--surface-sunken), see
   the 900px block above) is tuned for links sitting on the fullscreen
   menu's solid white sheet — on the glass header pill that same
   near-white fill reads as a stark, oddly-shaped flash on tap. The chip
   gets its own quieter press/open state instead, matching the tint the
   header's other buttons already use. */
.site-header .user-chip-trigger { -webkit-tap-highlight-color: transparent; }
.site-header .user-chip .user-chip-trigger:hover,
.site-header .user-chip .user-chip-trigger:focus-visible,
.site-header .user-chip .user-chip-trigger:active,
.site-header .user-chip.is-open .user-chip-trigger {
  background: oklch(0% 0 0 / 0.08);
}

.lang-switch { display: flex; align-items: center; border: 1.5px solid oklch(20% 0.014 60 / 0.14); border-radius: 999px; overflow: hidden; }
.lang-switch a { display: inline-block; padding: 7px 12px; font-size: 13px; font-weight: 700; line-height: 1; text-decoration: none; color: var(--label); letter-spacing: 0.02em; transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease); }
.lang-switch a.is-active { background: var(--ink); color: var(--surface); }
.lang-switch a:not(.is-active):hover { background: oklch(20% 0.014 60 / 0.08); }

.nav-toggle {
  display: none; background: transparent; border: 1.5px solid oklch(20% 0.014 60 / 0.14); border-radius: 10px;
  width: 42px; height: 42px; align-items: center; justify-content: center; cursor: pointer;
}
.nav-toggle svg { width: 20px; height: 20px; stroke: var(--ink); }

/* Hamburger morphs into an X: the outer bars slide onto the middle
   bar's line and rotate into place, the middle bar fades out. Each
   bar rotates around its own center (not the icon's), so translate
   and rotate have to travel together in one transform. */
.nav-toggle svg line {
  transition: transform 320ms var(--ease), opacity 200ms var(--ease);
  transform-box: fill-box; transform-origin: center;
}
.nav-toggle[aria-expanded="true"] svg .bar-1 { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] svg .bar-2 { opacity: 0; }
.nav-toggle[aria-expanded="true"] svg .bar-3 { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 900px) {
  /* The hover-glide dot only makes sense for the horizontal desktop
     nav where items sit side by side — in this stacked mobile panel
     there's nothing for it to glide between, so it was showing up as
     a stray dot floating under whichever link happened to be current. */
  .nav-indicator { display: none; }

  /* Fullscreen takeover sliding in from the left edge of the screen —
     position:fixed (not absolute) so it's anchored to the viewport, not
     the header, and covers the whole page below the bar rather than
     dropping down as a panel. Only transform/visibility animate;
     nothing needs a height collapse since fixed positioning already
     keeps it out of the page's flow. */
  .main-nav {
    position: fixed; top: var(--header-h); left: 0; right: 0; bottom: 0; z-index: 41;
    background: var(--surface);
    flex-direction: column; align-items: stretch;
    display: flex; gap: 4px; padding: 28px 28px 32px; overflow-y: auto;
    visibility: hidden; transform: translateX(-100%); pointer-events: none;
    transition: transform 420ms cubic-bezier(.2, .8, .2, 1), visibility 420ms;
  }
  .main-nav.is-open { visibility: visible; transform: translateX(0); pointer-events: auto; }
  /* Scoped to .main-nav specifically — the header's logged-in user chip
     also uses .nav-item/.dropdown-trigger (for the Community/About-style
     dropdown look) but lives in the compact .header-actions row, not the
     fullscreen nav, so it should keep its normal desktop-sized popover
     rather than inheriting this full-width/oversized mobile treatment. */
  .main-nav > a, .main-nav .nav-item > .dropdown-trigger { padding: 14px 4px; width: 100%; justify-content: space-between; font-size: 19px; }

  /* Accordion, animated via a single grid row collapsing between 0fr
     and 1fr rather than height:0 → auto (which snaps instead of
     transitioning, since "auto" isn't normally an animatable value). */
  .main-nav .dropdown-menu {
    position: static; box-shadow: none; border: none; opacity: 1; visibility: visible; transform: none;
    display: grid; grid-template-rows: 0fr; overflow: hidden;
    transition: grid-template-rows 380ms var(--ease);
  }
  .main-nav .nav-item.is-open .dropdown-menu { grid-template-rows: 1fr; }
  .main-nav .dropdown-menu-inner { min-height: 0; padding: 4px 0 10px 12px; }
  .main-nav .dropdown-menu a { font-size: 16.5px; padding: 10px 12px; }
  .nav-toggle { display: inline-flex; }
}
html.nav-locked { overflow: hidden; }

@media (max-width: 480px) {
  /* Not enough room for the full login label + lang switch + hamburger —
     drop to an icon-only login button so nothing gets pushed off-screen. */
  .header-actions { gap: 6px; }
  .header-actions .btn-label { display: none; }
  .header-actions .btn-ghost.btn-sm { padding: 9px; }
  .lang-switch a { padding: 7px 9px; }

  /* One more notch of breathing room reclaimed from the actions pill on
     the narrowest phones, on top of the 900px pass above — this is the
     width the brand wordmark was actually getting clipped at. */
  .container { padding: 0 16px; }
  .site-header .container { gap: 8px; }
  .site-header .brand { padding: 0 10px 0 8px; gap: 6px; }
  .site-header .brand-dot { width: 15px; height: 15px; }
  .site-header .header-actions { padding: 0 5px; gap: 4px; }
  .user-chip > .user-chip-trigger { padding: 6px 8px; }
  .user-chip-avatar { width: 24px; height: 24px; }
  .site-header .lang-switch { margin-left: 4px; }
  .lang-switch a { padding: 6px 8px; font-size: 12.5px; }
  .nav-toggle { width: 38px; height: 38px; }
}

@media (max-width: 360px) {
  /* Smallest phones (iPhone SE-class, 320-360 CSS px) still don't have
     room for the full wordmark next to a three-control actions pill —
     one more pass of the same trims, tight enough to matter at this
     width but still comfortably tappable. */
  .site-header .brand-word { font-size: 16.5px; }
  .site-header .header-actions { gap: 3px; padding: 0 4px; }
  .user-chip > .user-chip-trigger { padding: 6px 7px; }
  .lang-switch a { padding: 6px 7px; }
  .nav-toggle { width: 36px; height: 36px; }
}

/* ---------------- Page banner (image + glass header sits on top) ---------------- */
.page-banner {
  position: relative; margin-top: calc(-1 * var(--header-h));
  display: flex; align-items: flex-end; overflow: hidden; color: var(--on-accent);
}
.page-banner.is-tall { height: 100vh; height: 100svh; min-height: 560px; }
/* Bottom-anchored (see .page-banner above), so this height is really
   "how far the heading sits from the sticky header" — 260px read as
   cramped, the eyebrow landing right under the header pill with barely
   a gap. Every is-short page (about, advertising, calendar, news,
   profile) shares this one rule, so bumping it moves all of them down
   together and keeps them consistent with each other. */
.page-banner.is-short { height: 300px; }
/* Article featured-image header: taller than a generic interior banner
   (room for a real photo to read as a photo) but well short of the
   homepage's full-viewport hero, so the article's own content isn't
   pushed too far down the page. */
.page-banner.is-article { height: 440px; }
/* .page-banner's own display:flex (an author rule) otherwise beats the
   [hidden] attribute's default display:none, since author rules always
   win over the UA stylesheet regardless of specificity — same fix as
   the header's login button needed. Without this the no-image hero
   banner would render as an empty dark box on every article. */
.article-hero-banner[hidden] { display: none; }
.banner-art {
  position: absolute; left: 0; right: 0; top: 0; height: 100%; z-index: 0;
  background-image: url('https://sowdnrypzmthvqgjbykj.supabase.co/storage/v1/object/public/blog-images/pexels-tomfisk-10130181.jpg');
  background-size: cover; background-position: center;
  /* iOS Safari redraws full-bleed content on its own layer during the
     address-bar show/hide animation that scrolling triggers; a plain
     background-image left on the page's main layer gets caught in
     that redraw and visibly stutters/rescales for a frame or two.
     Promoting it to its own GPU layer keeps it compositing smoothly
     instead of repainting — this is what actually fixes the choppy
     "image grows as you scroll" glitch (the 100svh height above
     already stops the box itself from resizing). */
  transform: translateZ(0); -webkit-backface-visibility: hidden; backface-visibility: hidden;
}
/* Same job as .banner-art, but for a real <img> instead of a CSS
   background — used where the photo is per-item/dynamic (the article
   hero) rather than the one fixed stock photo every other banner uses. */
.banner-art-img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 0;
  transform: translateZ(0); -webkit-backface-visibility: hidden; backface-visibility: hidden;
}
.banner-scrim { position: absolute; inset: 0; z-index: 1; background: linear-gradient(180deg, oklch(20% 0.03 40 / 0.18), oklch(9% 0.02 40 / 0.72)); }
.banner-content { position: relative; z-index: 2; width: 100%; padding: 0 0 44px; }
.banner-content .eyebrow { color: oklch(96% 0.02 60 / 0.9); text-shadow: 0 1px 10px oklch(10% 0.02 40 / 0.45); }
.banner-content h1 { color: var(--on-accent); line-height: 1.06; text-shadow: 0 4px 24px oklch(10% 0.02 40 / 0.55); }
.banner-content .lede { color: oklch(94% 0.01 60 / 0.92); text-shadow: 0 2px 12px oklch(10% 0.02 40 / 0.45); }
.is-short .banner-content { padding-bottom: 30px; }
.is-short .banner-content h1 { font-size: clamp(28px, 4vw, 38px); margin-bottom: 6px; }

/* Interior-page banner variant: plain white, no photo — trialing on
   the calendar page before it goes anywhere else. Just the border to
   give it a defined edge against the white page content below. */
.page-banner.is-pattern { border-bottom: 1px solid var(--border); }
.page-banner.is-pattern .banner-art { background: var(--surface); }
.page-banner.is-pattern .banner-scrim { background: none; }
.page-banner.is-pattern .banner-content .eyebrow { color: var(--accent-dark); text-shadow: none; }
.page-banner.is-pattern .banner-content h1 { color: var(--ink); text-shadow: none; }
.page-banner.is-pattern .banner-content .lede { color: var(--ink-soft); text-shadow: none; }


/* ---------------- Sections ---------------- */
.section { padding: 56px 0; }
.section-tight { padding: 40px 0; }
@media (max-width: 640px) {
  .section { padding: 40px 0; }
  .section-tight { padding: 30px 0; }
}
.section-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 26px; flex-wrap: wrap; }
.section-alt { background: var(--surface-sunken); }
.container-wide { width: 100%; max-width: 1440px; margin: 0 auto; padding: 0 24px; }
@media (max-width: 640px) { .container-wide { padding: 0 20px; } }

/* ---------------- Profile page: sidebar + content shell ----------------
   Regular account panels (Profile/Membership/Settings) sit in a normal
   max-width column; admin/author panels that actually benefit from room
   (user management tables, analytics, the editor) get the page's full
   width via .profile-content having no max-width of its own — each
   panel opts into a narrower reading width itself where it wants one. */
.profile-shell { display: flex; align-items: flex-start; gap: 28px; }
.profile-sidebar {
  width: 240px; flex-shrink: 0; position: sticky; top: calc(var(--header-h) + 20px);
  display: flex; flex-direction: column; gap: 4px; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 14px;
  transition: width var(--motion-base) var(--ease);
}
.profile-shell.is-collapsed .profile-sidebar { width: 68px; }
.profile-sidebar-collapse {
  align-self: flex-end; width: 30px; height: 30px; border-radius: 50%; border: 1.5px solid var(--border);
  background: var(--surface); color: var(--ink-soft); display: flex; align-items: center; justify-content: center;
  cursor: pointer; margin-bottom: 6px; transition: background var(--motion-fast) var(--ease), transform var(--motion-fast) var(--ease);
}
.profile-sidebar-collapse:hover { background: var(--surface-sunken); }
.profile-sidebar-collapse svg { width: 15px; height: 15px; transition: transform var(--motion-base) var(--ease); }
.profile-shell.is-collapsed .profile-sidebar-collapse svg { transform: rotate(180deg); }
.profile-nav-label {
  padding: 12px 10px 4px; font-size: 11px; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--label);
}
.profile-shell.is-collapsed .profile-nav-label { opacity: 0; height: 0; padding: 0; overflow: hidden; }
.profile-nav { display: flex; flex-direction: column; gap: 4px; }
.profile-nav-sep { height: 1px; background: var(--border); margin: 8px 4px; }
.profile-nav-item {
  display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px; border-radius: var(--radius-sm);
  border: none; background: transparent; color: var(--ink-soft); font-family: var(--font-body); font-weight: 600;
  font-size: 14px; text-align: left; cursor: pointer; white-space: nowrap; overflow: hidden;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.profile-nav-item:hover { background: var(--surface-sunken); color: var(--ink); }
.profile-nav-item.is-active { background: var(--ink); color: var(--surface); }
.profile-nav-item svg { width: 17px; height: 17px; flex-shrink: 0; }
.profile-nav-item .count-pill {
  margin-left: auto; background: var(--danger); color: #fff; font-size: 11px; font-weight: 800;
  padding: 1px 6px; border-radius: 999px; flex-shrink: 0;
}
.profile-nav-item.is-active .count-pill { background: rgba(255,255,255,0.25); }
.profile-shell.is-collapsed .profile-nav-item span:not(.count-pill) { display: none; }
.profile-shell.is-collapsed .profile-nav-item { justify-content: center; }
.profile-shell.is-collapsed .profile-nav-item .count-pill { position: absolute; margin-left: 14px; margin-top: -14px; }

.profile-content { flex: 1; min-width: 0; }
.payment-badges {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 12px;
  font-size: 12px; color: var(--label); font-weight: 600;
}
.payment-badges svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--label); }
.payment-badges .payment-methods { color: var(--ink-soft); }
.chart-wrap { position: relative; height: 220px; margin-top: 14px; }
.chart-wrap--donut { height: 180px; }
.chart-legend { display: flex; gap: 16px; justify-content: center; margin-top: 10px; flex-wrap: wrap; }
.chart-legend-item { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--ink-soft); font-weight: 600; }
.chart-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.site-image-preview {
  display: flex; align-items: center; justify-content: center; color: var(--label);
  font-size: 13px; font-weight: 600; border: 1.5px dashed var(--border);
}
.profile-panel-narrow { max-width: 560px; }
.profile-panel--in { animation: tabPanelIn 320ms var(--ease); }

/* ---------------- Article editor (write.html / skryf.html) ---------------- */
.editor-topbar {
  display: flex; align-items: center; gap: 16px; padding: 12px 0; margin-bottom: 18px;
  border-bottom: 1px solid var(--border); position: sticky; top: var(--header-h); background: var(--bg); z-index: 10;
}
.editor-save-state { font-size: 13px; color: var(--label); flex: 1; }
.editor-actions { display: flex; gap: 10px; }
.editor-grid { display: grid; grid-template-columns: 1fr 300px; gap: 28px; align-items: start; }
@media (max-width: 860px) { .editor-grid { grid-template-columns: 1fr; } }
.editor-title-input {
  width: 100%; border: none; background: transparent; font-family: var(--font-display); font-weight: 600;
  font-size: clamp(26px, 3.4vw, 38px); color: var(--ink); padding: 0; margin-bottom: 20px; outline: none;
}
.editor-title-input::placeholder { color: var(--label); }
.editor-cover {
  position: relative; width: 100%; aspect-ratio: 16/7; border-radius: var(--radius-lg); overflow: hidden;
  background: var(--surface-sunken); border: 1.5px dashed var(--border); cursor: pointer; margin-bottom: 24px;
  display: flex; align-items: center; justify-content: center;
}
.editor-cover img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.editor-cover-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; color: var(--label); font-weight: 600; font-size: 13.5px; }
.editor-cover-empty svg { width: 26px; height: 26px; }
.editor-cover.has-image .editor-cover-empty { display: none; }
.editor-cover-remove {
  position: absolute; top: 10px; right: 10px; width: 30px; height: 30px; border-radius: 50%; border: none;
  background: oklch(15% 0.02 40 / 0.55); color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
}

.editor-blocks { display: flex; flex-direction: column; gap: 4px; }
.editor-block { position: relative; padding: 10px 44px 10px 0; border-radius: var(--radius-sm); }
.editor-block:hover { background: var(--surface-sunken); }
.editor-block-toolbar { position: absolute; top: 6px; right: 0; display: flex; gap: 2px; opacity: 0; transition: opacity var(--motion-fast) var(--ease); }
.editor-block:hover .editor-block-toolbar { opacity: 1; }
.editor-block-toolbar button {
  width: 26px; height: 26px; border-radius: 6px; border: none; background: var(--surface); color: var(--ink-soft);
  display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 1px 2px oklch(20% 0.02 60 / 0.12);
}
.editor-block-toolbar button:hover { color: var(--danger); }
.editor-block-toolbar svg { width: 13px; height: 13px; }
.editor-block textarea {
  width: 100%; border: none; background: transparent; resize: none; overflow: hidden; outline: none;
  font-family: var(--font-body); font-size: 16.5px; line-height: 1.65; color: var(--ink); padding: 4px 0;
}
.editor-block--quote { border-left: 3px solid var(--accent); padding-left: 16px; }
.editor-block--quote textarea { font-family: var(--font-display); font-style: italic; font-size: 19px; color: var(--ink-soft); }
.editor-block--image { padding-top: 4px; }
.editor-block--image .editor-block-image-drop {
  width: 100%; min-height: 120px; border-radius: var(--radius-md); background: var(--surface-sunken);
  border: 1.5px dashed var(--border); cursor: pointer; display: flex; align-items: center; justify-content: center;
  color: var(--label); font-size: 13px; font-weight: 600; position: relative; overflow: hidden;
}
.editor-block--image img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.editor-block--image input[type="text"] {
  width: 100%; margin-top: 8px; border: none; border-bottom: 1px solid var(--border); background: transparent;
  font-size: 13px; color: var(--label); padding: 4px 0; outline: none;
}

.editor-add-bar { display: flex; gap: 8px; margin-top: 12px; padding-top: 16px; border-top: 1px solid var(--border); flex-wrap: wrap; }
.editor-field-label { display: block; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; color: var(--label); margin-bottom: 6px; }
.editor-side-input {
  width: 100%; padding: 9px 12px; border-radius: var(--radius-sm); border: 1.5px solid var(--border);
  background: var(--surface); color: var(--ink); font-family: var(--font-body); font-size: 14px; resize: vertical;
}
.editor-side-input:focus { border-color: var(--accent); outline: none; }

/* A horizontally-scrolling icon strip (the first mobile version of
   this) turned out to read as low-contrast clutter rather than a nav —
   small icons on a plain white card, most of them scrolled off-screen
   with no obvious affordance to find them. This instead collapses to a
   single "current section" bar you tap to expand, same idea as a
   native picker: one clear, legible row when closed, the full list
   (still a normal vertical stack, same styling as desktop) when open. */
.profile-mobile-toggle {
  display: none; width: 100%; align-items: center; gap: 12px; padding: 13px 14px;
  background: var(--surface-sunken); border: 1.5px solid var(--border); border-radius: var(--radius-md);
  color: var(--ink); font-family: var(--font-body); font-weight: 700; font-size: 15px; cursor: pointer;
  text-align: left;
}
.profile-mobile-toggle svg.toggle-icon { width: 19px; height: 19px; flex-shrink: 0; color: var(--accent-dark); }
.profile-mobile-toggle .toggle-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.profile-mobile-toggle .count-pill { flex-shrink: 0; }
.profile-mobile-toggle .chev { width: 16px; height: 16px; flex-shrink: 0; color: var(--label); transition: transform var(--motion-base) var(--ease); }
.profile-sidebar.is-nav-open .profile-mobile-toggle .chev { transform: rotate(180deg); }

@media (max-width: 900px) {
  .profile-shell { flex-direction: column; }
  .profile-sidebar {
    /* position:relative (not the base rule's sticky) drops the
       sticky-while-scrolling behaviour, which only makes sense next to
       a tall content column — top:auto resets the base rule's `top`
       offset (meant only for that desktop sticky position), since
       relative still honors `top` unlike static, and without resetting
       it the sidebar renders ~96px lower than its actual flex slot,
       overlapping .profile-content above it. */
    width: 100% !important; position: relative; top: auto; flex-direction: column;
    padding: 10px; gap: 0;
  }
  .profile-sidebar-collapse { display: none; }
  .profile-mobile-toggle { display: flex; }
  .profile-nav {
    flex-direction: column; gap: 2px; max-height: 0; overflow: hidden;
    margin-top: 0; transition: max-height 320ms var(--ease), margin-top 320ms var(--ease);
  }
  .profile-sidebar.is-nav-open .profile-nav { max-height: 640px; margin-top: 8px; }
  .profile-nav-label { padding: 10px 10px 4px; }
  .profile-nav-item { width: 100%; padding: 12px 13px; }
  .profile-nav-item .count-pill { position: static; margin-left: auto; }
}

.grid { display: grid; gap: 20px; }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
@media (max-width: 860px) { .grid-3, .grid-2 { grid-template-columns: 1fr; } }

/* ---------------- Cards ---------------- */
.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 20px; display: flex; flex-direction: column; gap: 8px;
}
.card-link { text-decoration: none; color: inherit; }
.card h3 { margin-bottom: 2px; }
.card p { margin: 0; color: var(--ink-soft); font-size: 14.5px; }

.tag {
  display: inline-flex; align-items: center; font-size: 12px; font-weight: 700;
  padding: 4px 10px; border-radius: 999px; background: var(--surface-sunken); color: var(--label);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.tag-accent { background: var(--accent-soft); color: var(--accent-dark); }
.tag-news { background: var(--accent-soft); color: var(--accent-dark); }
.tag-article { background: var(--ink); color: var(--surface); }

/* ---------------- Event list ---------------- */
.event-row { display: flex; align-items: center; gap: 18px; padding: 16px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); }
.event-date { flex-shrink: 0; width: 62px; text-align: center; padding: 8px 0; border-radius: var(--radius-sm); background: var(--accent-soft); color: var(--accent-dark); }
.event-date .day { display: block; font-family: var(--font-display); font-size: 22px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; }
.event-date .mon { display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; margin-top: 2px; }
.event-body { flex: 1; min-width: 0; }
.event-body h3 { margin-bottom: 3px; margin-top: 6px; }
.event-meta { display: flex; gap: 14px; flex-wrap: wrap; font-size: 13.5px; color: var(--label); font-weight: 600; }
.event-meta span { display: inline-flex; align-items: center; gap: 5px; }

.filter-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 24px; }
.filter-chip { border: 1.5px solid var(--border); background: var(--surface); color: var(--ink-soft); padding: 7px 15px; border-radius: 999px; font-weight: 600; font-size: 13.5px; cursor: pointer; transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease); }
.filter-chip.is-active { background: var(--ink); border-color: var(--ink); color: var(--surface); }
.filter-chip svg { width: 14px; height: 14px; vertical-align: -2px; margin-right: 5px; }

/* ---------------- Animated tab strips (assets/js/tab-panels.js) ----------------
   A sliding pill (like the auth modal's login/signup indicator) glides
   behind whichever tab is active instead of each button just repainting
   its own background — used for the profile page's Profile/Settings
   tabs and the admin dashboard's tab strip. */
.filter-bar--tabs { position: relative; background: var(--surface-sunken); padding: 4px; border-radius: 999px; gap: 2px; }
.filter-bar--tabs .filter-chip {
  position: relative; z-index: 1; background: transparent; border-color: transparent; color: var(--ink-soft);
}
.filter-bar--tabs .filter-chip.is-active { background: transparent; border-color: transparent; color: var(--surface); }
.tab-strip-indicator {
  position: absolute; top: 0; left: 0; background: var(--ink); border-radius: 999px; z-index: 0;
  transition: transform 360ms cubic-bezier(.2,.8,.2,1), width 360ms cubic-bezier(.2,.8,.2,1), height 360ms cubic-bezier(.2,.8,.2,1);
}
.tab-panel--in { animation: tabPanelIn 320ms var(--ease); }
@keyframes tabPanelIn {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .tab-strip-indicator { transition: none; }
  .tab-panel--in { animation: none; }
}
/* Category row sits under the content-type row — smaller and in the
   accent color when active, so the two rows read as primary/secondary
   rather than two equally-weighted controls. */
.filter-bar--categories .filter-chip { padding: 5px 13px; font-size: 12.5px; }
.filter-bar--categories .filter-chip.is-active { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-dark); }

/* ---------------- News feed controls (search + sort) ---------------- */
.news-controls { display: flex; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.news-search { position: relative; flex: 1; min-width: 220px; }
.news-search .icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--label); pointer-events: none; }
.news-search input {
  width: 100%; padding: 11px 14px 11px 40px; border-radius: 999px; border: 1.5px solid var(--border);
  font-family: var(--font-body); font-size: 14.5px; background: var(--surface); color: var(--ink);
  transition: border-color var(--motion-fast) var(--ease);
}
.news-search input:focus { border-color: var(--accent); outline: none; }
.news-search input::-webkit-search-cancel-button { cursor: pointer; }
.news-sort {
  padding: 11px 36px 11px 16px; border-radius: 999px; border: 1.5px solid var(--border);
  font-family: var(--font-body); font-weight: 600; font-size: 13.5px; color: var(--ink); background: var(--surface);
  appearance: none; -webkit-appearance: none; -moz-appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23726a62' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center; background-size: 14px;
  transition: border-color var(--motion-fast) var(--ease);
}
.news-sort:focus { border-color: var(--accent); outline: none; }

/* Category filter as a proper dropdown (reusing the nav's own
   .dropdown-trigger/.dropdown-menu component — same open/close
   animation, no new transition code needed) instead of a chip row,
   since the category list can run long enough to wrap messily. */
.news-filter-dropdown { position: relative; }
.news-filter-dropdown .dropdown-trigger {
  display: inline-flex; align-items: center; gap: 8px; padding: 11px 16px; border-radius: 999px;
  border: 1.5px solid var(--border); background: var(--surface); color: var(--ink);
  font-family: var(--font-body); font-weight: 600; font-size: 13.5px; cursor: pointer;
  transition: border-color var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.news-filter-dropdown .dropdown-trigger:hover { border-color: var(--accent); }
.news-filter-dropdown.has-selection .dropdown-trigger { border-color: var(--accent); color: var(--accent-dark); }
.news-filter-dropdown .dropdown-menu { min-width: 220px; max-height: 320px; overflow-y: auto; }
.news-filter-dropdown .dropdown-menu-item.is-active { background: var(--accent-soft); color: var(--accent-dark); }

/* Below the width where these controls stop fitting on one row, they
   used to wrap in place — but the category dropdown's menu is
   position:absolute and left:0 off its *trigger*, so once flex-wrap
   pushed that trigger away from the row's left edge, a 220px-min-width
   menu opening off it could reach past the right edge of the viewport.
   An absolutely positioned element extending past the viewport still
   grows the page's scrollable area, which is the "scrolls right and
   doesn't come back" glitch. Stacking every control full-width removes
   the ambiguity: the trigger is exactly the container's width, so its
   menu (matched to that same width below) never has anywhere to
   overflow to. */
@media (max-width: 640px) {
  .news-controls { flex-direction: column; align-items: stretch; }
  .news-search, .news-filter-dropdown, .news-sort, .news-controls .filter-bar { width: 100%; }
  .news-filter-dropdown .dropdown-trigger { width: 100%; justify-content: space-between; }
  .news-filter-dropdown .dropdown-menu { left: 0; right: 0; width: auto; min-width: 0; }
}

.author-avatar { width: 24px; height: 24px; border-radius: 50%; flex-shrink: 0; object-fit: cover; background: var(--surface-sunken); }

/* Still used by calendar.js's day-agenda (.event-row list) even though
   the old news/articles/discussions row listings that used to share
   this wrapper are gone. */
.list { display: flex; flex-direction: column; gap: 14px; }

/* ---------------- Recent stories collage (homepage: bento-style grid,
   news + articles mixed) ---------------- */
.posts-collage { display: grid; grid-template-columns: repeat(6, 1fr); grid-auto-rows: 88px; gap: 22px; }

.collage-item {
  position: relative; overflow: hidden; border-radius: 2px; text-decoration: none; display: block;
  border: 6px solid var(--surface);
  box-shadow: 0 4px 18px oklch(20% 0.014 60 / 0.1);
  transition: transform 480ms var(--ease), box-shadow 380ms var(--ease);
}
.collage-item:hover, .collage-item.is-active {
  transform: translateY(-6px) scale(1.012); box-shadow: 0 26px 50px oklch(20% 0.014 60 / 0.22); z-index: 3;
}

.collage-item--feature { grid-column: 1 / span 4; grid-row: 1 / span 4; }
.collage-item--tall { grid-column: 5 / span 2; grid-row: 1 / span 4; }
.collage-item--a { grid-column: 1 / span 3; grid-row: 5 / span 3; }
.collage-item--b { grid-column: 4 / span 3; grid-row: 5 / span 3; }

.collage-item > img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.001); transition: transform 700ms var(--ease);
}
.collage-item:hover > img, .collage-item.is-active > img { transform: scale(1.06); }

.collage-item::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, oklch(20% 0.014 60 / 0) 38%, oklch(14% 0.014 60 / 0.82) 100%);
}

.collage-caption { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; padding: 22px 24px; color: var(--surface); }
.collage-tag { display: block; margin-bottom: 10px; font-size: 11px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: oklch(100% 0 0 / 0.75); }

.collage-byline { display: flex; align-items: center; gap: 9px; margin-bottom: 12px; }
.collage-avatar { width: 26px; height: 26px; border-radius: 50%; object-fit: cover; border: 1.5px solid oklch(100% 0 0 / 0.55); flex-shrink: 0; }
.collage-byline-text { display: flex; flex-direction: column; line-height: 1.3; }
.collage-author { font-size: 12.5px; font-weight: 600; color: var(--surface); }
.collage-date { font-size: 11px; letter-spacing: 0.03em; color: oklch(100% 0 0 / 0.65); }
.collage-item--a .collage-avatar, .collage-item--b .collage-avatar { width: 22px; height: 22px; }
.collage-item--a .collage-author, .collage-item--b .collage-author { font-size: 12px; }

.collage-stats { display: flex; align-items: center; gap: 14px; margin-top: 10px; }
.collage-stat { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; font-weight: 600; color: oklch(100% 0 0 / 0.75); }
.collage-stat .icon { width: 12px; height: 12px; }
.collage-item--a .collage-stats, .collage-item--b .collage-stats { gap: 8px; }
.collage-item--a .collage-stat, .collage-item--b .collage-stat { font-size: 10.5px; }

.collage-caption h3 {
  font-family: var(--font-display); font-style: italic; font-weight: 500; color: var(--surface);
  line-height: 1.3; margin: 0; transition: transform 620ms var(--ease);
}
.collage-item--feature .collage-caption h3 { font-size: 1.55rem; }
.collage-item--tall .collage-caption h3 { font-size: 1.2rem; }
.collage-item--a .collage-caption h3, .collage-item--b .collage-caption h3 { font-size: 1.05rem; }

.collage-excerpt-wrap { display: grid; grid-template-rows: 0fr; margin-top: 6px; transition: grid-template-rows 620ms var(--ease); }
.collage-caption p {
  margin: 0; font-size: 13.5px; line-height: 1.55; color: oklch(100% 0 0 / 0.85); max-width: 420px;
  min-height: 0; overflow: hidden; opacity: 0; transform: translateY(6px);
  transition: opacity 480ms var(--ease) 60ms, transform 480ms var(--ease) 60ms;
}
.collage-item:hover .collage-excerpt-wrap, .collage-item.is-active .collage-excerpt-wrap { grid-template-rows: 1fr; }
.collage-item:hover .collage-caption p, .collage-item.is-active .collage-caption p { opacity: 1; transform: translateY(0); }
.collage-item:hover .collage-caption h3, .collage-item.is-active .collage-caption h3 { transform: translateY(-2px); }

/* "Read more" cue: collapsed to nothing at rest, slides in from the
   left and grows into view on hover. Since it sits last in the caption's
   stack, it pushes everything above it up as it opens — the "move the
   text up a bit" part comes for free from that layout, not from
   separately animating the other elements. */
.collage-readmore {
  display: flex; align-items: center; gap: 6px; overflow: hidden; white-space: nowrap;
  max-height: 0; margin-top: 0; opacity: 0; transform: translateX(-10px);
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.01em; color: var(--surface);
  transition: max-height 420ms var(--ease), margin-top 420ms var(--ease), opacity 300ms var(--ease), transform 420ms var(--ease);
}
.collage-readmore .icon { width: 15px; height: 15px; transition: transform 420ms var(--ease); }
.collage-item:hover .collage-readmore, .collage-item.is-active .collage-readmore {
  max-height: 22px; margin-top: 12px; opacity: 1; transform: translateX(0);
}
.collage-item:hover .collage-readmore .icon, .collage-item.is-active .collage-readmore .icon { transform: translateX(4px); }
.collage-item--no-media .collage-readmore { color: var(--accent-dark); }

/* Stories without a photo read as a deliberate white "read" card, not a
   failed image load: plain white background with its own border (photo
   tiles get their contrast from the photo; this needs one of its own),
   ink text, content centered rather than pinned to the bottom, and a
   large accent-tinted quotation mark as the one clear graphic signal that
   this is by design. No dark photo-legibility overlay — there's no photo
   under it to darken. The excerpt is shown in full up front rather than
   hidden until hover, since that reveal exists to declutter a photo, and
   there's no photo here to declutter. */
.collage-item--no-media {
  background: var(--surface); border-color: var(--border); border-width: 1px;
}
.collage-item--no-media::after { content: none; }
.collage-item--no-media::before {
  content: '\201C'; position: absolute; top: -0.1em; left: 20px; z-index: 1;
  font-family: var(--font-display); font-style: italic; font-size: 7.5rem; line-height: 1;
  color: var(--accent-soft-strong); pointer-events: none;
}
.collage-item--no-media .collage-caption { top: 0; display: flex; flex-direction: column; justify-content: center; }
.collage-item--no-media .collage-tag { color: var(--accent-dark); }
.collage-item--no-media .collage-author { color: var(--ink); }
.collage-item--no-media .collage-date { color: var(--label); }
.collage-item--no-media .collage-avatar { border-color: oklch(20% 0.014 60 / 0.16); }
.collage-item--no-media .collage-stat { color: var(--label); }
.collage-item--no-media .collage-caption h3 { color: var(--ink); }
.collage-item--no-media .collage-caption p { color: var(--ink-soft); }
.collage-item--no-media .collage-excerpt-wrap { grid-template-rows: 1fr; margin-top: 10px; }
.collage-item--no-media .collage-caption p {
  opacity: 1; transform: none; max-width: none; -webkit-line-clamp: 6; display: -webkit-box; -webkit-box-orient: vertical;
}
/* The giant quotation mark and 6-line excerpt above are tuned for the
   feature/tall slots' larger box — in the compact a/b slot (roughly a
   third of the area) they'd overflow or crowd out the title. Same
   ingredients, smaller: this is what keeps a no-photo card from reading
   as "bulky" when the bento cycle happens to land it in a small slot. */
.collage-item--a.collage-item--no-media::before, .collage-item--b.collage-item--no-media::before {
  font-size: 4rem; left: 16px;
}
.collage-item--a.collage-item--no-media .collage-caption p, .collage-item--b.collage-item--no-media .collage-caption p {
  -webkit-line-clamp: 3;
}

@media (max-width: 900px) {
  .posts-collage { grid-template-columns: 1fr; grid-auto-rows: auto; }
  .collage-item { grid-column: auto !important; grid-row: auto !important; aspect-ratio: 4/5; }
}

/* News feed grid: the SAME bento variant sizing as the homepage collage
   (feature/tall/a/b, cycled every 4 items by render.js), not a plain
   uniform grid — a filtered/searched/sorted list can be any length, so
   instead of the homepage's one hand-placed group, every item declares
   only a relative span (span N, not "column 1 / span 4") and
   grid-auto-flow: dense repeats the same 4-item pattern down the page
   as many times as there are items. A no-image "quote" card just takes
   whatever slot it lands in like any photo card — sometimes the big
   feature/tall slot, sometimes the small a/b one — instead of always
   being forced to a bulky full-width row, which is what used to make
   two of them in a row read as a stacked wall of white cards. */
.news-grid {
  display: grid; grid-template-columns: repeat(6, 1fr); grid-auto-rows: 88px; grid-auto-flow: row dense; gap: 22px;
  opacity: 1; transition: opacity 180ms var(--ease);
}
.news-grid.is-filtering { opacity: 0; }
.news-grid .collage-item--feature { grid-column: span 4; grid-row: span 4; }
.news-grid .collage-item--tall { grid-column: span 2; grid-row: span 4; }
.news-grid .collage-item--a { grid-column: span 3; grid-row: span 3; }
.news-grid .collage-item--b { grid-column: span 3; grid-row: span 3; }
@media (max-width: 900px) {
  .news-grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 240px; }
  .news-grid .collage-item--feature, .news-grid .collage-item--tall,
  .news-grid .collage-item--a, .news-grid .collage-item--b { grid-column: span 1; grid-row: span 1; }
}
@media (max-width: 640px) {
  .news-grid { grid-template-columns: 1fr; grid-auto-rows: auto; }
  .news-grid .collage-item { aspect-ratio: 4 / 5; }
  .news-grid .collage-item--feature, .news-grid .collage-item--tall,
  .news-grid .collage-item--a, .news-grid .collage-item--b { grid-column: auto; grid-row: auto; }
}

/* ---------------- Month calendar ---------------- */
.cal-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 22px; }
.cal-nav { display: flex; align-items: center; gap: 14px; }
.cal-nav h2 { margin: 0; min-width: 190px; text-align: center; }
@media (max-width: 480px) { .cal-nav h2 { min-width: 140px; font-size: 20px; } }
.cal-nav-btn {
  width: 36px; height: 36px; border-radius: 50%; border: 1.5px solid var(--border); background: var(--surface);
  display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--ink);
  transition: background 140ms var(--ease), transform 120ms var(--ease);
}
.cal-nav-btn:hover { background: var(--surface-sunken); }
.cal-nav-btn:active { transform: scale(0.9); }
.cal-nav-btn svg { width: 16px; height: 16px; }

#cal-month-label { display: inline-block; transition: opacity 180ms var(--ease), transform 180ms var(--ease); }
#cal-month-label.is-changing { opacity: 0; transform: translateY(4px); }

.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 32px; }
.cal-weekday { text-align: center; font-size: 12px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--label); padding-bottom: 8px; }
.cal-day {
  aspect-ratio: 1; border: none; background: transparent; border-radius: var(--radius-md);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  cursor: pointer; font-family: var(--font-body); position: relative;
  transition: background 140ms var(--ease), transform 120ms var(--ease);
  animation: calCellIn 320ms var(--ease) backwards;
}
.cal-day:hover { background: var(--surface-sunken); transform: scale(1.05); }
.cal-day:active { transform: scale(0.94); }
.cal-day.is-outside .cal-day-num { color: oklch(85% 0.012 60); }
.cal-day-num { font-size: 15px; font-weight: 600; color: var(--ink); width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background 200ms var(--ease), color 200ms var(--ease); }
.cal-day.is-today .cal-day-num { background: var(--accent-soft); color: var(--accent-dark); }
.cal-day.is-selected .cal-day-num { background: var(--ink); color: var(--surface); animation: calPop 320ms var(--ease); }
.cal-dots { display: flex; gap: 3px; height: 6px; }
.cal-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }

.cal-agenda { border-top: 1px solid var(--border); padding-top: 26px; }
.cal-agenda h3 { margin-bottom: 14px; }
button.event-row { width: 100%; text-align: left; font: inherit; cursor: pointer; animation: calRowIn 380ms var(--ease) backwards; }
/* Inside the homepage's day-modal, .cal-agenda is the only thing in the
   panel — drop the top rule/padding that separates it from the grid
   when it's rendered inline on the full calendar.html page instead. */
.modal-panel .cal-agenda { border-top: none; padding-top: 0; }

@keyframes calCellIn {
  from { opacity: 0; transform: scale(0.85); }
  to { opacity: 1; transform: scale(1); }
}
@keyframes calRowIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes calPop {
  0% { transform: scale(1); }
  40% { transform: scale(1.18); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .cal-day, button.event-row, .cal-day-num { animation: none !important; }
}

/* ---------------- Ad cards ----------------
   Rebuilt to share the article collage's photo-card language: a full-bleed
   image, dark gradient for legibility, and a caption pinned to the bottom
   that lifts and zooms on hover — so "Featured in town" reads as part of
   the same visual system as "Latest from Christiana" rather than a
   plain data-sheet card. */
.ad-card {
  position: relative; overflow: hidden; border-radius: 2px; text-decoration: none; display: block;
  border: 6px solid var(--surface); aspect-ratio: 4 / 5;
  box-shadow: 0 4px 18px oklch(20% 0.014 60 / 0.1);
  transition: transform 480ms var(--ease), box-shadow 380ms var(--ease);
}
.ad-card:hover, .ad-card.is-active { transform: translateY(-6px) scale(1.012); box-shadow: 0 26px 50px oklch(20% 0.014 60 / 0.22); z-index: 3; }
.ad-card.is-featured { box-shadow: 0 0 0 2px var(--supporter-gold), 0 4px 18px oklch(20% 0.014 60 / 0.1); }
.ad-featured-badge {
  position: absolute; top: 12px; left: 12px; z-index: 3; display: inline-flex; align-items: center; gap: 5px;
  background: var(--supporter-gold); color: #fff; font-size: 11.5px; font-weight: 800; text-transform: uppercase;
  letter-spacing: 0.04em; padding: 5px 10px; border-radius: 999px; box-shadow: 0 2px 6px oklch(20% 0.02 60 / 0.3);
}
.ad-featured-badge svg { width: 11px; height: 11px; }

.ad-card > img {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.001); transition: transform 700ms var(--ease);
}
.ad-card:hover > img, .ad-card.is-active > img { transform: scale(1.06); }

.ad-card::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, oklch(20% 0.014 60 / 0) 38%, oklch(14% 0.014 60 / 0.82) 100%);
}
.ad-card-caption { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; padding: 20px 20px 18px; color: var(--surface); }
.ad-card-top { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.ad-logo { width: 42px; height: 42px; border-radius: 10px; object-fit: cover; flex-shrink: 0; border: 1.5px solid oklch(100% 0 0 / 0.55); }
.ad-card-heading { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.ad-card-heading h3 { margin: 0; font-family: var(--font-display); font-style: italic; font-weight: 500; font-size: 1.05rem; color: var(--surface); }
.ad-category { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: oklch(100% 0 0 / 0.75); }
.ad-category .icon { color: oklch(100% 0 0 / 0.9); width: 13px; height: 13px; }

.ad-card-caption p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.5; color: oklch(100% 0 0 / 0.88); }

/* Contact details stay tucked away under the blurb until hover, then
   slide in from the left — same reveal-on-hover idea as the article
   collage's "Read more" cue, just travelling sideways instead of
   growing vertically. Because it's the last thing in the caption,
   the bottom-anchored box growing to fit it pushes the name/category
   above it up, same as the collage does. */
.ad-contact {
  display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600; color: oklch(100% 0 0 / 0.9);
  overflow: hidden; white-space: nowrap; max-height: 0; margin-top: 0; padding-top: 0; opacity: 0;
  transform: translateX(-18px); border-top: 1px solid transparent;
  transition: max-height 380ms var(--ease), margin-top 380ms var(--ease), padding-top 380ms var(--ease),
    opacity 280ms var(--ease), transform 380ms var(--ease), border-color 380ms var(--ease);
}
.ad-card:hover .ad-contact, .ad-card.is-active .ad-contact {
  max-height: 32px; margin-top: 2px; padding-top: 10px; opacity: 1; transform: translateX(0);
  border-top-color: oklch(100% 0 0 / 0.22);
}
.ad-contact .icon { color: oklch(100% 0 0 / 0.9); flex-shrink: 0; }

/* ---------------- Article detail ---------------- */
.article-head { max-width: 72ch; }
/* A slow settle rather than a static drop-in — the image is ever so
   slightly over-zoomed at load and eases back to its resting scale. */
@keyframes heroSettle { from { transform: scale(1.045); opacity: 0.7; } to { transform: scale(1); opacity: 1; } }
/* Featured-image header (article-hero-banner further down) gets the
   settle-in; other pages' fixed background banners don't need it. */
.article-hero-banner .banner-art-img { animation: heroSettle 1400ms var(--ease) both; }
@media (prefers-reduced-motion: reduce) { .article-hero-banner .banner-art-img { animation: none; } }

/* The photo used to need a dark scrim so overlaid text stayed legible —
   now that the text lives below it instead, that scrim just reads as an
   odd dark tint with a hard cut into the white page. Fading the bottom
   of the photo into the page's own background instead makes it read as
   one continuous surface, photo dissolving into page rather than
   stopping dead. */
.article-hero-banner .banner-scrim {
  background: linear-gradient(180deg, oklch(20% 0.03 40 / 0.06) 0%, transparent 35%, var(--surface) 100%);
}

/* Category/title/byline used to live two places — overlaid on the hero
   photo for image articles, plain below for photo-less ones — which
   also meant fighting the photo for contrast. They live in exactly one
   place now, always below the banner, so this is the only styling that
   needs to exist. Kept neutral (ink/gray, not the site's orange accent)
   deliberately — the accent color is for interactive/branded moments
   elsewhere, and competing with it here just adds noise to a masthead
   that's meant to read calmly. */
.article-kicker { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.article-kicker .eyebrow { color: var(--ink-soft); }
.article-kicker .tag-news { background: var(--surface-sunken); color: var(--ink-soft); }
.article-views { font-size: 13px; font-weight: 600; color: var(--label); margin-left: auto; }
.article-title { font-size: clamp(30px, 4.4vw, 50px); line-height: 1.08; margin: 14px 0 20px; }
.article-byline.byline {
  margin-top: 0; padding-bottom: 26px; margin-bottom: 30px; border-bottom: 1px solid var(--border);
}

/* Deliberately a touch darker than the site's default --ink — this is
   the one place on the site meant to be read start-to-finish, so it
   gets the highest-contrast text on the page rather than sharing the
   softer tone chrome/UI text uses. */
.article-body { max-width: 68ch; font-size: 17.5px; line-height: 1.7; color: oklch(14% 0.01 50); }
.article-body p { max-width: none; color: inherit; }
/* Classic newspaper drop cap on the opening paragraph only — a serif
   capital tall enough to sit against three lines of body text, pulled
   left so those lines wrap around it instead of starting flush. */
.article-body > p:first-of-type::first-letter {
  float: left; font-family: var(--font-display); font-weight: 600; font-style: italic;
  font-size: 3.6em; line-height: 0.78; padding: 0.03em 0.1em 0 0; color: var(--accent-dark);
}
.article-body blockquote {
  margin: 32px 0; padding: 4px 0 4px 24px; border-left: 3px solid var(--accent);
  font-family: var(--font-display); font-style: italic; font-size: 1.25em; line-height: 1.5; color: var(--ink-soft);
}
.article-figure { margin: 32px 0; }
.article-figure img { width: 100%; border-radius: var(--radius-md); display: block; }
.article-figure figcaption { margin-top: 8px; font-size: 13px; color: var(--label); text-align: center; }
.byline { display: flex; gap: 10px; align-items: center; font-size: 14px; color: var(--label); font-weight: 600; margin-top: 14px; }
.byline .author-avatar { width: 30px; height: 30px; }
.byline .dot-sep::before, .row-byline .dot-sep::before { content: "·"; margin: 0 2px; }
.byline-author-link { display: inline-flex; align-items: center; gap: 10px; color: inherit; text-decoration: none; }
.byline-author-link:hover { color: var(--accent-dark); text-decoration: underline; }

/* ---------------- Author page ---------------- */
.author-profile { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.author-profile-avatar {
  width: 96px; height: 96px; border-radius: 50%; object-fit: cover; background: var(--surface-sunken);
  border: 3px solid var(--surface); box-shadow: var(--shadow-card); flex-shrink: 0;
}
.author-profile-meta { margin: 6px 0 0; color: var(--label); font-weight: 600; }
.author-profile-bio { margin: 24px 0 0; max-width: 62ch; color: var(--ink-soft); font-size: 15.5px; line-height: 1.6; }

/* ---------------- Article share row ---------------- */
.article-share { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 36px; padding-top: 24px; border-top: 1px solid var(--border); }
.article-share-label { font-size: 13px; font-weight: 700; color: var(--label); text-transform: uppercase; letter-spacing: 0.06em; margin-right: 4px; }
.share-btn { position: relative; }
.like-btn svg { width: 16px; height: 16px; margin-right: 2px; transition: transform 200ms var(--ease); }
.like-btn.is-active { color: var(--danger); border-color: oklch(56% 0.19 25 / 0.35); background: oklch(56% 0.19 25 / 0.08); }
.like-btn.is-active svg { fill: currentColor; }
.like-btn.is-pulsing svg { animation: likePulse 420ms var(--ease); }
@keyframes likePulse {
  0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); }
}
.share-copied-tip {
  position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(4px);
  background: var(--ink); color: var(--surface); font-size: 12px; font-weight: 700; white-space: nowrap;
  padding: 5px 10px; border-radius: 999px; opacity: 0; pointer-events: none;
  transition: opacity 200ms var(--ease), transform 200ms var(--ease);
}
.share-copied-tip.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }

/* ---------------- Article comments ---------------- */
.comments-section { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--border); }
.comments-heading { display: flex; align-items: baseline; gap: 8px; }
.comments-count { color: var(--label); font-weight: 600; font-size: 0.65em; }
.comments-empty { color: var(--label); font-size: 14.5px; margin: 20px 0; }

.comment-list { display: flex; flex-direction: column; gap: 18px; margin: 22px 0 28px; }
.comment-item { display: flex; gap: 12px; }
.comment-avatar { width: 38px; height: 38px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: var(--surface-sunken); }
.comment-body { flex: 1; min-width: 0; background: var(--surface-sunken); border-radius: var(--radius-md); padding: 12px 16px; }
.comment-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; flex-wrap: wrap; }
.comment-author { font-weight: 700; font-size: 14px; color: var(--ink); }
.comment-date { font-size: 12.5px; color: var(--label); }
.comment-body p { margin: 0 0 8px; font-size: 14.5px; line-height: 1.55; color: var(--ink-soft); }

/* Posting a comment re-renders the whole list from Supabase (simplest
   correct way to pick up the new row's real id/timestamp) rather than
   optimistically splicing one in — the entrance animation is what keeps
   that from reading as a flat, instant swap. Only ever added to the
   just-posted item via JS, not on every render, so older comments don't
   replay it each time the list refreshes. */
.comment-item.is-new { opacity: 0; transform: translateY(10px) scale(0.98); }
.comment-item.is-new.is-entering {
  opacity: 1; transform: translateY(0) scale(1);
  transition: opacity 380ms var(--ease), transform 380ms var(--ease);
}

.comment-actions { display: flex; align-items: center; gap: 4px; margin-top: 2px; }
.comment-action {
  display: inline-flex; align-items: center; gap: 5px; padding: 5px 9px; border-radius: 999px;
  border: none; background: transparent; color: var(--label); font-family: var(--font-body);
  font-weight: 600; font-size: 12.5px; cursor: pointer;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.comment-action svg { width: 14px; height: 14px; }
.comment-action:hover { background: oklch(20% 0.014 60 / 0.06); color: var(--ink); }
.comment-action.is-active { color: var(--accent-dark); }
.comment-action--report { padding: 5px; margin-left: auto; }
.comment-action--report:hover { background: oklch(56% 0.19 25 / 0.1); color: var(--danger); }

/* One level of nesting: replies sit indented under a connecting rail so
   the top-level/reply split reads at a glance, not just from indent
   alone — a lighter, more compact bubble keeps replies visually
   subordinate to the comment they're answering. */
.comment-replies {
  display: flex; flex-direction: column; gap: 14px; margin: 14px 0 0 19px; padding-left: 22px;
  border-left: 2px solid var(--border);
}
.comment-item--reply .comment-avatar { width: 28px; height: 28px; }
.comment-item--reply .comment-body { background: transparent; padding: 4px 0; }
.comment-item--reply .comment-author { font-size: 13px; }
.comment-item--reply .comment-body p { font-size: 14px; }

.comment-reply-form { display: flex; gap: 12px; align-items: flex-start; margin: 12px 0 0 41px; }
/* [hidden] is only as specific as a class selector, and same-specificity
   same-origin rules resolve by source order — so this override has to
   come after the display:flex above to actually win, otherwise the
   [hidden] attribute would do nothing. Same fix the header's login
   button and the article hero banner already needed elsewhere in this
   file. */
.comment-reply-form[hidden] { display: none; }
.comment-reply-form textarea {
  width: 100%; min-height: 52px; padding: 9px 12px; border-radius: var(--radius-md); border: 1.5px solid var(--border);
  font-family: var(--font-body); font-size: 14px; resize: vertical; background: var(--surface); color: var(--ink);
  transition: border-color var(--motion-fast) var(--ease);
}
.comment-reply-form textarea:focus { border-color: var(--accent); outline: none; }
.comment-reply-form .comment-form-actions { gap: 8px; margin-top: 6px; }

.comment-guest {
  display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;
  background: var(--surface-sunken); border-radius: var(--radius-md); padding: 16px 20px;
}
/* Same [hidden]-loses-to-author-display bug as the reply form and the
   header's login button elsewhere in this file: this class's own
   display:flex outranks the UA [hidden]{display:none} unless an
   [hidden] rule for it exists too. Without this, the "sign in to
   comment" prompt shows even when signed in, right next to the
   composer it's supposed to replace. */
.comment-guest[hidden] { display: none; }
.comment-guest p { margin: 0; color: var(--ink-soft); font-weight: 600; font-size: 14.5px; }

.comment-composer { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 10px; }
.comment-composer-avatar { width: 38px; height: 38px; border-radius: 50%; object-fit: cover; flex-shrink: 0; background: var(--surface-sunken); }
.comment-composer textarea {
  flex: 1; min-height: 70px; padding: 11px 14px; border-radius: var(--radius-md); border: 1.5px solid var(--border);
  font-family: var(--font-body); font-size: 14.5px; resize: vertical; background: var(--surface); color: var(--ink);
  transition: border-color var(--motion-fast) var(--ease);
}
.comment-composer textarea:focus { border-color: var(--accent); outline: none; }
.comment-form-actions { display: flex; justify-content: flex-end; }
.form-note { font-size: 13px; color: var(--label); }
.social-note {
  max-height: 0; opacity: 0; overflow: hidden; margin-top: 0; transform: translateY(-4px);
  transition: max-height 320ms var(--ease), opacity 260ms var(--ease), margin-top 320ms var(--ease), transform 320ms var(--ease);
}
.social-note.is-visible { max-height: 60px; opacity: 1; margin-top: 10px; transform: translateY(0); }

/* ---------------- Forms ---------------- */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; max-width: 620px; }
.form-grid .full { grid-column: 1 / -1; }
.field label { display: block; font-size: 13.5px; font-weight: 700; color: var(--ink-soft); margin-bottom: 6px; }
.field input, .field textarea, .field select { width: 100%; padding: 11px 13px; border-radius: var(--radius-sm); border: 1.5px solid var(--border); font-family: var(--font-body); font-size: 15px; background: var(--surface); color: var(--ink); transition: border-color var(--motion-fast) var(--ease); }
.field input:focus, .field textarea:focus, .field select:focus { border-color: var(--accent); outline: none; }

/* Native <select> gets the browser's own dropdown chrome by default,
   which clashes with the site's custom-drawn nav dropdowns — reset it
   and draw the same chevron used there instead. */
.field select {
  appearance: none; -webkit-appearance: none; -moz-appearance: none;
  padding-right: 40px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23726a62' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 14px center; background-size: 15px;
}

/* Icon inside the input — used by the auth modal's fields. */
.field-icon { position: relative; }
.field-icon svg { position: absolute; left: 13px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; color: var(--label); pointer-events: none; }
.field-icon input { padding-left: 38px; }

.link-btn {
  background: none; border: none; padding: 0; margin: 0; cursor: pointer;
  font-family: var(--font-body); font-size: 13px; font-weight: 600; color: var(--label);
  text-decoration: underline; text-underline-offset: 2px;
}
.link-btn:hover { color: var(--accent-dark); }

.field-error { display: none; margin: 6px 0 0; font-size: 12.5px; font-weight: 600; color: var(--danger); }
.field.has-error input, .field.has-error textarea, .field.has-error select { border-color: var(--danger); }
.field.has-error .field-error { display: block; }
.field.is-shaking { animation: fieldShake 420ms var(--ease); }
@keyframes fieldShake {
  10%, 90% { transform: translateX(-1px); }
  20%, 80% { transform: translateX(2px); }
  30%, 50%, 70% { transform: translateX(-5px); }
  40%, 60% { transform: translateX(5px); }
}
@media (prefers-reduced-motion: reduce) { .field.is-shaking { animation: none; } }

/* Submit → spinner → checkmark, each state a smooth handoff from the
   last rather than a hard swap: .btn already transitions background/
   color (see the base .btn rule), so is-loading fading the label to
   transparent and is-success fading the background to green both ride
   that same transition — only the spinner/checkmark need their own
   keyframes on top. */
.btn.is-loading { position: relative; color: transparent; pointer-events: none; }
.btn.is-loading::after {
  content: ''; position: absolute; left: 50%; top: 50%; width: 18px; height: 18px;
  margin: -9px 0 0 -9px; border-radius: 50%;
  border: 2.5px solid oklch(100% 0 0 / 0.4); border-top-color: var(--on-accent);
  animation: btnSpin 650ms linear infinite;
}
@keyframes btnSpin { to { transform: rotate(360deg); } }

.btn.is-success { background: var(--success); color: var(--on-accent); cursor: default; animation: btnSuccessPop 420ms var(--ease); }
.btn.is-success svg { width: 18px; height: 18px; animation: checkPop 380ms var(--ease); }
@keyframes checkPop {
  0% { transform: scale(0.4); opacity: 0; }
  60% { transform: scale(1.15); opacity: 1; }
  100% { transform: scale(1); }
}
@keyframes btnSuccessPop {
  0% { transform: scale(1); }
  35% { transform: scale(1.05); }
  100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .btn.is-loading::after { animation-duration: 1400ms; }
  .btn.is-success, .btn.is-success svg { animation: none; }
}
@media (max-width: 640px) { .form-grid { grid-template-columns: 1fr; } .form-grid .full { grid-column: auto; } }

/* Slides/fades in rather than popping straight to visible — max-height
   (not display) is what's animated, since display can't transition, and
   it collapsing to 0 at rest means it takes no space and gently pushes
   the fields below it down as it opens, instead of them jumping. */
.notice {
  border: 1.5px solid var(--accent); background: var(--accent-soft); color: var(--accent-dark);
  border-radius: var(--radius-sm); font-weight: 700; font-size: 14.5px;
  max-height: 0; opacity: 0; padding: 0 16px; margin-bottom: 0; overflow: hidden; pointer-events: none;
  transform: translateY(-6px) scale(0.98);
  transition: max-height 380ms var(--ease), opacity 280ms var(--ease), transform 320ms var(--ease),
    padding 380ms var(--ease), margin-bottom 380ms var(--ease);
}
.notice.is-visible {
  max-height: 120px; opacity: 1; padding: 12px 16px; margin-bottom: 4px; pointer-events: auto;
  transform: translateY(0) scale(1);
}
.notice.is-error { border-color: var(--danger); background: var(--danger-soft); color: var(--danger); }
/* Scoped to is-success rather than recoloring the base .notice — that
   class is shared by success AND error/info banners across other pages
   (profile updates, reset-password, the editor's status banner), which
   all rely on it staying the brand accent colour. */
.notice.is-success { border-color: var(--success); background: var(--success-soft); color: var(--success); }

/* ---------------- Contact section ---------------- */
.contact-grid { align-items: start; gap: 48px; }
.contact-intro .lede { margin-top: 10px; }
.contact-methods { list-style: none; padding: 0; margin: 26px 0 0; display: flex; flex-direction: column; gap: 14px; }
.contact-methods li { display: flex; align-items: center; gap: 14px; font-weight: 600; font-size: 15px; color: var(--ink-soft); }
.contact-icon {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 40px; height: 40px; border-radius: 50%; background: var(--accent-soft); color: var(--accent-dark);
}
.contact-icon .icon { width: 17px; height: 17px; }
.contact-form {
  position: relative; overflow: hidden;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card); padding: 32px;
}
@media (max-width: 640px) { .contact-form { padding: 24px; } }
@media (max-width: 480px) { .contact-form { padding: 20px; } }

/* Send animation: each field collapses toward the card's centre as if
   drawn into an envelope, which then pops in, puffs up and flies off —
   a decorative stand-in for the network request happening underneath
   (initContactForm holds the success state for as long as the animation
   takes, see main.js). The envelope sits absolutely positioned over the
   grid the whole time at scale(0)/opacity:0, so it costs nothing to lay
   out and only appears once .is-sending plays its keyframes. */
.contact-envelope {
  position: absolute; left: 50%; top: 50%; width: 76px; height: 56px;
  transform: translate(-50%, -50%) scale(0); opacity: 0; pointer-events: none; z-index: 5;
  color: var(--accent-dark); filter: drop-shadow(0 10px 18px oklch(20% 0.03 40 / 0.25));
}
.contact-envelope svg { width: 100%; height: 100%; display: block; }

.contact-form.is-sending .form-grid > .field {
  animation: contactFieldSwallow 460ms var(--ease) forwards;
}
.contact-form.is-sending .form-grid > .field:nth-child(1) { animation-delay: 0ms; }
.contact-form.is-sending .form-grid > .field:nth-child(2) { animation-delay: 60ms; }
.contact-form.is-sending .form-grid > .field:nth-child(3) { animation-delay: 120ms; }
.contact-form.is-sending .form-grid > .field:nth-child(4) { animation-delay: 180ms; }
.contact-form.is-sending .form-grid > .field:nth-child(5) { animation-delay: 240ms; }
@keyframes contactFieldSwallow {
  to { transform: scale(0.2) translateY(28px); opacity: 0; }
}

.contact-form.is-sending .contact-envelope {
  animation: contactEnvelopeAppear 380ms var(--ease) 260ms forwards,
    contactEnvelopeFly 520ms var(--ease) 700ms forwards;
}
@keyframes contactEnvelopeAppear {
  0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
  60% { transform: translate(-50%, -50%) scale(1.12); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
@keyframes contactEnvelopeFly {
  0% { transform: translate(-50%, -50%) scale(1) rotate(0); opacity: 1; }
  100% { transform: translate(120%, -260%) scale(0.5) rotate(18deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .contact-form.is-sending .form-grid > .field,
  .contact-form.is-sending .contact-envelope { animation: none; }
}

/* ---------------- Profile page ---------------- */
.profile-guest { text-align: center; padding: 48px 32px; }
@media (max-width: 480px) { .profile-guest { padding: 36px 20px; } }
.profile-guest .btn { margin-top: 18px; }
.profile-form { display: flex; flex-direction: column; gap: 18px; }
/* Same [hidden]-loses-to-display bug as .comment-guest above — without
   this, the (empty) profile form stays visible to signed-out visitors
   right alongside the "sign in to view your profile" prompt. */
.profile-form[hidden] { display: none; }
.profile-avatar-row { display: flex; align-items: center; gap: 18px; }
.profile-avatar-wrap { position: relative; flex-shrink: 0; }
.profile-avatar { width: 88px; height: 88px; border-radius: 50%; object-fit: cover; background: var(--surface-sunken); border: 1px solid var(--border); display: block; }
.profile-avatar-edit {
  position: absolute; right: -2px; bottom: -2px; width: 30px; height: 30px; border-radius: 50%;
  background: var(--ink); color: var(--surface); border: 2px solid var(--surface);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  transition: background var(--motion-fast) var(--ease), transform 160ms var(--ease);
}
.profile-avatar-edit:hover { background: var(--accent-dark); transform: scale(1.06); }
.profile-avatar-edit svg { width: 14px; height: 14px; }
.profile-avatar-hint { margin: 0; font-size: 13px; color: var(--label); }
.profile-form .form-note { margin-top: -4px; }

.empty { text-align: center; padding: 48px 20px; color: var(--label); }

/* ---------------- Back to top ---------------- */
.back-to-top {
  position: fixed; right: 22px; bottom: 22px; z-index: 60;
  width: 44px; height: 44px; border-radius: 50%; border: none;
  background: var(--ink); color: var(--surface); box-shadow: var(--shadow-card);
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  opacity: 0; transform: translateY(14px) scale(0.9); pointer-events: none;
  transition: opacity 260ms var(--ease), transform 260ms var(--ease), background 200ms var(--ease);
}
.back-to-top.is-visible { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.back-to-top:hover { background: var(--accent-dark); }
.back-to-top svg { width: 19px; height: 19px; }
@media (max-width: 640px) { .back-to-top { right: 16px; bottom: 16px; } }

/* ---------------- Install / notifications banner (PWA) ---------------- */
.install-banner {
  position: fixed; left: 22px; bottom: 22px; z-index: 55; display: flex; align-items: center; gap: 14px;
  max-width: 400px; padding: 16px 16px 16px 14px; border-radius: var(--radius-lg);
  background: var(--surface); box-shadow: var(--shadow-modal); border: 1px solid var(--border);
  transform: translateY(24px) scale(0.98); opacity: 0; pointer-events: none;
  transition: transform 320ms var(--ease), opacity 280ms var(--ease);
}
.install-banner.is-visible { transform: translateY(0) scale(1); opacity: 1; pointer-events: auto; }
.install-banner-icon { width: 44px; height: 44px; border-radius: 12px; flex-shrink: 0; }
.install-banner-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.install-banner-text strong { font-size: 14.5px; color: var(--ink); }
.install-banner-text span { font-size: 12.5px; color: var(--label); line-height: 1.4; }
.install-banner-actions { display: flex; flex-direction: column; gap: 6px; align-items: stretch; flex-shrink: 0; }
.install-banner-close {
  position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; border-radius: 50%;
  border: none; background: var(--surface-sunken); color: var(--ink-soft); font-size: 14px; line-height: 1; cursor: pointer;
}
@media (max-width: 480px) {
  .install-banner { left: 12px; right: 12px; bottom: 12px; max-width: none; }
}

/* ---------------- Scroll reveal ---------------- */
.reveal { opacity: 0; transform: translateY(36px); transition: opacity 700ms var(--ease), transform 700ms var(--ease); }
.reveal.is-visible { opacity: 1; transform: translateY(0); }

/* Grid/list/collage children cascade in one after another rather than
   all fading in at once. box-shadow rides along in the same transition
   declaration as a deliberate choice, not an oversight: this selector
   outranks .collage-item/.ad-card's own hover transition by specificity,
   so it's the one actually in effect for their transform — including
   box-shadow here keeps their hover lift/shadow smooth instead of
   snapping once they sit inside a .reveal section. */
.reveal :is(.grid, .list, .footer-grid, .cal-agenda, .posts-collage) > * {
  opacity: 0; transform: translateY(18px);
  transition: opacity 560ms var(--ease), transform 560ms var(--ease), box-shadow 380ms var(--ease);
}
.reveal.is-visible :is(.grid, .list, .footer-grid, .cal-agenda, .posts-collage) > * {
  opacity: 1; transform: translateY(0);
}
.reveal.is-visible :is(.grid, .list, .footer-grid, .cal-agenda, .posts-collage) > *:nth-child(1) { transition-delay: 60ms; }
.reveal.is-visible :is(.grid, .list, .footer-grid, .cal-agenda, .posts-collage) > *:nth-child(2) { transition-delay: 120ms; }
.reveal.is-visible :is(.grid, .list, .footer-grid, .cal-agenda, .posts-collage) > *:nth-child(3) { transition-delay: 180ms; }
.reveal.is-visible :is(.grid, .list, .footer-grid, .cal-agenda, .posts-collage) > *:nth-child(4) { transition-delay: 240ms; }
.reveal.is-visible :is(.grid, .list, .footer-grid, .cal-agenda, .posts-collage) > *:nth-child(5) { transition-delay: 300ms; }
.reveal.is-visible :is(.grid, .list, .footer-grid, .cal-agenda, .posts-collage) > *:nth-child(6) { transition-delay: 360ms; }

@media (min-width: 901px) {
  /* Hero image drifts as the page scrolls (see the parallax handler in
     reveal.js) — the extra top/height buffer keeps that drift from ever
     exposing an edge past the clipped banner. Sized to the drift's max
     translateY (130px) with a little headroom. */
  .banner-art { top: -140px; height: calc(100% + 280px); will-change: transform; }
}

/* ---------------- CTA band ---------------- */
.cta-band { background: var(--ink); color: var(--surface); border-radius: var(--radius-lg); padding: 40px; display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
.cta-band h2 { color: var(--surface); margin-bottom: 4px; }
.cta-band p { color: oklch(85% 0.01 60); margin: 0; }
.cta-band .btn-primary { background: var(--accent); }
@media (max-width: 640px) { .cta-band { padding: 28px 24px; gap: 18px; } }

/* ---------------- Footer ---------------- */
.site-footer { border-top: 1px solid var(--border); padding: 48px 0 28px; margin-top: 40px; }
.footer-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 32px; margin-bottom: 32px; }
.footer-col h4 { font-size: 13px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--label); margin-bottom: 12px; font-family: var(--font-body); }
.footer-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.footer-col a { color: var(--ink-soft); text-decoration: none; font-size: 14.5px; }
.footer-col a:hover { color: var(--accent-dark); }
.footer-brand p { color: var(--ink-soft); font-size: 14.5px; max-width: 34ch; }
.footer-donate-btn { display: inline-flex; margin-top: 16px; }
.footer-col a.footer-donate-btn, .footer-col a.footer-donate-btn:hover { color: var(--on-accent); font-size: 14px; }
.footer-donate-btn svg { width: 15px; height: 15px; }
.footer-bottom { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding-top: 24px; border-top: 1px solid var(--border); font-size: 13px; color: var(--label); }
@media (max-width: 860px) { .footer-grid { grid-template-columns: 1fr 1fr; } }
@media (max-width: 480px) { .footer-grid { grid-template-columns: 1fr; } }

/* ---------------- Auth modal ---------------- */
.modal-overlay {
  position: fixed; inset: 0; z-index: 100; background: oklch(20% 0.02 60 / 0.45);
  display: flex; align-items: center; justify-content: center; padding: 36px 20px;
  opacity: 0; visibility: hidden; transition: opacity var(--motion-base) var(--ease), visibility var(--motion-base);
}
.modal-overlay.is-open { opacity: 1; visibility: visible; }
.modal-panel {
  width: 100%; max-width: 440px; max-height: 100%; overflow-y: auto;
  background: var(--surface); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-modal); padding: 38px 36px; position: relative;
  transform: translateY(10px) scale(0.98); opacity: 0;
  transition: transform var(--motion-base) var(--ease), opacity var(--motion-base) var(--ease);
}
@media (max-width: 480px) {
  .modal-overlay { padding: 20px 14px; }
  .modal-panel { padding: 28px 22px; }
  .modal-split-form { padding: 28px 22px; }
}
.modal-overlay.is-open .modal-panel { transform: translateY(0) scale(1); opacity: 1; }
.modal-panel.is-wide { max-width: 460px; }

/* Login modal: opened via JS with an inline transform-origin pinned to
   the triggering button and a small starting scale, then released here
   with a slower, springier transition so it visibly grows out of the
   button instead of just fading in centered on screen. */
.modal-panel.is-growing { transition: transform 460ms cubic-bezier(.2, .8, .2, 1), opacity 320ms var(--ease); }

/* Auth modal: form on the left, a photo panel with the brand mark on
   the right. Below the split's breakpoint there isn't room for both,
   so it collapses back to a single form-only column.

   The panel gets an explicit (not just capped) height so the two
   columns have a definite size to work with: the form scrolls its own
   content internally rather than the whole panel scrolling, which
   means the photo column — stretched to match that same definite
   height — always fills edge-to-edge instead of being cut short when
   the signup form runs long. */
.modal-panel.is-split {
  max-width: 860px; padding: 0; display: flex; align-items: stretch;
  height: min(680px, calc(100vh - 72px)); overflow: hidden;
}
.modal-split-form { flex: 1 1 54%; min-width: 0; padding: 38px 36px; overflow-y: auto; }
.modal-split-media {
  flex: 1 1 46%; position: relative; height: 100%; border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  overflow: hidden;
}
.modal-split-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; z-index: 0; }
.modal-split-media::before { content: ''; position: absolute; inset: 0; z-index: 1; background: linear-gradient(190deg, oklch(20% 0.03 40 / 0.1), oklch(8% 0.02 40 / 0.68)); }
.modal-split-brand { position: absolute; right: 20px; bottom: 18px; display: flex; align-items: center; gap: 8px; z-index: 2; }
.modal-split-brand .brand-dot { width: 20px; height: 20px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
.modal-split-brand .brand-word { font-family: var(--font-logo); font-weight: 600; font-size: 18px; color: #fff; text-shadow: 0 1px 6px oklch(10% 0.02 40 / 0.55); }
@media (max-width: 760px) {
  .modal-panel.is-split { max-width: 440px; flex-direction: column; height: min(600px, calc(100vh - 72px)); }
  .modal-split-media { display: none; }
}

/* Brand lockup repeated above the tabs, in the form column itself, so
   it stays visible even on narrow screens where the photo panel (the
   only other place the logo appears) is hidden. */
.modal-brand { margin-bottom: 22px; }
.modal-brand .brand-word { font-size: 19px; }

.modal-close { position: absolute; top: 16px; right: 16px; width: 32px; height: 32px; border-radius: 50%; border: none; background: var(--surface-sunken); color: var(--ink-soft); font-size: 18px; line-height: 1; cursor: pointer; z-index: 2; }
.modal-close:hover { background: var(--border); color: var(--ink); }
.modal-tabs { position: relative; display: flex; background: var(--surface-sunken); border-radius: 999px; padding: 4px; margin-bottom: 28px; }
.modal-tab { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px; border: none; background: transparent; padding: 10px 0; border-radius: 999px; font-weight: 700; font-size: 14px; color: var(--label); cursor: pointer; position: relative; z-index: 1; transition: color var(--motion-fast) var(--ease); }
.modal-tab .icon { width: 15px; height: 15px; }
.modal-tab.is-active { color: var(--surface); }
.modal-tab-indicator { position: absolute; top: 4px; left: 4px; width: calc(50% - 4px); height: calc(100% - 8px); background: var(--ink); border-radius: 999px; transition: transform var(--motion-base) var(--ease); }
.modal-tabs[data-active="signup"] .modal-tab-indicator { transform: translateX(100%); }

.social-auth { display: flex; flex-direction: column; gap: 10px; margin-bottom: 26px; }
.btn-social {
  display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%;
  padding: 11px 16px; border-radius: 999px; border: 1.5px solid var(--border);
  background: var(--surface); color: var(--ink); font-family: var(--font-body);
  font-weight: 600; font-size: 14.5px; cursor: pointer;
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease), transform var(--motion-fast) var(--ease);
}
.btn-social:hover { background: var(--surface-sunken); border-color: var(--ink-soft); }
.btn-social:active { transform: scale(0.98); }
.btn-social svg { width: 18px; height: 18px; flex-shrink: 0; }

.modal-divider { display: flex; align-items: center; gap: 12px; margin-bottom: 26px; color: var(--label); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
.modal-divider::before, .modal-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }

/* Both forms are stacked in the same grid cell so the panel's height
   is always the taller of the two — switching tabs no longer resizes
   the modal, it just swaps which stacked form is visible. */
.modal-forms { display: grid; }
.modal-form { display: flex; flex-direction: column; gap: 18px; grid-area: 1 / 1; }
.modal-form[hidden] { display: flex; visibility: hidden; pointer-events: none; }
.modal-form .form-note { margin-top: -4px; }
.modal-form .btn-primary { justify-content: center; }

/* ---------------- Confirm/prompt modal (assets/js/confirm-modal.js) ---------------- */
.confirm-panel { max-width: 400px; padding: 30px 28px; }
.confirm-title { margin: 0 0 8px; font-family: var(--font-display); font-size: 21px; }
.confirm-body { margin: 0; color: var(--ink-soft); font-size: 14.5px; line-height: 1.55; }
.confirm-textarea {
  width: 100%; margin-top: 16px; padding: 12px 14px; border-radius: var(--radius-md);
  border: 1.5px solid var(--border); background: var(--surface); color: var(--ink);
  font-family: var(--font-body); font-size: 14.5px; resize: vertical;
}
.confirm-textarea:focus { border-color: var(--accent); outline: none; }
.confirm-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 22px; }
.btn-danger { background: var(--danger); color: #fff; border-color: var(--danger); }
.btn-danger:hover { filter: brightness(1.08); }

/* ---------------- PayFast redirect overlay (assets/js/payments.js) ---------------- */
.payfast-redirect-overlay {
  position: fixed; inset: 0; z-index: 300; background: var(--bg);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity 260ms var(--ease);
}
.payfast-redirect-overlay.is-visible { opacity: 1; }
.payfast-redirect-card { display: flex; flex-direction: column; align-items: center; gap: 16px; text-align: center; }
.payfast-redirect-card .brand-dot { width: 34px; height: 34px; border-radius: 50%; background: var(--accent); }
.payfast-redirect-card p { margin: 0; color: var(--ink-soft); font-weight: 600; font-size: 15px; }
.payfast-spinner {
  width: 26px; height: 26px; border-radius: 50%; border: 3px solid var(--border); border-top-color: var(--accent);
  animation: payfastSpin 700ms linear infinite;
}
@keyframes payfastSpin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .payfast-spinner { animation-duration: 1400ms; } }

/* Admin-only "Write as" panel in the editor sidebar */
.editor-penname-hint { font-size: 12.5px; color: var(--label); margin: 0 0 10px; line-height: 1.45; }
.editor-penname-current { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.editor-penname-current img, .editor-penname-photo img { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.editor-penname-form { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); }
.editor-penname-form[hidden], .editor-penname-current[hidden], #editor-penname-card[hidden] { display: none; }
.editor-penname-photo { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.editor-penname-actions { display: flex; gap: 8px; }
.editor-penname-msg { font-size: 12.5px; color: var(--danger); margin: 8px 0 0; min-height: 1em; }
