/* AgendAI · common page chrome (header, footer, rail).
   Authored fresh from the inline styles in docs/Website design/c-week-desktop.html.
   tokens.css is loaded first; this file uses var(--…) throughout. */

body { background: var(--cream); }

/* Global focus-visible. Browser default focus rings are often invisible
   on our custom-styled buttons, links, and inputs (ink / cream tokens
   produce low-contrast outlines). Keyboard users (Tab) need a visible
   focus indicator — this is the WCAG 2.4.7 baseline. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: 4px;
}
/* Cards live on pastel format backgrounds (fmt-meetup, fmt-workshop, ...);
   an ink outline disappears into the dark format inks. Use cream and pull
   the offset inside the card so the ring sits on the colored field. */
.ev.m4-card:focus-visible,
.me:focus-visible,
.related-mini:focus-visible {
  outline-color: var(--cream);
  outline-offset: -2px;
}

/* TODO[digest-backend]: weekly-email subscribe block hidden until the
   ingest backend is wired (capture endpoint + welcome flow + unsubscribe).
   Markup still ships in the DOM — delete this single rule to re-enable
   both the desktop rail .digest and the mobile-modal .modal-digest. */
[data-feature="digest"] { display: none !important; }

.page {
  width: 100%;
  min-width: 1280px;
  min-height: 100vh;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
}

/* Per-page overrides — week-page and month-page already drop min-width below
   to fit the responsive grids. Event-detail and 404 do the same so they don't
   force a 1280px viewport on mobile (no horizontal scroll). */
.event-detail-page,
.not-found-page { min-width: 0; }

/* Issue 3: directory pages (week/month) lock to viewport height; the body
   itself never scrolls. Day-cells (week) / month-grid (month) take the
   remaining space and scroll internally if their content overflows.
   Mobile media query below relaxes this so vertical-stacked day cards can
   scroll the page (mobile usability beats strict viewport rule). */
html:has(body > .page.week-page),
html:has(body > .page.month-page) {
  height: 100%;
}
body:has(> .page.week-page),
body:has(> .page.month-page) {
  height: 100vh;
  overflow: hidden;
  margin: 0;
}
.page.week-page,
.page.month-page {
  height: 100vh;
  min-height: 0;
  overflow: hidden;
}

/* ── header ── */
header.top {
  padding: 14px 28px;
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
  display: flex;
  align-items: center;
  gap: 24px;
}
header.top .masthead .brand {
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1;
}
header.top .masthead .site-tagline {
  font-family: var(--sans);
  font-size: 10px;
  line-height: 1.15;
  color: var(--ink-mute);
  margin: 0;
  letter-spacing: 0.2px;
}
header.top .page-title {
  font-family: var(--serif);
  font-size: 30px;
  line-height: 1;
  color: var(--ink);
  margin: 0;
  font-weight: 400;
  white-space: nowrap;
  /* Long titles (e.g. "Related to: <long event title>" on the related-
     schedule page) ellipsis at the title-block boundary instead of
     overflowing into the header-nav (Today / Week / Month / Add buttons). */
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
header.top .header-nav {
  display: flex;
  align-items: center;
  gap: 18px;
}
header.top .header-nav-step {
  display: flex;
  align-items: center;
  gap: 6px;
}
/* Title block: month label + (Week N · M events) subline. Side-by-side on
   desktop (current visual) — stacks vertically below 720px so the subline
   sits tight under the month rather than orphaned on its own row. */
header.top .title-block {
  display: flex;
  align-items: baseline;
  gap: 18px;
  min-width: 0;
}
/* View-switch + filter button bundle. Wrapping them in one flex item
   prevents the filter funnel from orphaning to its own row on narrow
   viewports — they wrap as a unit. */
header.top .view-switch-bundle {
  display: flex;
  align-items: center;
  gap: 10px;
}
/* Height-match the WEEK | MONTH links to 32px so they align with their
   row neighbors (.nav-btn and .today-btn, both 32px tall). Tokens-css
   ships `.view-switch a { padding: 7px 14px }` which renders ~24px;
   that's a visible height mismatch when all three live in one row.
   min-width equalizes the two cells (otherwise "Week" is 56px and
   "Month" is 62px because text widths differ — the active one then
   looks smaller than its inactive neighbor). */
header.top .view-switch a {
  height: 32px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 64px;
  box-sizing: border-box;
}

/* Add Event button — desktop top-right anchor in the header nav. Hidden
   on mobile (the mobile filter modal carries an "+ Add event" link
   next to the Search input instead). */
header.top .add-event-btn {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 0 12px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  white-space: nowrap;
}
header.top .add-event-btn:hover {
  background: var(--cream-mid);
  border-color: var(--ink);
}
.nav-btn[disabled],
.nav-btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}
a.nav-btn,
a.today-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}
/* Mobile chrome wrap: header masthead is fixed-width inline (184px) and the
   page-title is white-space:nowrap; below ~720px the row would overflow. Let
   it wrap and tighten padding so event-detail / 404 fit a phone viewport. */
@media (max-width: 720px) {
  header.top {
    flex-wrap: wrap;
    gap: 12px;
    padding: 12px 16px;
  }
  header.top .page-title {
    font-size: 22px;
    white-space: normal;
    text-wrap: balance;
  }
  header.top .title-block {
    /* Stack "May 2026" (h1) over "WEEK 20 · 18 EVENTS" (subline) on
       mobile — keeps them visually grouped as a single title unit
       instead of the subline orphaning to its own row. */
    flex-direction: column;
    align-items: flex-start;
    gap: 4px;
  }
  /* Desktop add-event button hidden on mobile — replaced by the
     "+ Add event" link inside the filter modal's Search group. */
  header.top .add-event-btn { display: none; }
  header.top .header-nav {
    /* On mobile the nav takes its own full-width row, so the nav-step
       group anchors to the left edge and the view-switch + filter bundle
       anchors to the right edge — feels balanced rather than crammed
       against the left margin with a long empty band on the right. The
       6px gap is the minimum when the row is tight (only used if both
       groups grow enough to meet in the middle, which they don't). */
    gap: 6px;
    flex-wrap: wrap;
    flex-basis: 100%;
    justify-content: space-between;
  }
  /* Tap-target sizing — WCAG 2.5.5 wants 44×44; we land at 40 with adequate
     spacing on either side (gap: 6px), which clears WCAG 2.2 minimum
     (24×24 or 24px-spaced). The desktop default for these controls is
     32×32 from tokens.css; bumping inside this @media keeps the desktop
     row neighbors at their tight 32px and only widens for thumbs. */
  .nav-btn {
    width: 40px;
    height: 40px;
  }
  .today-btn,
  header.top .view-switch a,
  header.top .add-event-btn,
  .header-filter-btn {
    height: 40px;
  }
  footer.foot {
    /* Stack the descriptive paragraph and the "A project by ..." credit
       vertically on mobile. Without this override the .foot-left flex:1
       fights flex-wrap and the credit floats mid-height of the paragraph
       column — fixes a regression introduced when foot-left was made to
       fill available width on wide screens. */
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    padding: 12px 16px;
  }
}

