/* ============================================================
   WHAT'S ON — Pacific Northwest editorial broadsheet
   Paper + ink + a single vermilion accent. Letterpress energy.
   ============================================================ */
:root {
  --paper:      #f4efe3;   /* warm newsprint */
  --paper-deep: #ece4d2;
  --ink:        #18140f;   /* near-black ink */
  --ink-soft:   #4a4338;
  --rule:       #d8cdb6;
  --accent:     #e2452b;   /* Rainier-sunset vermilion */
  --accent-ink: #b6311c;
  --slate:      #2f5d62;   /* Sound-water teal, secondary accent */
  --gold:       #9a6b1f;   /* burnished gold, comedy accent */
  --moss:       #5a6e2f;   /* market-stall green, food accent */
  --fern:       #2e7d4f;   /* evergreen, nature/gardens accent */
  --plum:       #6b3a7a;   /* quiz-night plum, trivia accent */
  --shadow:     rgba(24,20,15,.16);

  --display: 'Archivo Black', sans-serif;
  --serif:   'Fraunces', Georgia, serif;
  --mono:    'Spline Sans Mono', ui-monospace, monospace;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html { scroll-behavior: smooth; }

body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  /* Full-bleed on desktop: the page uses the whole viewport width rather than a
     centered ~1280px column with large empty side margins. A responsive side
     padding keeps content off the very edge (a hair on phones, more on wide
     screens). No max-width cap — the reader asked to use the margin whitespace
     for display. */
  width: 100%;
  margin: 0 auto;
  padding: 0 clamp(12px, 4vw, 48px) 80px;
  position: relative;
  overflow-x: hidden;
}

/* film-grain / paper texture overlay */
.grain {
  position: fixed; inset: 0; z-index: 999; pointer-events: none;
  opacity: .5; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.35'/%3E%3C/svg%3E");
}

/* ---------- MASTHEAD ----------
   Sticky header stack, top→bottom: .viewbar → .controls → .day-head →
   .area-head. Each parks just below the one above it; JS measures the live
   heights into the --top-* vars (see syncStickyOffsets) since chips wrap and
   fonts scale responsively. The .masthead-top control bar (dateline / city /
   language / ✉ / ⚙) and the .masthead BANNER (logo + tagline) both scroll away
   — they sit above .viewbar in the flow — so the first screen keeps as much
   vertical room as possible for listings (precious on mobile). */
.masthead { padding: 0 0 22px; text-align: center; }
.masthead-top {
  position: static;
  display: flex; justify-content: space-between; align-items: baseline;
  font-family: var(--mono); font-size: 12px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-soft);
  background: var(--paper);
  border-bottom: 2px solid var(--ink); padding: 4px 0 10px;
}
.edition { text-align: right; margin-left: auto; }
/* City/edition switcher — a bare native <select> styled to read as part of the
   masthead dateline. Native control keeps it accessible + mobile-friendly. The
   accent underline signals it's interactive; the ▾ is drawn as a background. */
.city-select {
  /* Right padding must clear the dropdown-arrow glyph (drawn at right 1-6px)
     so the longest edition name ("CIUDAD DE MÉXICO") isn't crowded/clipped by
     it. */
  margin-left: 18px; padding: 0 26px 2px 0;
  font-family: var(--mono); font-size: 12px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink); font-weight: 600;
  background: transparent; border: none;
  border-bottom: 2px solid var(--accent); border-radius: 0;
  cursor: pointer; -webkit-appearance: none; appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--accent) 50%),
                    linear-gradient(135deg, var(--accent) 50%, transparent 50%);
  background-position: right 6px top 60%, right 1px top 60%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}
.city-select:hover { color: var(--accent-ink); }
.city-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.city-select option { color: var(--ink); background: var(--paper); }
/* Language toggle: the compact sibling of the edition switcher. Same monospace
   stamp look, tighter, sits just before the ✉/⚙ icons. */
.lang-select {
  margin-left: 14px; padding: 0 16px 2px 0;
  font-family: var(--mono); font-size: 12px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft); font-weight: 600;
  background: transparent; border: none;
  border-bottom: 2px solid var(--rule); border-radius: 0;
  cursor: pointer; -webkit-appearance: none; appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--ink-soft) 50%),
                    linear-gradient(135deg, var(--ink-soft) 50%, transparent 50%);
  background-position: right 6px top 60%, right 1px top 60%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}
.lang-select:hover { color: var(--accent-ink); }
.lang-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.lang-select option { color: var(--ink); background: var(--paper); }
.prefs-btn {
  font-size: 72px; line-height: 1; color: var(--ink-soft);
  background: transparent; border: none; cursor: pointer;
  margin-left: 14px; padding: 0 2px; transition: color .15s ease, transform .2s ease;
}
.prefs-btn:hover { color: var(--accent-ink); }
#prefsBtn:hover { transform: rotate(45deg); }   /* spin the gear only */
/* svgIcons editions (e.g. CDMX): the button holds an inline SVG sized in `em`,
   so drop the huge emoji font-size to an icon scale and center the glyph. */