/* ── filter rail ── */
aside.rail {
  border-right: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
  padding: 0;
}
.rail-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  /* Vertical padding matches .month-head .col (10px each side) so the
     "Filters" row's border-bottom and the day-of-week strip's
     border-bottom land on the same y-coordinate — the page-wide
     horizontal raster line is continuous from rail to grid. */
  padding: 10px 18px;
  border-bottom: 1px solid var(--rule);
  flex-shrink: 0;
}
.rail-head-title {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.rail-reset {
  /* Hidden until a filter / search is active (body class toggled by
     filters.js). visibility:hidden reserves the row width so nothing
     jumps when reset appears. Sized lean (no border + no extra padding)
     so it sits inside the same 10px line-height as .rail-head-title,
     matching .month-head .col height exactly. */
  visibility: hidden;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  line-height: 1;
  color: var(--ink-soft);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-color: var(--rule);
  cursor: pointer;
}
.rail-reset:hover { color: var(--ink); text-decoration-color: var(--ink); }
body.has-active-filters .rail-reset,
body.has-active-filters .mobile-filter-modal .modal-head .reset {
  visibility: visible;
}
.search-row {
  position: relative;
  display: flex;
  align-items: center;
}
.search-row input[type="search"] {
  width: 100%;
  height: 32px;
  padding: 0 28px 0 10px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--paper);
  font-family: var(--sans);
  /* 16px keeps iOS Safari from auto-zooming on focus. Worth the slight
     visual heaviness vs. previous 13px. Height bumped 30→32 to match. */
  font-size: 16px;
  color: var(--ink);
}
.search-row input[type="search"]::-webkit-search-cancel-button { display: none; }
.search-clear {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  width: 22px;
  height: 22px;
  border: 0;
  background: transparent;
  color: var(--ink-mute);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
}
.search-clear:hover { color: var(--ink); background: var(--cream-mid); }
.search-clear[hidden] { display: none; }
aside.rail .filters {
  flex: 1 1 0;
  overflow-y: auto;
  /* Safari: gutter-less overlay scrollbar (auto-hides per OS pref).
     Chromium ≥120 silently treats `overlay` as `auto`. Firefox same. */
  overflow-y: overlay;
  padding: 16px 18px 10px;
  min-height: 0;
}
aside.rail .digest {
  flex-shrink: 0;
  padding: 12px 18px 14px;
  background: var(--cream-mid);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
aside.rail .digest .label {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--ink-mute);
  display: flex;
  align-items: center;
  gap: 6px;
}
aside.rail .digest .label::before {
  content: '';
  display: inline-block;
  width: 12px;
  height: 1px;
  background: var(--gold-deep);
}
aside.rail .digest .head {
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.4;
  color: var(--ink);
  font-weight: 400;
  text-wrap: pretty;
}
/* Issue 2: empty- vs filtered-state CTA copy. Body class toggles which is
   visible. Default = empty; .has-active-filters swaps to filtered copy. */
aside.rail .digest .cta-filtered { display: none; }
body.has-active-filters aside.rail .digest .cta-empty { display: none; }
body.has-active-filters aside.rail .digest .cta-filtered { display: block; }
.mobile-filter-modal .modal-digest .cta-filtered { display: none; }
body.has-active-filters .mobile-filter-modal .modal-digest .cta-empty { display: none; }
body.has-active-filters .mobile-filter-modal .modal-digest .cta-filtered { display: inline; }
aside.rail .digest form {
  display: flex;
  gap: 0;
  margin-top: 4px;
}
aside.rail .digest input {
  flex: 1;
  min-width: 0;
  height: 30px;
  padding: 0 10px;
  border: 1px solid var(--rule);
  border-right: 0;
  border-radius: 4px 0 0 4px;
  background: var(--paper);
  font-family: var(--sans);
  font-size: 11px;
  color: var(--ink);
}
aside.rail .digest input::placeholder { color: var(--ink-faint); }
aside.rail .digest button {
  height: 30px;
  padding: 0 10px;
  background: var(--ink);
  color: var(--cream);
  border: 0;
  border-radius: 0 4px 4px 0;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  cursor: pointer;
}
aside.rail .digest button:hover { background: var(--ink-soft); }
aside.rail h4 {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-soft);
  margin: 0 0 8px;
}
aside.rail .group { margin-bottom: 16px; }

/* ── Filter visibility (driven by [hidden] attr on .ev/.me/.dot) ─────────
   filters.js owns the per-card `hidden` attribute (which UA styles render
   as display: none via the built-in `[hidden]{display:none}` rule —
   except for our explicit `.ev[hidden]{display:none !important}` for
   .ev cards below, which guards against later flex-display rules).

   The bug class this replaces: previously filter state lived in URL hash
   + events.json (async fetch) + applyFilters running after promise resolve.
   First paint showed cards before filters were applied, then a frame later
   they hid. Race conditions abounded.

   New flow — fully synchronous, no async:
     1. Inline <head> script in meta.ts:filterFoucHeadScript() parses the
        URL hash SYNCHRONOUSLY, sets html classes (f-loc-*, f-city-active
        + f-city-{slug}, f-audience-..., f-format-...), sets html.dataset.q,
        and injects a <style id="filter-fouc"> block of `display: none`
        rules so that non-matching cards never paint visibly even on first
        load.
     2. Each .ev / .me / .dot card carries data-loc, data-city, data-audience
        (space-separated), data-format (space-separated), data-search
        (lowercased haystack).
     3. On DOMContentLoaded, filters.js walks all cards once, sets `hidden`
        attribute to mirror the FOUC <style>, then removes the FOUC block.
        From that point on, `[hidden]` is the single source of truth and
        the `:has(...:not([hidden]))` view-flip below works.
     4. Filter clicks / search keystrokes update html classes + per-card
        `hidden` attrs in the same synchronous handler. URL hash is
        replaceState-d in the same tick.

   Result: zero async, zero Promise, zero coordination layer. Filter
   visibility is determined synchronously from URL hash on every entry
   point (initial parse, click, hashchange, search). */

/* Active-filter chips row — mobile-only band beneath the header
   showing one removable pill per active filter (city / format /
   audience / location / q). Desktop users have the rail showing the
   same state so chips are redundant there. The row is `hidden` by
   default; filters.js removes the attribute when there's something
   to show. */
.active-chips-row { display: none; }
@media (max-width: 720px) {
  .active-chips-row:not([hidden]) {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 8px 16px;
    background: var(--paper);
    border-bottom: 1px solid var(--rule);
  }
}
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 26px;
  padding: 0 4px 0 10px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.4px;
  color: var(--ink);
  background: var(--cream-mid);
  border: 1px solid var(--rule);
  border-radius: 14px;
  white-space: nowrap;
}
.filter-chip-x {
  width: 20px;
  height: 20px;
  border: 0;
  background: transparent;
  cursor: pointer;
  color: var(--ink-mute);
  font-size: 14px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.filter-chip-x:hover { color: var(--ink); }

/* Schedule fallback: hidden by default; shown when the grid would be
   empty after filters/search. Build-time `body[data-view="schedule"]`
   covers sparse pages (set by shouldUseScheduleView in upcoming-schedule.ts);
   runtime `:has()` covers when filters/search collapse the grid to zero.

   The `:has()` predicate reads [hidden] which filters.js owns, so it
   stays in sync without any JS toggle of body[data-view]. Whichever
   trigger fires first wins; both expressing "show the schedule view"
   is fine (idempotent CSS). */
.schedule-fallback {
  display: none;
  padding: 0 16px 32px;
}

/* Mobile-only prev/next week nav at the foot of the week grid. Desktop
   has the same arrows in the header; mobile users on a sparse week
   shouldn't have to scroll back up to switch weeks. */
.week-foot-nav { display: none; }
@media (max-width: 720px) {
  .week-foot-nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 16px;
    border-top: 1px solid var(--rule);
    margin-top: 8px;
  }
  .week-foot-nav a {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    color: var(--ink);
    text-decoration: none;
    padding: 10px 12px;
    border: 1px solid var(--rule);
    border-radius: 4px;
    background: var(--paper);
  }
  .week-foot-nav a:active { background: var(--cream-mid); }
}
body[data-view="schedule"] .schedule-fallback,
.month-page:not(:has(.month-grid .me:not([hidden]))) .schedule-fallback,
.week-page:not(:has(.week-grid .ev:not([hidden]))) .schedule-fallback { display: block; }

/* Empty state when filters/search hide every schedule-day group. Default
   hidden; revealed when no .schedule-day is visible. */
.schedule-empty-filtered { display: none; }
.schedule-fallback:not(:has(.schedule-day:not([hidden]))) .schedule-empty-filtered { display: block; }
/* When the schedule is empty due to filters, hide the "Upcoming events"
   title too — the empty-state copy already explains what to do, the
   redundant header reads as a broken section. */
.schedule-fallback:not(:has(.schedule-day:not([hidden]))) .schedule-fallback-title { display: none; }
body[data-view="schedule"] .week-grid,
body[data-view="schedule"] .month-wrap > .month-head,
body[data-view="schedule"] .month-wrap > .month-grid,
body[data-view="schedule"] .empty-period,
body[data-view="schedule"] .week-foot-nav,
.month-page:not(:has(.month-grid .me:not([hidden]))) .month-wrap > .month-head,
.month-page:not(:has(.month-grid .me:not([hidden]))) .month-wrap > .month-grid,
.week-page:not(:has(.week-grid .ev:not([hidden]))) .week-grid,
.week-page:not(:has(.week-grid .ev:not([hidden]))) .empty-period,
/* Schedule view spans many weeks of upcoming events; the per-week prev/next
   foot-nav (mobile only) reads as scoping confusion in that mode. Hide it
   whenever the schedule fallback is showing — both build-time and
   filter-collapse triggers. */