.prefs-btn--svg { font-size: 26px; display: inline-flex; align-items: center; }

/* Favorite ("always") category chips: a gold left accent + star, even when not pressed. */
.chip-fav { border-color: var(--gold); color: var(--ink); }
.chip-fav:not([aria-pressed="true"]) { box-shadow: inset 3px 0 0 var(--gold); }

/* Mobile filter toggle — collapses the chip row on phones. Hidden on desktop
   (chips always show there); revealed in the mobile breakpoint below. Reads as
   a chip so it sits in the same visual language; the ▾ rotates when open. */
.filter-toggle { display: none; }
.filter-toggle .filter-toggle-count {
  color: var(--accent-ink); font-weight: 600; margin-left: 2px;
}
.filter-toggle::after {
  content: "▾"; display: inline-block; margin-left: 8px; font-size: 10px;
  transition: transform .15s ease;
}
.filter-toggle[aria-expanded="true"]::after { transform: rotate(180deg); }

/* Neighborhood panel (closest library + market) */
.hood-panel {
  display: flex; flex-wrap: nowrap; align-items: flex-start; gap: 8px 16px;
  font-family: var(--mono); font-size: 12px;
  padding: 10px 2px; border-bottom: 1px solid var(--rule);
}
/* The library + market cells live in their own flex container to the right of
   the neighborhood label. It wraps INTERNALLY, so when both cells can't fit on
   one line they stack vertically (left-aligned with each other) beside the
   label box — instead of the second cell dropping below the label. */
.hood-cells { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 22px; }
/* Author display:flex above beats the UA [hidden] rule, so without this the
   empty panel still drew its border + padding as a stray rule under the
   masthead. Honor `hidden` explicitly. */
.hood-panel[hidden] { display: none; }
.hood-you {
  text-transform: uppercase; letter-spacing: .1em; font-weight: 600;
  color: var(--paper); background: var(--slate); padding: 3px 9px;
  flex-shrink: 0;  /* keep the label box intact; the cells column yields width */
}
.hood-cell { display: flex; align-items: baseline; gap: 8px; }
.hood-label { text-transform: uppercase; letter-spacing: .06em; color: var(--ink-soft); font-size: 10px; }
.hood-name { color: var(--ink); text-decoration: none; border-bottom: 1px dotted var(--slate); }
.hood-name:hover { color: var(--accent-ink); }
.hood-dist { color: var(--accent-ink); font-weight: 600; }

/* Preferences modal */
.modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; }
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(24, 20, 15, .55); }
.modal-card {
  position: relative; background: var(--paper); color: var(--ink);
  width: min(560px, 92vw); max-height: 88vh; overflow-y: auto;
  border: 2px solid var(--ink); box-shadow: 8px 8px 0 var(--accent);
}
.modal-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 16px 20px; border-bottom: 2px solid var(--ink);
}
.modal-head h2 { font-family: var(--display); font-size: 26px; margin: 0; }
/* Negative-margin padding grows the hit area to ~44px without moving a pixel. */
.modal-close { font-size: 26px; line-height: 1; background: none; border: none; cursor: pointer;
  color: var(--ink-soft); padding: 10px 14px; margin: -10px -14px; }