.week-page:not(:has(.week-grid .ev:not([hidden]))) .week-foot-nav { display: none; }
/* The schedule view often runs taller than the viewport (up to 30 cards
   grouped by day). The default .week-content has overflow: hidden so
   the calendar grid stretches to fill it; in schedule mode we need
   that container to scroll its content instead. Both the build-time
   (body[data-view=schedule]) AND runtime filter-collapse (:has()) paths
   reveal the schedule view; both need scroll. (Month page's .month-wrap
   already has overflow-y:auto by default at line ~2123 — no override
   needed there.) */
body[data-view="schedule"] .week-page .week-content,
.week-page:not(:has(.week-grid .ev:not([hidden]))) .week-content {
  overflow-y: auto;
}
.schedule-fallback-title {
  font-family: var(--serif);
  font-size: 22px;
  font-weight: 400;
  color: var(--ink);
  margin: 16px 0 12px;
}
.schedule-empty {
  padding: 32px 16px;
  text-align: center;
  font-family: var(--sans);
  color: var(--ink-mute);
}
aside.rail label,
aside.rail button[data-filter-group] {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
  font-family: var(--sans);
  font-size: 13px;
  color: var(--ink-soft);
  cursor: pointer;
}
/* Reset native button styling so the role="checkbox" buttons inherit the
   same look as the original <label> rows. width:100% so they fill the
   group column; text-align:left so the label sits at the left edge. */
aside.rail button[data-filter-group] {
  background: transparent;
  border: 0;
  margin: 0;
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
}
aside.rail .checkbox {
  width: 12px;
  height: 12px;
  border: 1px solid var(--ink-mute);
  border-radius: 2px;
  background: var(--paper);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--cream);
  font-size: 9px;
}
aside.rail .checkbox.on {
  background: var(--ink);
  border-color: var(--ink);
}
aside.rail .count {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--ink-faint);
  margin-left: auto;
}

.legend {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.legend .row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--ink-soft);
  letter-spacing: 0.4px;
  text-transform: uppercase;
}

/* ── footer ── */
footer.foot {
  padding: 12px 28px;
  border-top: 1px solid var(--rule);
  background: var(--paper);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
  flex-shrink: 0;
}
header.top { flex-shrink: 0; }
.foot-left {
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex: 1;
  min-width: 0;
}
.foot-tag {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.foot-tag a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
}
.foot-tag a:hover { border-bottom-color: var(--ink); }
/* Descriptive SEO/UX prose on the left side of the footer. Sentence-case
   readable text, distinct from the uppercase mono labels on the right.
   Fills the available horizontal space — no max-width cap, since a
   footer is skim-read (not body prose) and a tight column leaves an
   ugly empty band on wide viewports. */
.foot-desc {
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-mute);
  margin: 0;
}
/* The cities + source-count clause is only shown on wide viewports
   (>=1200px). Below that it'd push the paragraph onto 4-5 lines on
   medium screens — the SEO content stays in title / meta / JSON-LD
   and the visible footer just keeps the opener + CTA. */
.foot-desc-wide { display: none; }
@media (min-width: 1200px) {
  .foot-desc-wide { display: inline; }
}
.foot-desc a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
}
.foot-desc a:hover { border-bottom-color: var(--ink); }

/* ───────────────── event detail page ───────────────── */
/* Ported from docs/Website design/C-event-desktop.html (popover styles)
   + C-event-desktop-long.html. The popover-as-overlay design becomes the
   page-as-content design: the same classes and visual rhythm, repositioned
   from a fixed overlay into the page flow. */

main.event-main {
  display: flex; flex-direction: column;
  align-items: center;
  padding: 36px 28px 48px;
  background: var(--cream);
  flex: 1;
}

.event-detail {
  width: 100%;
  max-width: 720px;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-left: 5px solid var(--teal);
  border-radius: 4px;
  display: flex; flex-direction: column;
  overflow: hidden;
}
.event-detail.fmt-workshop   { border-left-color: oklch(0.62 0.13 70); }
.event-detail.fmt-conference { border-left-color: oklch(0.50 0.13 25); }
.event-detail.fmt-networking { border-left-color: oklch(0.55 0.10 285); }
.event-detail.fmt-hackathon  { border-left-color: oklch(0.55 0.13 145); }

.event-detail--long {
  max-width: 820px;
}

.event-status-banner {
  padding: 10px 22px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  border-bottom: 1px solid var(--rule);
}
.event-status-cancelled {
  background: oklch(0.92 0.05 25);
  color: oklch(0.32 0.13 25);
}
.event-status-past {
  background: var(--cream-mid);
  color: var(--ink-mute);
}
.event-status-removed {
  background: var(--cream-deep);
  color: var(--ink-soft);
}

.event-hero {
  padding: 22px 26px 18px;
  display: flex; flex-direction: column; gap: 14px;
}
.event-title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 32px;
  line-height: 1.12;
  margin: 0;
  color: var(--ink);
  text-wrap: pretty;
}

.event-when-where {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink);
}
.event-meta-row {
  display: grid;
  grid-template-columns: 18px 1fr;
  column-gap: 12px;
  align-items: baseline;
}
.event-meta-row .icn {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-mute);
  text-align: center;
}
.event-meta-row .v {
  line-height: 1.4;
}
.event-meta-row small {
  display: block;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-mute);
  letter-spacing: 0.3px;
  margin-top: 2px;
}
.event-flag {
  display: inline-block;
}

.event-register {
  margin-top: 4px;
  align-self: flex-start;
  height: 40px;
  padding: 0 18px;
  background: var(--ink);
  color: var(--cream);
  border-radius: 5px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.9px;
  text-transform: uppercase;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.event-register:hover { background: var(--ink-soft); }

.event-body {
  padding: 18px 26px 22px;
  border-top: 1px solid var(--rule);
  display: flex; flex-direction: column; gap: 16px;
}

.event-description {
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.event-description p { margin: 0 0 12px; }
.event-description p:last-child { margin-bottom: 0; }
.event-description a {
  color: var(--ink);
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
}
.event-description a:hover { border-bottom-color: var(--ink); }

.event-detail--long .event-description {
  font-size: 14.5px;
  line-height: 1.6;
}

.event-tags {
  display: flex; flex-wrap: wrap; gap: 5px;
}

.event-curation {
  background: var(--cream-mid);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 10px 14px;
  display: flex; flex-direction: column; gap: 4px;
}
.event-curation .head {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.event-curation .body {
  font-family: var(--sans);
  font-size: 12.5px;
  color: var(--ink-soft);
}
.event-source-id {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--ink);
}

.event-related {
  width: 100%;
  max-width: 720px;
  margin-top: 32px;
}
.event-related h3 {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0 0 12px;
  font-weight: 500;
}
.related-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
}
.related-card {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 4px;
  padding: 12px 14px;
  text-decoration: none;
  color: var(--ink);
  display: flex; flex-direction: column; gap: 4px;
}
.related-card:hover { box-shadow: var(--lift); }
.related-date {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--ink-mute);
  letter-spacing: 0.3px;
}
.related-title {
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.18;
  color: var(--ink);
  text-wrap: pretty;
}
.related-venue {
  font-family: var(--sans);
  font-size: 11.5px;
  color: var(--ink-mute);
  line-height: 1.3;
}

@media (max-width: 720px) {
  .related-grid { grid-template-columns: 1fr; }
  .event-detail-page main.event-main { padding: 18px 14px 32px; }
}

/* ───────────────── event popover overlay ─────────────────
   Ported from C-event-desktop.html (lines 187-300) and adapted with
   responsive bottom-sheet behaviour from C-event-mobile.html.
   The popover overlays the week view; backdrop blurs content beneath. */