.modal-close:hover { color: var(--accent-ink); }
.modal-body { padding: 8px 20px 20px; }
.pref-section { padding: 16px 0; border-bottom: 1px solid var(--rule); }
.pref-section:last-child { border-bottom: none; }
.pref-section h3 {
  font-family: var(--mono); font-size: 12px; letter-spacing: .1em; text-transform: uppercase;
  margin: 0 0 4px; color: var(--ink);
}
.pref-hint { font-size: 13px; color: var(--ink-soft); margin: 0 0 10px; }
.pref-select {
  font-family: var(--mono); font-size: 14px; padding: 7px 10px; width: 100%;
  background: var(--paper-deep); border: 1.5px solid var(--rule); color: var(--ink);
}
.pref-cats { display: flex; flex-wrap: wrap; gap: 8px; }
.pref-cat {
  font-family: var(--mono); font-size: 12px; letter-spacing: .06em; text-transform: uppercase;
  background: transparent; color: var(--ink-soft);
  border: 1.5px solid var(--rule); padding: 6px 11px; cursor: pointer; transition: all .15s ease;
}
.pref-cat:hover { border-color: var(--ink); color: var(--ink); }
.pref-cat.on { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.modal-foot {
  display: flex; justify-content: space-between; align-items: center;
  padding: 16px 20px; border-top: 2px solid var(--ink);
}
.pref-save {
  font-family: var(--mono); font-size: 13px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; background: var(--accent); color: var(--paper);
  border: none; padding: 9px 18px; cursor: pointer; transition: background .15s ease;
}
.pref-save:hover { background: var(--accent-ink); }
.pref-save:disabled { opacity: .5; cursor: wait; }

/* Feedback modal extras */
.fb-textarea { width: 100%; resize: vertical; min-height: 96px; line-height: 1.45; }
.fb-optional { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--ink-soft); }
.fb-status { font-size: 13px; color: var(--ink-soft); }
.fb-status.ok { color: var(--moss, #5a6f3f); }
.fb-status.err { color: var(--accent-ink); }

.logo {
  font-family: var(--display);
  font-size: clamp(58px, 15vw, 168px);
  line-height: .86;
  letter-spacing: -.02em;
  text-transform: uppercase;
  margin: 0 0 6px;
  color: var(--ink);
  text-shadow: 3px 3px 0 var(--accent);
}

.tagline {
  font-style: italic;
  font-size: clamp(16px, 2.2vw, 22px);
  color: var(--ink-soft);
  margin-bottom: 0;
}

/* ---------- VIEW TABS ---------- */
.viewbar {
  position: sticky; top: var(--top-viewbar, 44px); z-index: 55;
  display: flex; align-items: stretch; gap: 0;
  background: var(--paper);
  border-bottom: 2px solid var(--ink);
}
.viewtab {
  font-family: var(--display); font-size: clamp(18px, 3vw, 26px);
  text-transform: uppercase; letter-spacing: .01em;
  background: none; border: none; cursor: pointer;
  padding: 8px 22px 10px; color: var(--rule);
  position: relative; transition: color .15s ease;
}
.viewtab:hover { color: var(--ink-soft); }
.viewtab[aria-selected="true"] { color: var(--ink); }
.viewtab[aria-selected="true"]::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -2px;
  height: 4px; background: var(--accent);
}
/* The action buttons (Free / Near me / Open now) sit as a group just to the
   right of the Venues tab, with a comfortable gap. The search/period/count
   group (.control-meta) is relocated here on desktop (placeControlMeta) and
   follows immediately to the right of "Near me"; margin-left:auto on it pushes
   the whole search cluster to the far right of the bar so it doesn't crowd the
   actions. */
.view-actions {
  display: flex; align-items: center; gap: 8px;
  margin-left: 28px;
}
/* .control-meta living in the viewbar (desktop). Sits centered on the bar's
   baseline and pushed to the right edge, to the right of Near me. */
.viewbar > .control-meta { align-self: center; margin-left: auto; }
.geo-btn {
  align-self: center;
  font-family: var(--mono); font-size: 12px; letter-spacing: .08em;
  text-transform: uppercase; cursor: pointer;
  background: transparent; color: var(--ink-soft);
  border: 1.5px solid var(--rule); padding: 7px 14px;
  transition: all .15s ease;
}
.geo-btn:hover { border-color: var(--ink); color: var(--ink); }
.geo-btn.active { background: var(--slate); color: var(--paper); border-color: var(--slate); }

/* ---------- CONTROLS ---------- */
/* On desktop the controls row is now the category chips ALONE — the search /
   period / count group rides up in the viewbar beside "Near me"
   (placeControlMeta). So the chip container fills the row and its chips size
   themselves to fit on ONE line (see .filter-group / .chip below). */
.controls {
  position: sticky; top: var(--top-controls, 106px); z-index: 50;
  display: flex; flex-wrap: wrap; gap: 10px 8px;
  align-items: center;
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(6px);
  padding: 14px 0; margin-bottom: 10px;
  border-bottom: 2px solid var(--ink);
}
/* Chips on one responsive line (desktop): the group fills the row and lays the
   chips out nowrap; each chip shares the width (flex) and its font/padding
   scale with the viewport (clamp) so all 13 categories fit one line down to
   ~narrow-desktop without wrapping. Mobile overrides this back to a wrapping
   box (the media query targets #catFilters by id). */
.filter-group {
  /* One line, always (desktop). The chips are sized to their label
     (min-width: fit-content below) and a JS auto-fit (fitCategoryChips) shrinks
     the shared font-size until the whole row fits one line — so the longer
     French/Spanish category words scale down rather than spilling past the box
     or wrapping. Mobile (<=620px) overrides this to a wrapping box. */
  display: flex; flex-wrap: nowrap; gap: clamp(3px, 0.5vw, 8px);
  flex: 1 1 100%; min-width: 0;
}
#viewActionsSlot:empty { display: none; }

.chip {
  font-family: var(--mono); font-weight: 500;
  letter-spacing: .06em; text-transform: uppercase;
  background: transparent; color: var(--ink-soft);
  border: 1.5px solid var(--rule); border-radius: 0;
  cursor: pointer; transition: all .15s ease;
  /* Grow to share the row width, but never shrink below the label itself
     (min-width: fit-content) — that shrink was what let long FR/ES labels spill
     past the chip's box. The shared font-size is driven by --chip-fs, which the
     JS auto-fit (fitCategoryChips) lowers until the whole row fits one line;
     padding scales with it (em) so the chips stay proportional as they shrink. */
  flex: 1 1 auto; min-width: fit-content; text-align: center; white-space: nowrap;
  font-size: var(--chip-fs, clamp(9px, 1.02vw, 12px));
  padding: 0.5em clamp(4px, 0.7vw, 12px);
}
.chip:hover { border-color: var(--ink); color: var(--ink); }
.chip[aria-pressed="true"] {
  background: var(--ink); color: var(--paper); border-color: var(--ink);
}
.chip[aria-pressed="true"][data-cat="theatre"],
.chip[aria-pressed="true"][data-cat="cultural"] { background: var(--accent); border-color: var(--accent); }
.chip[aria-pressed="true"][data-cat="music"],
.chip[aria-pressed="true"][data-cat="lecture"] { background: var(--slate); border-color: var(--slate); }
.chip[aria-pressed="true"][data-cat="dance"] { background: var(--accent-ink); border-color: var(--accent-ink); }
.chip[aria-pressed="true"][data-cat="comedy"] { background: var(--gold); border-color: var(--gold); }
.chip[aria-pressed="true"][data-cat="food"] { background: var(--moss); border-color: var(--moss); }
.chip[aria-pressed="true"][data-cat="sports"] { background: var(--slate); border-color: var(--slate); }
.chip[aria-pressed="true"][data-cat="nature"] { background: var(--fern); border-color: var(--fern); }
.chip[aria-pressed="true"][data-cat="trivia"] { background: var(--plum); border-color: var(--plum); }

.control-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 18px; }

/* search: spare letterpress field, baseline underline only */
.search-wrap {
  display: flex; align-items: center; gap: 7px;
  border-bottom: 1.5px solid var(--rule);
  padding: 3px 2px; transition: border-color .15s ease;
}
.search-wrap:focus-within { border-color: var(--ink); }
.search-icon { font-family: var(--mono); font-size: 15px; color: var(--ink-soft); line-height: 1; }
.search-box {
  font-family: var(--mono); font-size: 13px; color: var(--ink);
  background: transparent; border: none; outline: none;
  width: 14ch; padding: 2px 0; transition: width .2s ease;
}
.search-box::placeholder { color: var(--ink-soft); letter-spacing: .04em; }
.search-box:focus { width: 20ch; }
.search-box::-webkit-search-cancel-button { display: none; }
.search-clear {
  font-family: var(--mono); font-size: 10px; letter-spacing: .08em;
  color: var(--ink-soft); background: transparent; cursor: pointer;
  border: 1px solid var(--rule); border-radius: 2px; padding: 1px 5px;
  line-height: 1.4; transition: color .15s ease, border-color .15s ease;
}
.search-clear:hover { color: var(--accent-ink); border-color: var(--accent-ink); }
.search-clear[hidden] { display: none; }
.window-select {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--mono); font-size: 11px; letter-spacing: .12em; color: var(--ink-soft);
}
.window-select select {
  font-family: var(--mono); font-size: 13px; color: var(--ink);
  background: var(--paper-deep); border: 1.5px solid var(--ink);
  padding: 5px 8px; cursor: pointer;
}
.count { font-family: var(--mono); font-size: 13px; color: var(--accent-ink); font-weight: 600; white-space: nowrap; }
/* WHEN="custom": the from/to date pair next to the selector. Same mono chrome
   as .window-select so it reads as part of the control. */
.custom-range { display: flex; align-items: center; gap: 8px; }
.custom-range[hidden] { display: none; }
.custom-range label {
  display: flex; align-items: center; gap: 5px;
  font-family: var(--mono); font-size: 11px; letter-spacing: .12em; color: var(--ink-soft);
}
.custom-range input[type="date"] {
  font-family: var(--mono); font-size: 12px; color: var(--ink);
  background: var(--paper-deep); border: 1.5px solid var(--ink);
  padding: 4px 6px; cursor: pointer;
}

/* ---------- LISTINGS ---------- */
.listings { margin-top: 18px; }
.loading { font-style: italic; color: var(--ink-soft); padding: 60px 0; text-align: center; }

.day-block { margin-bottom: 38px; }
.day-head {
  display: flex; align-items: baseline; gap: 16px;
  /* Parks flush under the controls bar. --top-day is the cumulative height of
     every sticky layer above (masthead-top + viewbar + controls), measured by
     JS since they vary with chip wrap / responsive fonts. */
  position: sticky; top: var(--top-day, 168px);
  background: var(--paper); z-index: 30;
  padding: 6px 0 4px;
}
.day-dow {
  font-family: var(--display); font-size: clamp(28px, 5vw, 46px);
  text-transform: uppercase; line-height: 1; color: var(--ink);
}
.day-date {
  font-family: var(--mono); font-size: 13px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent-ink); padding-bottom: 4px;
}
.day-rule { flex: 1; height: 0; border-top: 2px solid var(--ink); align-self: center; }

/* neighborhood sub-divisions within a day */
.area-group { margin-bottom: 18px; }
.area-head {
  /* Bottom of the sticky stack: parks just under the day-of-week line, so
     events scroll away beneath both. --top-area = --top-day + the day-head's
     own height (JS-measured). */
  position: sticky; top: var(--top-area, 214px); z-index: 20;
  font-family: var(--mono); font-size: 12px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--accent-ink);
  background: var(--paper);
  margin: 0 0 6px; padding: 0 0 4px;
  border-bottom: 1px solid var(--rule);
}