.popover-backdrop {
  position: fixed; inset: 0;
  background: rgba(15, 27, 45, 0.36);
  z-index: 200;
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}
.popover {
  /* Final position is set by week.js (positionPopover) — anchored next to the
     originating card and clamped inside the calendar grid. The values below
     are the SSR fallback used briefly on /event/<slug> direct visits before
     the deferred script runs; JS overrides via inline styles. */
  position: fixed;
  top: 96px;
  right: 24px;
  width: 380px;
  max-height: calc(100vh - 160px);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 10px;
  box-shadow: 0 2px 0 rgba(15,27,45,.04), 0 24px 60px -20px rgba(15,27,45,.45);
  z-index: 201;
  display: flex; flex-direction: column;
  /* Chrome only — scrolling happens in the inner .pop-body so the edge
     fade clips the content, not the border/background of the panel. */
  overflow: hidden;
}
.popover.popover--long { width: 420px; }
.popover .pop-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.popover-backdrop[hidden],
.popover[hidden] { display: none !important; }

/* Calendar columns own scrolling; the popover is anchored inside the grid
   so the page itself never scrolled in the first place. */
body.popover-open { overflow: hidden; }

.popover .pop-head {
  padding: 16px 18px 14px;
  display: flex; flex-direction: column; gap: 10px;
  flex-shrink: 0;
}
.popover .pop-head .meta-row {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.6px;
  text-transform: uppercase; color: var(--ink-mute);
}
.popover .pop-head .meta-row .dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: oklch(0.55 0.09 195);
}
.popover.fmt-workshop   .pop-head .meta-row .dot { background: oklch(0.62 0.13 70); }
.popover.fmt-conference .pop-head .meta-row .dot { background: oklch(0.50 0.13 25); }
.popover.fmt-networking .pop-head .meta-row .dot { background: oklch(0.55 0.10 285); }
.popover.fmt-hackathon  .pop-head .meta-row .dot { background: oklch(0.55 0.13 145); }

.popover .popover-close {
  margin-left: auto;
  width: 24px; height: 24px;
  border: 1px solid var(--rule); background: var(--paper);
  border-radius: 4px;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  font-family: var(--sans); font-size: 14px; color: var(--ink-mute);
  line-height: 1; padding: 0;
}
.popover .popover-close:hover { background: var(--cream-mid); color: var(--ink); }

.popover h2 {
  font-family: var(--serif); font-weight: 400;
  font-size: 24px; line-height: 1.15;
  margin: 0; color: var(--ink);
  text-wrap: pretty;
}
.popover.popover--long h2 { font-size: 26px; }

/* tombstone-banner inside meta-row of removed popover */
.popover .tombstone-banner {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.7px;
  text-transform: uppercase; color: oklch(0.42 0.11 25);
  background: oklch(0.92 0.05 25);
  padding: 3px 7px; border-radius: 3px;
}
.popover h2.tombstone-title {
  text-decoration: line-through;
  color: var(--ink-mute);
}

.popover .pop-when-where {
  display: grid;
  grid-template-columns: 22px 1fr;
  row-gap: 10px; column-gap: 12px;
  padding: 0 18px 14px;
  font-family: var(--sans); font-size: 14px; color: var(--ink); line-height: 1.35;
  align-items: center;
}
.popover .pop-when-where .icn {
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--ink-mute);
  width: 22px; height: 22px;
}

.popover .pop-tags {
  padding: 0 18px 14px;
  display: flex; flex-wrap: wrap; gap: 5px;
}
/* Tags become buttons that toggle filter state on click. Visual style stays
   close to the read-only pill from the source mockup, but with cursor +
   hover affordance + matching button reset (no native button chrome). */
.popover .pop-tags .tag {
  font-size: 9px; letter-spacing: 0.4px;
  background: var(--cream-mid);
  border: 1px solid var(--rule);
  color: var(--ink);
  font-family: var(--mono);
  text-transform: uppercase;
  padding: 3px 7px;
  border-radius: 3px;
  cursor: pointer;
  line-height: 1.2;
}
.popover .pop-tags .tag:hover { background: var(--cream-deep); border-color: var(--ink-faint); }
.popover .pop-tags .tag:active { transform: translateY(1px); }

.popover .pop-desc {
  padding: 14px 18px;
  border-top: 1px solid var(--rule);
  font-family: var(--sans); font-size: 13px; line-height: 1.55;
  color: var(--ink-soft);
  text-wrap: pretty;
}
.popover.popover--long .pop-desc {
  font-size: 13.5px; line-height: 1.6;
}
.popover .pop-desc p { margin: 0 0 10px; }
.popover .pop-desc p:last-child { margin-bottom: 0; }
.popover .pop-desc a {
  color: var(--ink);
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
}
.popover .pop-desc a:hover { border-bottom-color: var(--ink); }

.popover .pop-actions {
  padding: 14px 18px;
  border-top: 1px solid var(--rule);
  display: flex; gap: 8px; align-items: center;
  flex-shrink: 0;
}
.popover .pop-actions .register {
  flex: 1;
  height: 38px; padding: 0 14px;
  background: var(--ink); color: var(--cream);
  border-radius: 5px;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.9px;
  text-transform: uppercase; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  border: 0; cursor: pointer;
}
.popover .pop-actions .register:hover { background: var(--ink-soft); }
.popover .pop-actions .icon-btn {
  width: 38px; height: 38px;
  border: 1px solid var(--rule); background: var(--paper);
  border-radius: 5px;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  font-family: var(--sans); font-size: 14px; color: var(--ink-soft);
}
.popover .pop-actions .icon-btn:hover { background: var(--cream-mid); }

/* Related events sit in the cream-mid block so they read as a distinct
   section from the description / actions above. (Replaces the old sources
   block that occupied this slot.) */
.popover .pop-related {
  padding: 14px 18px 16px;
  border-top: 1px solid var(--rule);
  background: var(--cream-mid);
  display: flex; flex-direction: column; gap: 10px;
}
.popover .pop-related-head {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.7px;
  text-transform: uppercase; color: var(--ink-mute);
}
.popover .related-mini-list {
  display: flex; flex-direction: column; gap: 6px;
}
/* Day-list variant of the popover (clicked from a month cell's +N more
   button). Same chrome as the event popover, body is a chronological
   list of all events on that day. Cards reuse .related-mini visuals. */
.popover.popover-day .pop-day-list {
  margin-top: 6px;
}
.popover.popover-day .related-mini {
  background: var(--paper);
}
.popover.popover-day .related-mini.fmt-meetup     { background: var(--fmt-meetup);     color: var(--fmt-meetup-ink); }
.popover.popover-day .related-mini.fmt-workshop   { background: var(--fmt-workshop);   color: var(--fmt-workshop-ink); }
.popover.popover-day .related-mini.fmt-conference { background: var(--fmt-conference); color: var(--fmt-conference-ink); }
.popover.popover-day .related-mini.fmt-networking { background: var(--fmt-networking); color: var(--fmt-networking-ink); }
.popover.popover-day .related-mini.fmt-hackathon  { background: var(--fmt-hackathon);  color: var(--fmt-hackathon-ink); }
.popover .related-mini {
  display: flex; flex-direction: column; gap: 2px;
  padding: 8px 10px;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 4px;
  text-decoration: none;
  color: var(--ink);
}
.popover .related-mini:hover { box-shadow: var(--lift); }
.popover .related-mini-date {
  font-family: var(--mono); font-size: 10px; color: var(--ink-mute);
  letter-spacing: 0.3px;
}
.popover .related-mini-title {
  font-family: var(--serif); font-size: 14px; line-height: 1.18;
  text-wrap: pretty;
}
.popover .related-mini-meta {
  font-family: var(--sans); font-size: 11px; color: var(--ink-mute);
}
.popover .related-see-all {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.6px;
  text-transform: uppercase; color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px; align-self: flex-start;
}
.popover .related-see-all:hover { border-bottom-color: var(--ink); }

/* Mark the originating card while popover is open. Applies to both
   week-grid `.ev` cards and month-grid `.me` rows so the same outline
   anchors the popover visually in either view. */