/* each event = an editorial listing row */
.event {
  display: grid;
  grid-template-columns: 92px 1fr auto;
  gap: 4px 22px;
  align-items: start;
  padding: 16px 8px 16px 10px;
  border-bottom: 1px solid var(--rule);
  text-decoration: none; color: inherit;
  position: relative;
  transition: background .18s ease, padding-left .18s ease;
}
.event:hover { background: var(--paper-deep); padding-left: 18px; }
.event:hover .event-title { color: var(--accent-ink); }
.event::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
  background: var(--accent); transform: scaleY(0); transform-origin: top;
  transition: transform .2s ease;
}
.event:hover::before { transform: scaleY(1); }

.event-time {
  font-family: var(--mono); font-size: 13px; font-weight: 600;
  color: var(--ink); padding-top: 5px; letter-spacing: .02em;
}
.event-time .allday { color: var(--ink-soft); font-weight: 400; font-style: normal; }
.addcal {
  display: block; margin-top: 6px;
  font-family: var(--mono); font-size: 9px; font-weight: 600; letter-spacing: .08em;
  color: var(--ink-soft); background: transparent;
  border: 1px solid var(--rule); border-radius: 2px;
  padding: 1px 5px; cursor: pointer; line-height: 1.5;
  opacity: .55; transition: opacity .15s ease, color .15s ease, border-color .15s ease;
}
.event:hover .addcal { opacity: 1; }
.addcal:hover { color: var(--accent-ink); border-color: var(--accent-ink); }
/* WhatsApp/share button: sits under +CAL, same understated stamp styling. */
.share-wa {
  display: block; margin-top: 4px;
  font-family: var(--mono); font-size: 9px; font-weight: 600; letter-spacing: .08em;
  color: var(--ink-soft); background: transparent;
  border: 1px solid var(--rule); border-radius: 2px;
  padding: 1px 5px; cursor: pointer; line-height: 1.5;
  opacity: .55; transition: opacity .15s ease, color .15s ease, border-color .15s ease;
}
.event:hover .share-wa { opacity: 1; }
.share-wa:hover { color: var(--accent-ink); border-color: var(--accent-ink); }
/* Icon variant (shareIcon editions, e.g. CDMX): a share glyph in place of the
   "+ WhatsApp" text. Size the SVG up from the 9px label size and center it. */
.share-wa--icon { display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px; padding: 3px 5px; }

/* Event-detail modal (the deep-link landing). Reuses the shared .modal shell. */
/* Event modal anatomy: head / scrolling body / pinned action foot. The generic
   .modal-card scrolls as one piece (fine for the short prefs/feedback forms);
   here the body alone scrolls so the head (category slug + close) and the foot
   (Visit site / + CAL / Share — the modal's whole point) never leave the
   viewport, however long a feed's description runs. dvh guards the iOS dynamic
   toolbar; overscroll containment stops the listing behind from scroll-chaining. */
.ev-modal-card {
  max-width: 560px; display: flex; flex-direction: column;
  max-height: min(88vh, calc(100dvh - 24px)); overflow: hidden;
}
.ev-modal-card .modal-body { overflow-y: auto; overscroll-behavior: contain; }
.modal-eyebrow { font-family: var(--mono); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-soft); }
.ev-modal-title { font-family: var(--serif); font-weight: 600;
  font-size: clamp(22px, 3.4vw, 30px); line-height: 1.15; margin: 6px 0 10px; }
.ev-modal-when { font-family: var(--mono); font-size: 13px; font-weight: 600;
  color: var(--accent-ink); margin-bottom: 4px; }
.ev-modal-meta { font-family: var(--serif); font-size: 15px; color: var(--ink-soft);
  margin-bottom: 12px; }
.ev-modal-thumb { display: block; width: 100%; max-height: 260px; object-fit: cover;
  border: 1px solid var(--rule); margin: 0 0 12px; background: var(--paper-deep); }
/* Clamped by default (feeds leak multi-screen press releases; rows clamp to 3
   lines for the same reason) — the mono "READ MORE" expander unclamps. */
.ev-modal-desc { font-family: var(--serif); font-size: 15px; line-height: 1.5;
  color: var(--ink); margin: 0 0 16px;
  display: -webkit-box; -webkit-line-clamp: 8; -webkit-box-orient: vertical;
  overflow: hidden; }
.ev-modal-desc.open { display: block; -webkit-line-clamp: unset; }
.ev-desc-more {
  font-family: var(--mono); font-size: 12px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent-ink); background: none;
  border: none; border-bottom: 1px solid var(--accent-ink);
  padding: 0 0 1px; margin: -8px 0 16px; cursor: pointer;
}
/* Venue map snippet: a fixed-height window of stitched OSM tiles, centered on
   the venue (tiles are absolutely positioned relative to the center by app.js).
   The whole snippet is a link to the maps page; ink border matches the paper
   aesthetic and the pin is a pure-CSS drop marker. */