.ev.is-open,
.me.is-open {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* toast */
.popover-toast {
  position: fixed;
  left: 50%; bottom: 24px;
  transform: translateX(-50%) translateY(8px);
  background: var(--ink); color: var(--cream);
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.6px;
  text-transform: uppercase;
  padding: 10px 16px; border-radius: 999px;
  z-index: 300;
  opacity: 0; pointer-events: none;
  transition: opacity .15s, transform .15s;
}
.popover-toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* mobile: bottom-pinned sheet (full width, slide up from bottom) */
@media (max-width: 720px) {
  .popover {
    top: auto;
    left: 0; right: 0; bottom: 0;
    width: 100%;
    max-height: 90vh;
    border-radius: 18px 18px 0 0;
    border-left: 0; border-right: 0; border-bottom: 0;
    box-shadow: 0 -20px 50px -10px rgba(15,27,45,.45);
  }
  .popover.popover--long { width: 100%; }
}

/* ───────────────── schedule view (related-events page) ───────────────── */

.schedule-page { min-width: 0; }
.schedule-main {
  flex: 1;
  padding: 24px 28px 36px;
  background: var(--cream);
  display: flex; flex-direction: column; gap: 18px;
  max-width: 880px;
  width: 100%;
  margin: 0 auto;
}

.schedule-back {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.6px;
  text-transform: uppercase; color: var(--ink-soft);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px; align-self: flex-start;
}
.schedule-back:hover { color: var(--ink); border-bottom-color: var(--ink); }

.schedule-empty {
  font-family: var(--sans); font-size: 14px; color: var(--ink-mute);
  font-style: italic; padding: 24px 0;
}

/* Stacked day groups — visual structure mirrors the mobile-week-view
   collapse: each `.schedule-day` is a chunky day-head followed by
   full-width `.ev.m4-card` cards (reused from week.ts:renderEventCard
   so cards stay visually identical to the calendar). */
.schedule-list {
  display: flex; flex-direction: column; gap: 22px;
}
/* Use :not([hidden]) so the UA stylesheet's [hidden]{display:none}
   continues to win when applyFilters marks a day-group hidden. No
   !important override needed. */
.schedule-day:not([hidden]) {
  display: flex; flex-direction: column;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--paper);
  overflow: hidden;
}
.schedule-day-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
}
.schedule-day-is-today .schedule-day-head {
  background: var(--ink);
  color: var(--cream);
}
.schedule-day-when {
  display: flex; align-items: baseline; gap: 10px;
}
.schedule-day-name {
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.8px;
  text-transform: uppercase; color: var(--ink-mute);
}
.schedule-day-is-today .schedule-day-name { color: var(--cream); opacity: 0.85; }
.schedule-day-today {
  color: var(--gold);
}
.schedule-day-num {
  font-family: var(--serif); font-size: 28px; line-height: 1;
  color: var(--ink); font-weight: 400;
}
.schedule-day-is-today .schedule-day-num { color: var(--cream); }
.schedule-day-month {
  font-family: var(--sans); font-size: 13px; color: var(--ink-mute);
}
.schedule-day-is-today .schedule-day-month { color: var(--cream); opacity: 0.7; }
.schedule-day-meta {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.7px;
  text-transform: uppercase; color: var(--ink-mute);
}
.schedule-day-is-today .schedule-day-meta { color: var(--cream); opacity: 0.7; }
.schedule-day-body {
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px;
  background: var(--cream);
}

@media (max-width: 720px) {
  .schedule-main { padding: 16px 14px 28px; }
  .schedule-day-head { padding: 10px 12px; }
  .schedule-day-num { font-size: 24px; }
  .schedule-day-body { padding: 10px; gap: 6px; }
}

/* ───────────────── week view ───────────────── */
/* Ported from docs/Website design/c-week-desktop.html
   + C-week-mobile.html. The week-page layout: header + body (filter rail
   + 7-day grid) + footer. M4 cards live inside day cells. */

.week-page { min-width: 0; }
.week-page > .body {
  display: grid;
  grid-template-columns: 184px 1fr;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.week-page .week-content {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  flex: 1;
  overflow: hidden;
}

/* Empty-period banner — shown on weeks/months between earliest and latest
   event with zero events of their own. Friendly nudge, not an error. */
.empty-period {
  padding: 24px 28px;
  background: var(--cream-mid);
  border-bottom: 1px solid var(--rule);
  font-family: var(--sans);
  color: var(--ink);
}
.empty-period-head {
  font-family: var(--serif);
  font-size: 22px;
  margin: 0 0 4px;
  font-weight: 400;
}
.empty-period-sub {
  margin: 0;
  font-size: 13px;
  color: var(--ink-mute);
}
.empty-period-sub a {
  color: var(--ink);
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
}

.week-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  flex: 1;
  min-height: 0;
  height: 100%;
  overflow: hidden;
}

.day {
  border-right: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}
.day:last-child { border-right: 0; }
/* Weekend body sits *between* cream (page bg) and cream-mid so the column
   reads as "subdued" without clashing with format cards — fmt-hackathon
   (#DDD9C8) and fmt-workshop (#E8DDB5) sit close to cream-mid and were
   visually merging into the column. Custom value, scoped here only. */