.ev-modal-map {
  position: relative; display: block; height: 180px; margin-bottom: 16px;
  overflow: hidden; border: 1.5px solid var(--ink); background: var(--paper-deep);
}
/* Newsprint at rest, full color on intent — the same treatment as the listing
   thumbs (filter: saturate(.9) there): raw OSM raster is the loudest color in
   the modal otherwise. */
.ev-map-tiles { position: absolute; left: 50%; top: 50%;
  filter: grayscale(.45) sepia(.15) contrast(1.05); transition: filter .18s ease; }
.ev-modal-map:hover .ev-map-tiles, .ev-modal-map:focus-visible .ev-map-tiles { filter: none; }
.ev-map-tiles img { position: absolute; width: 256px; height: 256px; max-width: none; }
.ev-map-pin {
  position: absolute; left: 50%; top: 50%; width: 12px; height: 12px;
  transform: translate(-50%, -50%); border-radius: 50%;
  background: var(--accent-ink);
  box-shadow: 0 0 0 2.5px var(--paper), 0 0 0 4px var(--accent-ink);
}
.ev-map-attr {
  position: absolute; right: 0; bottom: 0; padding: 1px 4px;
  font-family: var(--mono); font-size: 9px; color: var(--ink-soft);
  background: color-mix(in srgb, var(--paper) 82%, transparent);
}
.ev-modal-map:hover { border-color: var(--accent-ink); }
/* Pinned action foot. One primary (Visit site = the accent .pref-save block —
   leaving the site is the deliberate act this modal exists for); + CAL and
   Share are matching ink-outline secondaries. (The old WhatsApp brand-green
   button was the only off-palette color on the site, and it out-shouted the
   primary — reviewed out 2026-07-18.) */
.ev-modal-actions {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  flex-shrink: 0; padding: 12px 20px; border-top: 2px solid var(--ink);
  background: var(--paper);
}
.ev-modal-actions:empty { display: none; }
.ev-modal-actions .pref-save { text-decoration: none; }
.ev-modal-btn {
  font-family: var(--mono); font-size: 13px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; background: transparent; color: var(--ink);
  border: 1.5px solid var(--ink); padding: 9px 14px; cursor: pointer;
  transition: color .15s ease, border-color .15s ease;
}
.ev-modal-btn:hover { color: var(--accent-ink); border-color: var(--accent-ink); }

/* EXPERIMENT (2026-07-18, CSS-only): on phones the event detail is a
   full-screen SHEET, not a floating card. Rationale (design review): at 92vw
   the tap-outside backdrop is a sliver that can't really be hit, the × is out
   of thumb reach, and an inset card fights the iOS dynamic toolbar. Full-bleed
   makes dismissal honest — the back gesture, which the pushState wiring
   already handles (row click pushes the permalink; Back closes). Everything
   else (pinned head/foot, scrolling body) is unchanged. The accent offset
   shadow needs no room at full-bleed, so border/shadow go; the head keeps the
   ink rule as the only frame. Revert = delete this block. */
@media (max-width: 620px) {
  .ev-modal-card {
    width: 100vw; max-width: none; height: 100dvh; max-height: none;
    border: none; box-shadow: none;
  }
  /* The sheet is fixed-height; let the body take the slack so the action foot
     sits on the viewport bottom even when the (clamped) content runs short. */
  .ev-modal-card .modal-body { flex: 1; }
  /* Bottom padding clears the iOS home-indicator bar over the action foot. */
  .ev-modal-card .ev-modal-actions {
    padding-bottom: max(12px, env(safe-area-inset-bottom));
  }
}

.event-body { min-width: 0; }
/* Event image preview: floated so the title/meta/description wrap around it.
   Sized like a clipped news photo with a thin ink keyline; subtly desaturated
   to sit in the paper palette, full color on hover. */