.day.weekend { background: #F1ECDD; }
.day.today .day-head { background: var(--ink); color: var(--cream); }
.day.today .day-head .meta { color: var(--cream); opacity: 0.7; }
.day.today .day-head .gold { color: var(--gold); }

.day-head {
  padding: 10px 12px;
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
  flex-shrink: 0;
}
/* Day-head on weekends — was cream-deep (#DCD2B8) but that was nearly
   identical to fmt-hackathon (#DDD9C8). Stepped up to cream-mid so the
   head still reads as "darker than its body" without colliding with cards. */
.day.weekend .day-head { background: var(--cream-mid); }
.day-head .name {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.day-head .num {
  font-family: var(--serif);
  font-size: 28px;
  line-height: 1;
  margin-top: 2px;
}
.day-head .meta {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.6px;
  color: var(--ink-mute);
  margin-top: 4px;
}

.day-body {
  flex: 1;
  padding: 10px 10px 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  overflow-y: auto;
  overflow-y: overlay;     /* Safari: gutter-less + native auto-hide */
  min-height: 0;
}

.empty {
  font-family: var(--sans);
  font-size: 12px;
  color: var(--ink-faint);
  font-style: italic;
  padding: 24px 4px;
  text-align: center;
}

/* Format colors — paper-friendly tints, ported from
   Card-explorations.html lines 64-77. The card BACKGROUND encodes format. */
:root {
  --fmt-workshop:     #E8DDB5; /* cream-gold */
  --fmt-workshop-ink: #5A4A12;
  --fmt-meetup:       #C9DCD6; /* soft teal */
  --fmt-meetup-ink:   #1F4940;
  --fmt-conference:   #E1CFC4; /* warm clay */
  --fmt-conference-ink:#5C3320;
  --fmt-networking:   #D7D0E4; /* lavender */
  --fmt-networking-ink:#3B2F5C;
  --fmt-hackathon:    #DDD9C8; /* parchment */
  --fmt-hackathon-ink:#3E3A1E;
}

/* M4 card — port of Card-explorations.html CardM4 (~line 813).
   Title hero + 2-row meta (city / organizer · time). Background = format. */
.ev.m4-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* Fluid padding: 8px @ 960px → 12px @ 1280px+. Tighter cards at
     split-screen widths give the title more horizontal room. */
  padding: clamp(8px, 1.25vw - 4px, 12px) clamp(8px, 1.25vw - 4px, 12px) clamp(7px, 1.1vw - 3px, 11px);
  background: var(--cream-mid, #ECE4D2); /* default fallback when format unknown */
  border: 1px solid rgba(0,0,0,0.06);
  border-radius: 4px;
  text-decoration: none;
  color: var(--ink);
  transition: box-shadow .12s, transform .12s;
}
.ev.m4-card:hover {
  box-shadow: var(--lift);
  transform: translateY(-1px);
}
.ev.m4-card .title {
  font-family: var(--serif);
  /* Fluid: 14px @ 960px viewport → 18px @ 1280px+. Below 960px we hit
     the mobile stack so the clamp doesn't matter there. Aimed at the
     960–1280px split-screen range. */
  font-size: clamp(14px, 1.25vw + 2px, 18px);
  line-height: 1.08;
  text-wrap: balance;
  font-weight: 400;
  color: inherit;
  /* Avoid mid-word overflow on long unbreakable strings. */
  overflow-wrap: anywhere;
}
.ev.m4-card .meta {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid rgba(0,0,0,0.14);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ev.m4-card .meta .city {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.9px;
  text-transform: uppercase;
  font-weight: 700;
  color: inherit;
}
.ev.m4-card .meta .sub {
  font-family: var(--sans);
  font-size: 10.5px;
  /* Was 0.72 — at 10.5px on pastel format backgrounds (fmt-hackathon,
     fmt-meetup, fmt-workshop), 0.72 of the format-ink token landed at
     ~3.5:1 against the pastel fill, below WCAG AA 4.5:1. Raised to 0.85
     so the meta line stays subdued vs the bold title but stays readable. */
  opacity: 0.85;
  line-height: 1.25;
  color: inherit;
}

/* Format-tinted background fills — the visual format indicator. */
.ev.m4-card.fmt-workshop   { background: var(--fmt-workshop);   color: var(--fmt-workshop-ink); }
.ev.m4-card.fmt-meetup     { background: var(--fmt-meetup);     color: var(--fmt-meetup-ink); }
.ev.m4-card.fmt-conference { background: var(--fmt-conference); color: var(--fmt-conference-ink); }
.ev.m4-card.fmt-networking { background: var(--fmt-networking); color: var(--fmt-networking-ink); }
.ev.m4-card.fmt-hackathon  { background: var(--fmt-hackathon);  color: var(--fmt-hackathon-ink); }

.ev.ev-cancelled .title,
.ev.ev-cancelled .meta .city,
.ev.ev-cancelled .meta .sub {
  text-decoration: line-through;
  opacity: 0.5;
}
.card-cancelled-banner {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  color: oklch(0.32 0.13 25);
  background: oklch(0.92 0.05 25);
  padding: 2px 5px;
  border-radius: 2px;
  align-self: flex-start;
  margin-bottom: 4px;
}

/* Hidden by client-side filtering. */
.ev[hidden] { display: none !important; }

/* ───────────────── invisible scrollbars + scroll fade ─────────────────
   Hide the scrollbar entirely in all browsers — scroll still works via
   wheel/touch/keys but no chrome is rendered, so no gutter is reserved,
   so columns are always the same width whether or not they overflow.
   Trade-off: no visible scroll affordance. Mitigated by the bottom-fade
   gradient below (signals "more below" on overflowing day columns). */
.day-body,
aside.rail .filters,
aside.rail .digest,
.mobile-filter-modal .modal-body,
.popover .pop-body {
  scrollbar-width: none;       /* Firefox */
  -ms-overflow-style: none;    /* legacy Edge */
}
.day-body::-webkit-scrollbar,
aside.rail .filters::-webkit-scrollbar,
aside.rail .digest::-webkit-scrollbar,
.mobile-filter-modal .modal-body::-webkit-scrollbar,
.popover .pop-body::-webkit-scrollbar {
  display: none;               /* Chromium/Safari */
}

/* Edge fade hint on scrollable containers — signals there's more content
   above/below the viewport without a scrollbar chrome.

   Day-body / filter rail use mask-image (the surrounding chrome lives on a
   parent element, so masking the content edges is fine).

   The popover splits chrome from content (.popover vs .pop-body) AND has
   different background colors per section: pop-head sits on var(--paper),
   pop-related sits on var(--cream-mid). Mask-image would fade content to
   transparent → panel bg shows through → section-bg/panel-bg mismatch at
   the bottom. So the popover uses overlay gradients (pseudo-elements on
   .popover) where the gradient color matches the adjacent section's bg
   directly. See `.popover::before` / `.popover::after` below. */
.day-body,
aside.rail .filters {
  --fade-h: 18px;
  /* default (mid-scroll): fade BOTH edges */
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-h),
    #000 calc(100% - var(--fade-h)),
    transparent 100%
  );
          mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-h),
    #000 calc(100% - var(--fade-h)),
    transparent 100%
  );
}
/* Schedule-view scrolling container — when body[data-view="schedule"] is on,
   .week-content (week page) or .month-wrap (month page) becomes the
   scroll container. Same fade mask + data-at-* contract as day-body
   and rail-filters above. The actual mask-image rule lives there
   (top-fade default), with the matching at-top / at-bottom rules
   below extending the existing selectors. */
body[data-view="schedule"] .week-page .week-content,
body[data-view="schedule"] .month-page .month-wrap,
.week-page:not(:has(.week-grid .ev:not([hidden]))) .week-content,
.month-page:not(:has(.month-grid .me:not([hidden]))) .month-wrap {
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-h),
    #000 calc(100% - var(--fade-h)),
    transparent 100%
  );
          mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-h),
    #000 calc(100% - var(--fade-h)),
    transparent 100%
  );
}
/* At top: drop the top fade, keep the bottom fade. */
.day-body[data-at-top="true"],
aside.rail .filters[data-at-top="true"],
body[data-view="schedule"] .week-page .week-content[data-at-top="true"],
body[data-view="schedule"] .month-page .month-wrap[data-at-top="true"],
.week-page:not(:has(.week-grid .ev:not([hidden]))) .week-content[data-at-top="true"],
.month-page:not(:has(.month-grid .me:not([hidden]))) .month-wrap[data-at-top="true"] {
  -webkit-mask-image: linear-gradient(
    to bottom,
    #000 0,
    #000 calc(100% - var(--fade-h)),
    transparent 100%
  );
          mask-image: linear-gradient(
    to bottom,
    #000 0,
    #000 calc(100% - var(--fade-h)),
    transparent 100%
  );
}
/* At bottom: keep the top fade, drop the bottom fade. */
.day-body[data-at-bottom="true"],
aside.rail .filters[data-at-bottom="true"],
body[data-view="schedule"] .week-page .week-content[data-at-bottom="true"],
body[data-view="schedule"] .month-page .month-wrap[data-at-bottom="true"],
.week-page:not(:has(.week-grid .ev:not([hidden]))) .week-content[data-at-bottom="true"],
.month-page:not(:has(.month-grid .me:not([hidden]))) .month-wrap[data-at-bottom="true"] {
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-h),
    #000 100%
  );
          mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--fade-h),
    #000 100%
  );
}
/* Content fits — no fade either direction. */
.day-body[data-at-top="true"][data-at-bottom="true"],
aside.rail .filters[data-at-top="true"][data-at-bottom="true"],
body[data-view="schedule"] .week-page .week-content[data-at-top="true"][data-at-bottom="true"],
body[data-view="schedule"] .month-page .month-wrap[data-at-top="true"][data-at-bottom="true"],
.week-page:not(:has(.week-grid .ev:not([hidden]))) .week-content[data-at-top="true"][data-at-bottom="true"],
.month-page:not(:has(.month-grid .me:not([hidden]))) .month-wrap[data-at-top="true"][data-at-bottom="true"] {
  -webkit-mask-image: none;
          mask-image: none;
}

/* ── popover edge fades (overlay gradients) ─────────────────────────────
   ::before paints over the top of pop-body in pop-head's bg colour (paper),
   ::after paints over the bottom in pop-related's bg colour (cream-mid)
   when the popover has a related-events block, falling back to paper
   otherwise. Heights / stops bias toward "longer + chunkier at bottom" so
   the bottom fade reads as a strong handoff into the cream-mid section. */
.popover::before,
.popover::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  pointer-events: none;
  z-index: 2;
  opacity: 1;
  transition: opacity 0.15s ease;
}
/* Gradient endpoints use rgba() at alpha 0 with the SAME hue as the start
   color (paper #FBF8F1 / cream-mid #EDE6D3). The browser interpolates
   alpha along the gradient — using `transparent` (rgba(0,0,0,0)) instead
   would interpolate through dark gray, producing a muddy band. */
.popover::before {
  top: 0;
  height: 20px;
  background: linear-gradient(to bottom, #FBF8F1 25%, rgba(251, 248, 241, 0));
}
.popover::after {
  bottom: 0;
  height: 44px;
  /* Default fade target: paper (matches pop-actions when no related block). */
  background: linear-gradient(to top, #FBF8F1 35%, rgba(251, 248, 241, 0));
}
/* When pop-related is present (most events), it owns the bottom and
   paints cream-mid — so the bottom fade switches to cream-mid to stay
   continuous with that section. */
.popover:has(.pop-related)::after {
  background: linear-gradient(to top, #EDE6D3 35%, rgba(237, 230, 211, 0));
}
.popover:has(.pop-body[data-at-top="true"])::before { opacity: 0; }
.popover:has(.pop-body[data-at-bottom="true"])::after { opacity: 0; }

/* Scroll nudge — small chevron-down centered over the bottom fade region.
   Sits at z-index 3 (above the ::after fade gradient at z-index 2). Hidden
   when the user has reached the bottom (data-at-bottom="true" on pop-body)
   so it disappears as soon as it stops being useful. The gentle bob makes
   it readable as "more content below" instead of decoration; opacity stays
   low so it doesn't compete with the related-events list it sits over. */
.popover .pop-scroll-nudge {
  position: absolute;
  left: 50%;
  bottom: 6px;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-mute);
  pointer-events: none;
  z-index: 3;
  opacity: 0.6;
  transition: opacity 0.18s ease;
  animation: pop-nudge 1.8s ease-in-out infinite;
}
@keyframes pop-nudge {
  0%, 100% { transform: translate(-50%, 0); }
  50%      { transform: translate(-50%, 3px); }
}
.popover:has(.pop-body[data-at-bottom="true"]) .pop-scroll-nudge {
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .popover .pop-scroll-nudge { animation: none; transform: translateX(-50%); }
}

/* ───────────────── mobile header filter button ─────────────────
   Issue 8: replaces the old floating-bottom .filter-trigger pill. Only
   visible on mobile (≤720px); inline with the prev/next + view-switcher. */
.header-filter-btn { display: none; }
/* Calendar layout collapses to vertical stack + filter button replaces
   the rail. Threshold is 960px so split-screen on a 1920px display
   (two 960px halves) still gets the desktop 7-col layout on each side.
   Below 960px the 7-col grid would have <100px columns and M4 titles
   would overflow, so we switch to the mobile stack with full-width cards. */
@media (max-width: 959px) {
  .week-page > .body,
  .month-page > .body {
    grid-template-columns: 1fr;
    overflow: auto;
  }
  body:has(> .page.week-page),
  body:has(> .page.month-page) {
    height: auto;
    overflow: auto;
  }
  .page.week-page,
  .page.month-page {
    height: auto;
    overflow: visible;
  }
  .week-page aside.rail,
  .month-page aside.rail {
    display: none;
  }
  .week-grid {
    grid-template-columns: 1fr;
    height: auto;
    overflow: visible;
  }
  .day {
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    overflow: visible;
  }
  .day-body {
    overflow-y: visible;
    padding: 12px 14px 18px;
  }
  .header-filter-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    /* 40px tap-target on mobile (WCAG 2.5.5 baseline). Desktop equivalent
       is hidden via display:none above. */
    height: 40px;
    padding: 0 12px;
    border: 1px solid var(--rule);
    border-radius: 4px;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: 0.6px;
    text-transform: uppercase;
    cursor: pointer;
  }
  /* Active-state visual: when any filter is on, the button gains a
     gold fill (highlight enough to read at-a-glance, no text needed).
     body.has-active-filters is set by filters.js syncUi on every pass. */
  body.has-active-filters .header-filter-btn {
    background: var(--gold);
    border-color: var(--gold);
  }
  /* Legacy text badge — preserved for any non-icon trigger that still
     emits a .active-count span. Icon-only buttons drop straight to the
     fill swap above. */
  .header-filter-btn .active-count {
    background: var(--gold);
    color: var(--ink);
    font-family: var(--mono);
    font-size: 9px;
    padding: 1px 6px;
    border-radius: 999px;
    letter-spacing: 0.4px;
  }
  .header-filter-btn .active-count[hidden] { display: none; }
}

/* ───────────────── mobile filter modal ───────────────── */
.mobile-filter-modal[hidden] { display: none !important; }
.mobile-filter-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: var(--cream);
  display: flex;
  flex-direction: column;
}
.mobile-filter-modal .modal-head {
  padding: 14px 16px;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  display: flex;
  align-items: center;
  gap: 16px;
  flex-shrink: 0;
}
.mobile-filter-modal .modal-head .title {
  font-family: var(--serif);
  font-size: 22px;
  line-height: 1;
  color: var(--ink);
}
.mobile-filter-modal .modal-head .grow { flex: 1; }
.mobile-filter-modal .modal-head .reset {
  /* Hidden until a filter / search is active. Uses visibility: hidden so
     the modal header doesn't reflow when reset appears. */
  visibility: hidden;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  color: var(--ink-soft);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
  cursor: pointer;
}
.mobile-filter-modal .modal-head .close {
  width: 36px;
  height: 36px;
  border: 1px solid var(--rule);
  border-radius: 50%;
  background: var(--paper);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  cursor: pointer;
}

.mobile-filter-modal .modal-body {
  flex: 1;
  overflow-y: auto;
  overflow-y: overlay;
  padding: 14px 18px 18px;
  background: var(--cream);
}
.mobile-filter-modal .group { margin-bottom: 22px; }
.mobile-filter-modal .group h4 {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-soft);
  margin: 0 0 10px;
}
.mobile-filter-modal .group input[type="search"],
.mobile-filter-modal .group input[type="email"] {
  width: 100%;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--paper);
  font-family: var(--sans);
  /* 16px prevents iOS Safari auto-zoom on focus. Mobile modal can afford
     the larger size visually. */
  font-size: 16px;
  color: var(--ink);
}
.mobile-filter-modal .group .group-head-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 10px;
}
.mobile-filter-modal .group .group-head-row h4 { margin: 0; }
.mobile-filter-modal .add-event-link {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--rule);
  padding-bottom: 1px;
}
.mobile-filter-modal .add-event-link:hover { border-bottom-color: var(--ink); }
.mobile-filter-modal .group label,
.mobile-filter-modal .group button[data-filter-group] {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink-soft);
  cursor: pointer;
  border-bottom: 1px solid var(--rule);
  min-height: 40px;
}
.mobile-filter-modal .group label:last-child,
.mobile-filter-modal .group button[data-filter-group]:last-child {
  border-bottom: 0;
}
/* Reset native button defaults to match the original <label> rendering. */
.mobile-filter-modal .group button[data-filter-group] {
  background: transparent;
  border-top: 0;
  border-left: 0;
  border-right: 0;
  margin: 0;
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
}
.mobile-filter-modal .group .checkbox {
  width: 22px;
  height: 22px;
  border: 1px solid var(--ink-mute);
  border-radius: 4px;
  background: var(--paper);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--cream);
  font-size: 13px;
  flex-shrink: 0;
}
.mobile-filter-modal .group .checkbox.on {
  background: var(--ink);
  border-color: var(--ink);
}
.mobile-filter-modal .group .count {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-faint);
  margin-left: auto;
}
.mobile-filter-modal .group .legend {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 4px;
}
.mobile-filter-modal .group .legend .row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-soft);
  letter-spacing: 0.4px;
  text-transform: uppercase;
}
.mobile-filter-modal .modal-digest {
  flex-shrink: 0;
  padding: 14px 18px;
  background: var(--cream-mid);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.mobile-filter-modal .modal-digest .label {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--ink-mute);
  display: flex;
  align-items: center;
  gap: 6px;
}
.mobile-filter-modal .modal-digest .label::before {
  content: '';
  display: inline-block;
  width: 12px;
  height: 1px;
  background: var(--gold-deep);
}
.mobile-filter-modal .modal-digest .head {
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.4;
  color: var(--ink);
}
.mobile-filter-modal .modal-digest form {
  display: flex;
  gap: 0;
  margin-top: 4px;
}
.mobile-filter-modal .modal-digest input[type="email"] {
  flex: 1;
  min-width: 0;
  border-radius: 4px 0 0 4px;
  border-right: 0;
}
.mobile-filter-modal .modal-digest button {
  height: 40px;
  padding: 0 14px;
  background: var(--ink);
  color: var(--cream);
  border: 0;
  border-radius: 0 4px 4px 0;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.7px;
  text-transform: uppercase;
  cursor: pointer;
}
.mobile-filter-modal .modal-foot {
  flex-shrink: 0;
  padding: 14px 16px 22px;
  background: var(--paper);
  border-top: 1px solid var(--rule);
}
.mobile-filter-modal .modal-foot .apply {
  width: 100%;
  height: 52px;
  background: var(--ink);
  color: var(--cream);
  border: 0;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.9px;
  text-transform: uppercase;
  cursor: pointer;
}
.mobile-filter-modal .modal-foot .apply .arrow {
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1;
}