.event-thumb {
  float: right; width: 116px; height: 116px; object-fit: cover;
  margin: 2px 0 8px 16px; border: 1px solid var(--rule);
  background: var(--paper-deep);
  filter: saturate(.9) contrast(1.02); transition: filter .18s ease;
}
.event:hover .event-thumb { filter: none; }
.event-title {
  font-family: var(--serif); font-weight: 600;
  font-size: clamp(19px, 2.4vw, 25px); line-height: 1.18;
  transition: color .18s ease;
}
.event-meta {
  font-family: var(--mono); font-size: 12px; letter-spacing: .04em;
  color: var(--ink-soft); margin-top: 5px;
  display: flex; flex-wrap: wrap; gap: 6px 12px;
}
.event-desc {
  font-size: 15px; color: var(--ink-soft); margin-top: 7px;
  max-width: 75ch;  /* capped measure keeps long lines readable */
  /* Cap to 3 lines with an ellipsis. Cheap defense against feeds that leak
     long boilerplate or residual markup noise into the description. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.tag {
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  letter-spacing: .14em; text-transform: uppercase;
  padding: 3px 7px; border: 1px solid currentColor; white-space: nowrap;
  align-self: start;
}
.tag[data-cat="theatre"], .tag[data-cat="cultural"] { color: var(--accent-ink); }
.tag[data-cat="dance"] { color: var(--accent); }
.tag[data-cat="music"], .tag[data-cat="lecture"]   { color: var(--slate); }
.tag[data-cat="comedy"] { color: var(--gold); }
.tag[data-cat="food"] { color: var(--moss); }
.tag[data-cat="sports"] { color: var(--slate); }
.tag[data-cat="nature"] { color: var(--fern); }
.tag[data-cat="trivia"] { color: var(--plum); }
.tag[data-cat="film"], .tag[data-cat="museum"], .tag[data-cat="other"] { color: var(--ink-soft); }

/* Tag + arrow share the third grid column (one row 1 cell) so the arrow no
   longer wraps to a trailing row of its own under each event — tightens every
   card's height. Tag on top, arrow tucked just beneath it, both right-aligned. */
.event-aside {
  display: flex; flex-direction: column; align-items: flex-end; gap: 2px;
  margin-top: 6px;
}
.event-arrow { font-family: var(--mono); color: var(--rule); }
.event:hover .event-arrow { color: var(--accent); }

.empty {
  text-align: center; padding: 80px 20px; font-style: italic;
  font-size: 20px; color: var(--ink-soft);
}

/* ---------- VENUES VIEW ---------- */
.venue-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 0; margin-top: 18px;
  border-top: 1px solid var(--rule);
}
.venue {
  padding: 18px 16px; border-bottom: 1px solid var(--rule);
  border-right: 1px solid var(--rule);
  display: flex; flex-direction: column; gap: 6px;
  position: relative; transition: background .15s ease;
}
.venue:hover { background: var(--paper-deep); }
.venue-name {
  font-family: var(--serif); font-weight: 600; font-size: 20px; line-height: 1.15;
}
/* Drop the name below the absolutely-positioned "n ON" badge so long names like
   "Museum of Pop Culture (MoPOP)" don't run under it. Only when a badge shows. */
.venue-haslive .venue-name { margin-top: 18px; }
.venue-name a { color: inherit; text-decoration: none; }
.venue-name a:hover { color: var(--accent-ink); }
.venue-meta {
  font-family: var(--mono); font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-soft);
  display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center;
}
.venue-type {
  border: 1px solid currentColor; padding: 2px 6px; font-size: 10px;
}
.venue-addr {
  font-size: 14px; color: var(--slate); text-decoration: none;
  border-bottom: 1px dotted var(--slate); align-self: flex-start;
}
.venue-addr:hover { color: var(--accent-ink); border-color: var(--accent-ink); }
.venue-dist {
  font-family: var(--mono); font-size: 11px; color: var(--accent-ink);
  font-weight: 600;
}
.venue-status {
  font-family: var(--mono); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; padding: 2px 6px;
}
.venue-status--open { color: var(--moss); border: 1px solid var(--moss); }
.venue-status--closed { color: var(--ink-soft); border: 1px solid var(--rule); }
.venue-hours {
  font-family: var(--mono); font-size: 11px; color: var(--ink-soft);
  letter-spacing: .02em; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; cursor: default;
}
.venue-live {
  position: absolute; top: 14px; right: 14px;
  font-family: var(--mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--paper); background: var(--accent);
  padding: 2px 6px;
  border: none; line-height: 1.4;  /* reset <button> defaults when it's a link */
}
.venue-live--link { cursor: pointer; transition: background .15s ease, transform .1s ease; }
.venue-live--link:hover { background: var(--accent-ink); }
.venue-live--link:active { transform: scale(.94); }
.venue-feedless { opacity: .72; }

/* ---------- VENUE → EVENTS drill-down banner ---------- */
.venue-filter {
  display: flex; align-items: center; gap: 14px;
  margin-top: 14px; padding: 8px 12px;
  background: var(--paper-deep); border-left: 4px solid var(--accent);
  font-family: var(--mono); font-size: 13px; color: var(--ink-soft);
}
/* `display:flex` above outranks the plain `hidden` attribute, so without this
   the empty bar shows in the Events view (a stray rule between the count and the
   first day). Restore hidden's intent. */
.venue-filter[hidden] { display: none; }
.venue-filter-label b { color: var(--ink); }
.venue-filter-clear {
  font-family: var(--mono); font-size: 12px; letter-spacing: .04em;
  background: none; border: 1px solid var(--rule); color: var(--ink-soft);
  padding: 3px 9px; cursor: pointer; transition: all .15s ease;
}
.venue-filter-clear:hover { border-color: var(--ink); color: var(--ink); }

/* ---------- COLOPHON ---------- */
.colophon {
  margin-top: 56px; padding-top: 22px;
  border-top: 2px solid var(--ink);
  font-family: var(--mono); font-size: 12px; color: var(--ink-soft);
}
.fineprint { letter-spacing: .05em; }
.link-btn {
  font-family: var(--mono); font-size: 12px; color: var(--accent-ink);
  background: none; border: none; border-bottom: 1px solid var(--accent-ink);
  cursor: pointer; padding: 0; letter-spacing: .05em;
}
.link-btn:hover { color: var(--ink); border-color: var(--ink); }
.link-btn:disabled { opacity: .5; cursor: wait; }