body.modal-open { overflow: hidden; }

/* ───────────────── month view ───────────────── */
/* Ported from docs/Website design/C-month-desktop.html
   + C-month-mobile.html. The month-page layout: header + body
   (filter rail + 7-col grid) + footer. Each cell shows day number +
   up to N one-line event previews (.me rows) + a "+N more" link. */

.month-page { min-width: 0; }
.month-page > .body {
  display: grid;
  grid-template-columns: 184px 1fr;
  flex: 1;
  min-height: 0;
  overflow: hidden;
}

.month-wrap {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  flex: 1;
  /* Single scroll container for head + grid together. Previously the
     grid scrolled on its own (overflow: auto), but that made a
     scrollbar reserve width on the grid while the head kept its full
     width — vertical column edges then misaligned between head and
     grid at small viewport heights. Now both ride one scrollbar,
     columns stay perfectly aligned. */
  overflow-y: auto;
}

.month-head {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
  flex-shrink: 0;
  /* Stick to the top of the scrolling .month-wrap so the day-of-week
     labels stay visible while the grid scrolls beneath. */
  position: sticky;
  top: 0;
  z-index: 2;
}
.month-head .col {
  padding: 10px 14px;
  border-right: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.8px;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.month-head .col:last-child { border-right: 0; }
.month-head .col.weekend { background: #F1ECDD; }

.month-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  grid-auto-rows: 1fr;
  flex: 1;
  min-height: 0;
  /* Scrolling moved up to .month-wrap so the head + grid share one
     scrollbar (otherwise the grid lost width to its scrollbar while
     the head stayed full-width → vertical column misalignment). */
  overflow: visible;
}

.month-grid .cell {
  border-right: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: 8px 8px 4px;
  min-height: 110px;
  background: var(--paper);
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: var(--ink);
  position: relative;
  overflow: hidden;
}
.month-grid .cell:nth-child(7n) { border-right: 0; }
.month-grid .cell.weekend { background: #F1ECDD; }
.month-grid .cell.outside { background: var(--cream); }
.month-grid .cell.outside .num { color: var(--ink-faint); }
.month-grid .cell.today { background: oklch(0.96 0.04 80); }
.month-grid .cell.cell-empty { background: var(--cream); }
.month-grid .cell.cell-empty.weekend { background: #F1ECDD; }
.month-grid .cell.cell-empty .num { color: var(--ink-mute); }

.cell-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  text-decoration: none;
  color: inherit;
  flex-shrink: 0;
}
.month-grid .cell .num {
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1;
  color: var(--ink);
}
.month-grid .cell.today .cell-top .num {
  color: var(--cream);
  background: var(--ink);
  width: 26px;
  height: 26px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
}
.month-grid .cell .countdot {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.month-grid .cell .countdot[hidden] { display: none; }

.cell-events {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-height: 0;
  overflow: hidden;
}
.cell-spacer {
  flex: 1;
  display: block;
}

/* .me — one-line event preview row used in month cells. Matches the
   pastel format-color backgrounds of the week-view .m4-card so the
   month feels visually consistent (a workshop event is the same
   cream-gold whether you're viewing it in week or month). Format
   colour tokens are defined once in :root above. Default falls back
   to the paper-tint when format is unknown. */
.me {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 6px;
  background: var(--cream-mid);
  border: 1px solid rgba(0, 0, 0, 0.06);
  border-radius: 3px;
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.25;
  color: var(--ink);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  text-decoration: none;
  /* Don't shrink inside .cell-events flex column when there are more rows
     than fit. fitCellRows in month.js relies on measuring the natural row
     height to compute "+N more" — flex-shrink:1 (default) was squashing
     rows to ~9px when packed and the math then thought everything fit.
     Now rows stay at natural height; overflow is clipped via the
     .cell-events overflow:hidden + JS hiding overflow rows for "+N more". */
  flex-shrink: 0;
}
.me:hover { filter: brightness(0.97); }
.me.fmt-meetup     { background: var(--fmt-meetup);     color: var(--fmt-meetup-ink); }
.me.fmt-workshop   { background: var(--fmt-workshop);   color: var(--fmt-workshop-ink); }
.me.fmt-conference { background: var(--fmt-conference); color: var(--fmt-conference-ink); }
.me.fmt-networking { background: var(--fmt-networking); color: var(--fmt-networking-ink); }
.me.fmt-hackathon  { background: var(--fmt-hackathon);  color: var(--fmt-hackathon-ink); }
.me .tt {
  overflow: hidden;
  text-overflow: ellipsis;
}
.me.me-cancelled .tt { text-decoration: line-through; color: var(--ink-faint); }
.me[hidden] { display: none !important; }

.me-more {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--ink-mute);
  padding: 1px 4px;
  cursor: pointer;
  text-decoration: none;
  /* Element is a <button> so reset default button chrome. */
  background: transparent;
  border: 0;
  text-align: left;
  display: inline-block;
}
.me-more:hover { color: var(--ink); text-decoration: underline; }
.me-more[hidden] { display: none !important; }

/* Cell dots — mobile-only stacked-dots view inside each month cell.
   Hidden on desktop (where .me rows show full titles); shown at
   <=720px in place of .me rows. Dots inherit the same format-color
   tokens as .me cells so a workshop is the same cream-gold whether
   rendered as a card, a row, or a dot. */
.cell-dots {
  display: none;
  flex-wrap: wrap;
  gap: 3px;
  padding: 0 6px 6px;
  align-content: flex-start;
}
.cell-dots .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--cream-mid);
  display: inline-block;
}
.cell-dots .dot.fmt-meetup     { background: var(--fmt-meetup); }
.cell-dots .dot.fmt-workshop   { background: var(--fmt-workshop); }
.cell-dots .dot.fmt-conference { background: var(--fmt-conference); }
.cell-dots .dot.fmt-networking { background: var(--fmt-networking); }
.cell-dots .dot.fmt-hackathon  { background: var(--fmt-hackathon); }
.cell-dots .dot.dot-cancelled { opacity: 0.4; }
.cell-dots .dot[hidden] { display: none; }
@media (max-width: 720px) {
  .cell-dots { display: flex; }
  /* On mobile, dots replace .me preview rows entirely — titles can't
     fit in a ~50px column anyway. "+N more" link also drops since the
     dots show the full count visually. Tapping the cell-spacer link
     navigates to /week/<id> for full details. */
  .month-grid .cell-events,
  .month-grid .me,
  .month-grid .me-more { display: none; }
}

/* Hover tooltip for .me cells. JS in month.js positions it; the
   pointer-events:none keeps mouse from sticking on the tooltip itself,
   which would otherwise cause flickering when the row hover ends. */
.me-tooltip {
  position: fixed;
  z-index: 100;
  background: var(--ink);
  color: var(--cream);
  padding: 7px 10px;
  border-radius: 4px;
  font-family: var(--sans);
  font-size: 12.5px;
  line-height: 1.3;
  max-width: 320px;
  pointer-events: none;
  box-shadow: 0 4px 16px rgba(15, 27, 45, 0.18);
}
.me-tooltip-title {
  font-weight: 500;
  text-wrap: balance;
}
.me-tooltip-meta {
  margin-top: 2px;
  font-size: 11px;
  font-family: var(--mono);
  letter-spacing: 0.3px;
  color: var(--cream);
  opacity: 0.75;
}

/* Cancelled-event dot color (different from any format spine). */
.cat-cancelled { background: oklch(0.55 0.18 25); }

@media (max-width: 959px) {
  .month-page > .body {
    grid-template-columns: 1fr;
  }
  .month-page aside.rail {
    display: none;
  }
  .month-grid {
    grid-auto-rows: minmax(80px, auto);
    overflow: visible;
  }
  .month-grid .cell {
    min-height: 80px;
    padding: 6px 4px;
  }
  .month-grid .cell .countdot { display: none; }
  .month-grid .cell.today .cell-top .num {
    width: 22px;
    height: 22px;
    font-size: 12px;
  }
  .me {
    font-size: 10px;
    padding: 2px 4px;
  }
}