/* staggered entrance */
.day-block { animation: rise .5s cubic-bezier(.2,.7,.3,1) both; }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }

@media (max-width: 620px) {
  .event { grid-template-columns: 64px 1fr; }
  /* Scoped to LISTING rows — a bare .tag here once leaked into the event
     modal and blanked its category on phones. */
  .event-aside, .event-arrow, .event .tag { display: none; }

  /* --- Masthead-top: keep the utility bar to a single tidy row. The ✉/⚙ icons
     are 72px on desktop, which forces the dateline to wrap and shoves the gear
     off-screen at phone widths — shrink them and the type. The edition label is
     hidden (its info lives in the banner below), letting the city switcher take
     the right slot. --- */
  .masthead-top {
    align-items: center; gap: 6px; padding: 12px 0 8px;
    font-size: 11px; letter-spacing: .08em;
  }
  .masthead-top .edition { display: none; }
  .dateline { line-height: 1.25; }
  .city-select { margin-left: auto; }
  .city-select, .lang-select { margin-left: 10px; font-size: 11px; }
  .prefs-btn { font-size: 30px; margin-left: 8px; }

  /* --- View tabs: on phones the tabs alone occupy the viewbar; the action
     buttons (Free / Near me / Open now) are relocated by app.js into the
     collapsible Filters area below (placeViewActions), so they no longer
     overflow the tab row. --- */

  /* --- Banner: space is precious on phones. Hide the tagline, drop the
     full-width hairline rules that bracket the header stack, and shrink the
     wordmark so even the longest editions ("KARTELERA", "À L'AFFICHE") fit
     without clipping. The sticky bars keep their solid/blurred backgrounds, so
     content still reads clearly as it scrolls under them without the rules. --- */
  .masthead { padding: 6px 0 4px; text-align: center; }
  .logo { font-size: clamp(32px, 11vw, 72px); line-height: .9; text-shadow: 2px 2px 0 var(--accent); }
  .tagline { display: none; }
  .masthead-top, .viewbar, .controls { border-bottom: none; }

  /* --- Collapsible category filters: on phones the 11 chips eat ~5 rows before
     any event. Hide them by default behind the Filters toggle; reveal when the
     nav carries .filters-open (set by app.js). Desktop is untouched. --- */
  .filter-toggle {
    display: inline-flex; align-items: center;
    font-family: var(--mono); font-size: 12px; font-weight: 500;
    letter-spacing: .08em; text-transform: uppercase;
    background: transparent; color: var(--ink);
    border: 1.5px solid var(--ink); border-radius: 0;
    padding: 7px 14px; cursor: pointer;
  }
  /* One compact control row: FILTROS · Buscar · [period]. Flatten .control-meta
     (display:contents) so its children join the .controls flex row instead of
     stacking below; search flex-fills the middle, the period select self-labels
     (its "HORIZON/PERIODO" caption is hidden to save the line), and the count
     drops to its own short line under the row. */
  .controls { gap: 8px 12px; align-items: center; padding: 8px 0 6px; margin-bottom: 0; }
  .filter-toggle { padding: 6px 10px; white-space: nowrap; }
  .control-meta { display: contents; }
  .search-wrap { flex: 1 1 90px; min-width: 0; }
  .search-box, .search-box:focus { width: 100%; }
  #horizonLabel { display: none; }
  .window-select select { padding: 5px 6px; }
  .count { flex-basis: 100%; }

  /* Tighten the vertical rhythm — space was flagged as loose above EVENTOS, and
     above/below the day header (LUNES). Trim the tab padding, the listings top
     margin, and the day-head / area-head padding. */
  .viewtab { padding: 6px 18px 8px; }
  .listings { margin-top: 8px; }
  .day-head { padding: 2px 0 2px; }
  .area-head { padding: 4px 0 3px; margin-bottom: 4px; }
  .day-block { margin-bottom: 24px; }

  /* Chips + the relocated action buttons collapse together behind the toggle.
     Override the desktop one-line sizing: on phones the chips WRAP into a block
     and keep their natural (not width-shared) size — the desktop flex:1/nowrap/
     clamp rules would otherwise squeeze them into an unreadable single strip. */
  #catFilters, #viewActionsSlot { display: none; flex-basis: 100%; }
  .controls.filters-open #catFilters { display: flex; flex-wrap: wrap; gap: 8px; }
  .controls.filters-open #viewActionsSlot { display: flex; flex-wrap: wrap; gap: 8px; }
  #catFilters .chip {
    flex: 0 0 auto; font-size: 12px; letter-spacing: .08em; padding: 6px 12px;
  }

  /* Smaller preview on phones so the title keeps room to breathe. */
  .event-thumb { width: 84px; height: 84px; margin-left: 12px; }
}
@media (prefers-reduced-motion: reduce) {
  .day-block { animation: none; }
  * { transition: none !important; }
}
