*, *::before, *::after { box-sizing: border-box; }

/* ---------------------------------------------------------------------------
   화창한 소풍날 (Sunny Picnic Day) theme
   Warm cream paper, crayon-candy accents, sticker cards, teardrop map pins.
--------------------------------------------------------------------------- */

:root {
  --bg: #fff7ec;
  --surface: #ffffff;
  --ink: #33254e;
  /* #7a6f8f was 4.40:1 on --bg (fails WCAG 4.5:1) — darkened to pass on both
     --bg and white (~5.5:1 / ~5.8:1). */
  --muted: #6b6080;
  --line: #f2e3cf;
  /* #f2e3cf (== --line) is 1.26:1 on white — far short of the 3:1 UI-component
     minimum for form-control borders. */
  --control-border: #9c8862;

  --coral: #ff5d73;
  --coral-deep: #c22a44;
  --coral-tint: #ffe1e6;
  --sunny: #ffc531;
  --sunny-deep: #8a6400;
  --mint: #00b39b;
  --mint-deep: #00614f;
  --mint-tint: #d9f4ee;
  --sky: #4fb6e8;
  --sky-deep: #1d6f9b;
  --sky-tint: #dff1fb;

  --accent: var(--coral);
  --accent-strong: var(--coral-deep);
  --category-exhibition: var(--coral);
  --category-education: var(--mint);
  --category-performance: var(--sky);
  --category-festival: var(--sunny);
  --category-etc: #8a7f9c;

  --card-radius: 20px;
  --shadow: 0 10px 30px rgba(51, 37, 78, .09), 0 2px 6px rgba(51, 37, 78, .05);
  --shadow-pop: 0 16px 40px rgba(51, 37, 78, .14), 0 3px 8px rgba(51, 37, 78, .07);

  --app-nav-bg: rgba(255, 255, 255, .8);
  --app-nav-border: rgba(255, 93, 115, .24);
  --app-nav-text: var(--ink);
  --app-nav-hover-border: rgba(255, 93, 115, .5);
  --app-nav-hover-text: var(--coral-deep);
  --app-nav-active-bg: var(--coral);
  /* White on --coral was 2.97:1 (fails). var(--ink) on --coral is ~4.7:1. */
  --app-nav-active-text: var(--ink);
  --app-nav-active-shadow: 0 12px 28px rgba(255, 93, 115, .3);
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: "Pretendard Variable", Pretendard, "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", -apple-system, sans-serif;
  background:
    radial-gradient(560px 380px at 8% -4%, rgba(255, 197, 49, .2), transparent 68%),
    radial-gradient(520px 360px at 96% 4%, rgba(79, 182, 232, .16), transparent 66%),
    radial-gradient(620px 460px at 88% 96%, rgba(0, 179, 155, .12), transparent 70%),
    radial-gradient(480px 380px at 4% 92%, rgba(255, 93, 115, .1), transparent 64%),
    var(--bg);
  background-attachment: fixed;
  color: var(--ink);
}

button, input, select {
  font: inherit;
}

:focus-visible {
  /* var(--sky) was ~2.2-2.3:1 (fails the 3:1 non-text minimum) — --sky-deep
     reaches ~5.2-5.5:1 against both --bg and white. */
  outline: 3px solid var(--sky-deep);
  outline-offset: 2px;
}

.nadri-page {
  max-width: 1080px;
  min-height: 100vh;
  margin: 0 auto;
  padding: 0 0 72px;
}

/* --------------------------------- header -------------------------------- */

.app-header {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 20px 5vw 18px;
  background: rgba(255, 252, 245, .92);
  backdrop-filter: blur(18px);
}

#app-title {
  min-width: 0;
  overflow: visible;
  font-size: 23px;
  font-weight: 900;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-title-group {
  display: flex;
  align-items: center;
  min-width: 0;
  gap: 12px;
}

.data-info-button {
  display: inline-grid;
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink);
  cursor: pointer;
}

.data-info-button svg {
  width: 24px;
  height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2;
}

.data-info-button .data-info-icon-dot {
  fill: currentColor;
  stroke: none;
}

.data-info-button:hover {
  background: var(--sunny);
}

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

.header-tools {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex: 0 0 auto;
  gap: 10px;
  min-width: 0;
}

.language-select {
  min-height: 36px;
  border: 2px solid var(--control-border);
  border-radius: 999px;
  background: #fff;
  color: var(--ink);
  font-size: 13px;
  font-weight: 800;
  padding: 7px 12px;
  transition: border-color .18s ease;
}

.language-select:hover {
  border-color: var(--sunny);
}

/* ------------------------------- notices ---------------------------------- */

.page-disclaimer {
  position: relative;
  margin: 20px 5vw 0;
  border: 2px solid rgba(255, 197, 49, .55);
  border-radius: 16px;
  background: linear-gradient(0deg, rgba(255, 197, 49, .14), rgba(255, 197, 49, .14)), #fffdf6;
  color: #6b5417;
  font-size: 13px;
  font-weight: 700;
  line-height: 1.55;
  padding: 12px 16px 12px 42px;
}

.page-disclaimer::before {
  content: "💡";
  position: absolute;
  left: 14px;
  top: 11px;
  font-size: 16px;
}

.data-info-disclaimer {
  margin: 0 0 18px;
}

.enrichment-note {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: max-content;
  max-width: 90vw;
  margin: 10px 5vw 0;
  border: 2px solid rgba(79, 182, 232, .35);
  border-radius: 999px;
  background: var(--sky-tint);
  color: var(--sky-deep);
  font-size: 12px;
  font-weight: 800;
  padding: 6px 14px;
}

.enrichment-note::before {
  content: "🔄";
}

/* --------------------------------- tabs ----------------------------------- */

.tab-bar {
  /* List is controlled by the sticky handle; map and data notes have their
     own dedicated actions. Keep these programmatic tab controls out of the
     visual layout at every viewport. */
  display: none;
}

.tab-button {
  min-height: 42px;
  border: 2px solid rgba(51, 37, 78, .14);
  border-radius: 999px;
  background: #fff;
  color: var(--muted);
  cursor: pointer;
  font-size: 14px;
  font-weight: 900;
  padding: 9px 18px;
  transition: transform .16s ease, border-color .16s ease, background .16s ease, color .16s ease;
}

.tab-button:hover {
  border-color: var(--sunny);
  color: var(--ink);
  transform: translateY(-2px);
}

.tab-button.is-active {
  border-color: var(--ink);
  background: var(--sunny);
  color: var(--ink);
  box-shadow: 0 6px 0 rgba(51, 37, 78, .9);
  transform: translateY(-3px);
}

/* The map is opened from the Location handle and data notes from the
   title's question-mark button, so neither needs a visible tab. */
#tab-map-button,
#tab-info-button {
  display: none;
}

/* -------------------------------- filters --------------------------------- */

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 12px;
  margin: 20px 5vw 0;
  padding: 18px;
  border: 2px solid rgba(51, 37, 78, .08);
  border-radius: var(--card-radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}

.field-label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 12px;
  font-weight: 900;
  color: var(--muted);
}

/* Round-3 fix #2: 검색/정렬 each get their own row — flex-basis:100% makes
   each claim the FULL row width in the wrapping .filter-bar, pushing
   whatever comes after it (sort, then the locate button) onto a new line,
   regardless of how much horizontal space would otherwise be available. */
.field-label-search,
.field-label-sort {
  flex-basis: 100%;
}

.field-label-search {
  flex-grow: 1;
  flex-shrink: 1;
}

.field-label select,
.field-label input {
  min-height: 40px;
  border: 2px solid var(--control-border);
  border-radius: 12px;
  background: #fff;
  color: var(--ink);
  padding: 7px 10px;
  transition: border-color .18s ease;
}

.field-label select:hover,
.field-label input:hover {
  border-color: var(--sky);
}

/* Filter controls are represented by one shared sticky-handle system at
   every viewport. The fieldsets below retain their normal accessible
   legends, while the old desktop popover chrome is intentionally removed. */

/* Coordinator visual-test fix: browsers render a <fieldset>'s first
   <legend> straddling the fieldset's own top BORDER by default (vertically
   centered ON the border line, not inside the padding box) — this is
   standard/spec'd "rendered legend" behavior, not something triggered by
   our own CSS, and it's why the title used to render half-clipped above
   the panel. The two spec-sanctioned ways to opt a legend out of that
   special treatment are float != none or position != static — but our
   fieldset is display:flex, and float has NO effect on a flex item's
   layout, so a naive `float: left` on the legend would be silently
   ignored by the flex algorithm and NOT actually opt out. `position:
   absolute` always takes effect regardless of the flex parent, so instead
   of fighting the legend's own visual position at all, it's made a
   standard visually-hidden ("sr-only") element — it still provides the
   fieldset's accessible name and remains the aria-labelledby target for
   the chip group — and a separate, normal, aria-hidden .chip-fieldset-title
   element (a plain <p>, immediately after it — see index.html) renders the
   SAME text as an ordinary in-flow block, fully inside the padding box on
   every drawer/popover. */
.chip-fieldset-legend {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.chip-fieldset-title {
  margin: 0 0 10px;
  padding: 0;
  font-size: 13px;
  font-weight: 900;
  color: var(--muted);
}

.chip-group {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.date-range-fields {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: end;
  gap: 8px;
  min-width: 0;
}

.date-input-label {
  display: grid;
  gap: 5px;
  min-width: 0;
  color: var(--muted);
  font-size: 11px;
  font-weight: 800;
}

.date-input-label input {
  box-sizing: border-box;
  min-width: 0;
  width: 100%;
  min-height: 38px;
  border: 2px solid var(--control-border);
  border-radius: 10px;
  background: #fff;
  color: var(--ink);
  font: inherit;
  padding: 7px 8px;
}

.date-input-label input:focus-visible {
  outline: 3px solid rgba(63, 160, 214, .35);
  outline-offset: 1px;
  border-color: var(--sky-deep);
}

.date-range-divider {
  padding-bottom: 10px;
  color: var(--muted);
  font-weight: 900;
}

.date-range-hint {
  margin: 0;
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
  line-height: 1.45;
}

.date-preset-button,
.date-clear-button {
  align-self: flex-start;
  min-height: 32px;
  border: 2px solid var(--control-border);
  border-radius: 999px;
  background: #fff;
  color: var(--muted);
  cursor: pointer;
  font-size: 11px;
  font-weight: 900;
  padding: 6px 11px;
  transition: border-color .12s ease, color .12s ease, background .12s ease;
}

.date-preset-button {
  background: var(--sky-tint);
  border-color: var(--sky-deep);
  color: var(--sky-deep);
}

.date-preset-button:hover {
  background: #c9e9f8;
  border-color: var(--sky-deep);
  color: var(--sky-deep);
}

.date-clear-button:hover {
  border-color: var(--coral-deep);
  background: var(--coral-tint);
  color: var(--coral-deep);
}

.chip {
  min-height: 34px;
  border: 2px solid var(--control-border);
  border-radius: 999px;
  background: #fff;
  color: var(--ink);
  cursor: pointer;
  font-size: 12px;
  font-weight: 800;
  padding: 6px 13px;
  transition: transform .14s ease, border-color .14s ease, background .14s ease, color .14s ease, box-shadow .14s ease;
}

.chip:hover {
  border-color: var(--sky);
  transform: translateY(-1px);
}

.chip.is-selected,
.chip[aria-pressed="true"] {
  border-color: var(--ink);
  background: var(--sunny);
  color: var(--ink);
  box-shadow: 0 3px 0 rgba(51, 37, 78, .9);
}

/* Honesty caption for the distance filter: which base point ("my location"
   vs the 강남역 fallback) the km chips are actually measured from. */
.base-point-caption {
  margin: 4px 0 0;
  color: var(--muted);
  font-size: 11px;
  font-weight: 700;
}

.clear-filters-button {
  align-self: flex-end;
  min-height: 34px;
  border: 2px dashed rgba(51, 37, 78, .3);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  font-size: 12px;
  font-weight: 800;
  padding: 6px 14px;
  transition: border-color .16s ease, color .16s ease;
}

.clear-filters-button:hover {
  border-color: var(--coral-deep);
  color: var(--coral-deep);
}

.locate-button {
  min-height: 40px;
  border: 2px solid var(--mint);
  border-radius: 999px;
  background: var(--mint-tint);
  color: var(--mint-deep);
  cursor: pointer;
  font-size: 13px;
  font-weight: 900;
  padding: 8px 18px;
  transition: transform .16s ease, box-shadow .16s ease;
}

.locate-button::before {
  content: "📍 ";
}

.locate-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 18px rgba(0, 179, 155, .25);
}

.locate-status {
  margin: 8px 5vw 0;
  color: var(--muted);
  font-size: 13px;
  min-height: 1em;
}

.tab-panel[hidden] {
  display: none !important;
}

/* ------------------------------ month groups ------------------------------ */

.month-group {
  margin: 30px 5vw 0;
}

.month-group h2 {
  display: inline-block;
  margin: 0 0 16px;
  color: var(--ink);
  font-size: 21px;
  font-weight: 900;
  letter-spacing: -0.02em;
  padding-bottom: 8px;
  background-image: radial-gradient(circle at 4px 4px, var(--sunny) 3px, transparent 3.5px);
  background-repeat: repeat-x;
  background-size: 14px 8px;
  background-position: 0 100%;
}

.event-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 16px;
}

/* ------------------------------- event cards ------------------------------ */

.event-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 8px;
  border: 2px solid rgba(51, 37, 78, .08);
  border-radius: var(--card-radius);
  background: var(--surface);
  box-shadow: var(--shadow);
  padding: 16px;
  text-align: left;
  overflow: hidden;
  animation: card-pop .45s cubic-bezier(.2, .9, .3, 1.2) both;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}

.event-card:hover {
  transform: translateY(-4px) rotate(-.4deg);
  box-shadow: var(--shadow-pop);
  border-color: rgba(51, 37, 78, .16);
}

.event-card:nth-child(2) { animation-delay: .04s; }
.event-card:nth-child(3) { animation-delay: .08s; }
.event-card:nth-child(4) { animation-delay: .12s; }
.event-card:nth-child(5) { animation-delay: .16s; }
.event-card:nth-child(6) { animation-delay: .2s; }

@keyframes card-pop {
  from { opacity: 0; transform: translateY(14px) scale(.98); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

.event-meta-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  min-height: 26px;
  gap: 12px;
}

.event-meta-icon {
  display: inline-grid;
  flex: 0 0 25px;
  width: 25px;
  height: 25px;
  place-items: center;
}

.event-meta-control {
  position: relative;
  border: 0;
  background: transparent;
  color: inherit;
  cursor: help;
  padding: 0;
}

.event-meta-svg {
  display: block;
  width: 100%;
  height: 100%;
}

.category-icon-exhibition {
  color: var(--coral-deep);
}

.category-icon-education {
  color: var(--mint-deep);
}

.category-icon-performance {
  color: var(--sky-deep);
}

.category-icon-festival {
  color: var(--sunny-deep);
}

.category-icon-etc {
  color: var(--category-etc);
}

.event-price-meta {
  display: inline-flex;
  align-items: center;
  min-height: 25px;
  gap: 6px;
  font-size: 12px;
  font-weight: 900;
  letter-spacing: -0.02em;
}

.event-price-free-label {
  font-size: 11px;
  font-weight: 1000;
  letter-spacing: .08em;
}

.event-price-meta .event-meta-svg {
  flex: 0 0 23px;
  width: 23px;
  height: 23px;
}

/* The Won sign is a dense, filled glyph rather than a thin outline icon.
   Keep it slightly smaller than the generic metadata SVGs so it does not
   overpower the price amount beside it. */
.event-price-meta-paid .event-meta-svg {
  flex-basis: 17px;
  width: 17px;
  height: 17px;
}

.event-price-meta-free {
  color: var(--mint-deep);
}

.event-price-meta-paid {
  color: var(--sunny-deep);
}

.event-price-meta-unknown {
  color: var(--category-etc);
}

.event-price-value {
  font-variant-numeric: tabular-nums;
}

.event-audience-meta {
  display: inline-grid;
  flex: 0 0 25px;
  width: 25px;
  height: 25px;
  place-items: center;
  color: var(--sky-deep);
}

.event-meta-tooltip {
  position: absolute;
  top: calc(100% + 7px);
  left: 50%;
  z-index: 8;
  width: max-content;
  max-width: min(160px, 70vw);
  border-radius: 8px;
  background: var(--ink);
  box-shadow: 0 4px 10px rgba(51, 37, 78, .2);
  color: #fff;
  font-size: 11px;
  font-weight: 800;
  line-height: 1.25;
  opacity: 0;
  padding: 5px 8px;
  pointer-events: none;
  text-align: center;
  transform: translate(-50%, -4px);
  transition: opacity .14s ease, transform .14s ease;
  visibility: hidden;
}

.event-meta-control:hover .event-meta-tooltip,
.event-meta-control:focus-visible .event-meta-tooltip,
.event-meta-control.is-explained .event-meta-tooltip {
  opacity: 1;
  transform: translate(-50%, 0);
  visibility: visible;
}

/* Map popups and detail panels use the list card's complete interactive
   category, price, and audience metadata strip. */
.map-popup-meta,
.detail-meta {
  margin: 4px 0 10px;
}

.event-card h3 {
  margin: 0;
  color: var(--ink);
  font-size: 16px;
  font-weight: 900;
  line-height: 1.4;
}

.event-card p {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}

.event-card-distance {
  margin-left: auto;
  color: var(--sky-deep);
  font-size: 12px;
  font-weight: 900;
}

.event-card-distance::before {
  content: "🚶 ";
}

.event-card-button {
  align-self: flex-start;
  margin-top: 6px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: #fff;
  color: var(--ink);
  cursor: pointer;
  font-size: 12px;
  font-weight: 900;
  padding: 7px 16px;
  transition: transform .16s ease, background .16s ease, box-shadow .16s ease;
}

.event-card-button:hover {
  background: var(--sunny);
  transform: translateY(-2px);
  box-shadow: 0 4px 0 rgba(51, 37, 78, .9);
}

.empty-message {
  margin: 26px 5vw 0;
  padding: 22px;
  border: 2px dashed rgba(51, 37, 78, .18);
  border-radius: var(--card-radius);
  background: rgba(255, 255, 255, .6);
  color: var(--muted);
  font-size: 14px;
  text-align: center;
}

/* Real prev/next pagination over the combined filtered+sorted list
   (replaces the old cumulative "load more" button). */
.list-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin: 26px 5vw 0;
}

.list-pagination-button {
  min-height: 40px;
  min-width: 44px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: #fff;
  color: var(--ink);
  cursor: pointer;
  font-size: 13px;
  font-weight: 900;
  padding: 9px 20px;
  transition: transform .16s ease, background .16s ease, box-shadow .16s ease, opacity .16s ease;
}

.list-pagination-button:hover:not(:disabled) {
  background: var(--sunny);
  transform: translateY(-2px);
  box-shadow: 0 4px 0 rgba(51, 37, 78, .9);
}

.list-pagination-button:disabled {
  cursor: default;
  opacity: .4;
  transform: none;
  box-shadow: none;
}

.list-pagination-indicator {
  color: var(--muted);
  font-size: 13px;
  font-weight: 800;
  min-width: 100px;
  text-align: center;
}

/* ---------------------------------- map ----------------------------------- */

.map-container {
  height: min(70vh, 640px);
  margin: 22px 5vw 0;
  border: 3px solid #fff;
  border-radius: var(--card-radius);
  overflow: hidden;
  box-shadow: var(--shadow-pop);
}

/* Passive, non-blocking captions above the map — never gate marker
   rendering, just disclose the active base point and how many of the
   filtered items are visible in the current viewport. */
.map-base-caption,
.map-viewport-caption {
  margin: 16px 5vw 0;
  color: var(--muted);
  font-size: 12px;
  font-weight: 800;
}

.map-base-caption[hidden],
.map-viewport-caption[hidden] {
  display: none;
}

/* Teardrop pin markers (pure CSS, drawn inside an L.divIcon — see
   buildMapPinIcon in app.js). The icon box is 30x42; the head is a rotated
   round-cornered square whose bottom corner forms the tip, with a white
   dot punched in the middle. Color comes from the .map-pin-{category}
   class Leaflet applies to the icon wrapper (matching the same 5-category
   palette as .category-icon-*), never an inline style, so the divIcon's
   html stays static/data-free. Restored for viewport-rendered event
   markers — the per-viewport cap (~500) keeps real DOM-node pins cheap
   enough that canvas circleMarkers aren't needed for this scale. */
.map-pin {
  /* Must stay absolute — Leaflet positions marker icons with left/top 0 +
     translate3d, and overriding to relative puts icons back into pane flow. */
  position: absolute;
}

.map-pin-head {
  position: absolute;
  left: 50%;
  top: 2px;
  width: 30px;
  height: 30px;
  margin-left: -15px;
  border-radius: 50% 50% 50% 0;
  transform: rotate(-45deg);
  transform-origin: center;
  background: var(--category-etc);
  border: 3px solid #fff;
  box-shadow: 0 4px 10px rgba(51, 37, 78, .35);
  animation: pin-drop .35s cubic-bezier(.2, .9, .3, 1.3) both;
}

.map-pin-head::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #fff;
}

.map-pin-tip {
  position: absolute;
  left: 50%;
  top: 36px;
  width: 12px;
  height: 4px;
  margin-left: -6px;
  border-radius: 50%;
  background: rgba(51, 37, 78, .28);
  filter: blur(1px);
}

.map-pin-exhibition .map-pin-head { background: var(--category-exhibition); }
.map-pin-education .map-pin-head { background: var(--category-education); }
.map-pin-performance .map-pin-head { background: var(--category-performance); }
.map-pin-festival .map-pin-head { background: var(--category-festival); }
.map-pin-etc .map-pin-head { background: var(--category-etc); }

@keyframes pin-drop {
  from { transform: rotate(-45deg) translate(-8px, 8px); opacity: 0; }
  to { transform: rotate(-45deg) translate(0, 0); opacity: 1; }
}

/* A count cluster keeps the familiar pin silhouette and places its count
   in a red badge above it. It is only used for near-identical screen points. */
.map-cluster {
  position: absolute;
  cursor: pointer;
}

.map-cluster .map-pin-head {
  background: var(--ink);
}

.map-cluster:hover .map-pin-head,
.map-cluster:focus .map-pin-head {
  background: var(--coral-deep);
}

.map-cluster-count {
  position: absolute;
  top: -9px;
  left: 19px;
  z-index: 2;
  display: inline-flex;
  min-width: 19px;
  height: 19px;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  border: 2px solid #fff;
  border-radius: 999px;
  background: var(--coral);
  box-shadow: 0 2px 5px rgba(51, 37, 78, .25);
  color: #fff;
  font-size: 10px;
  font-weight: 900;
  line-height: 1;
}

.map-cluster-popup-title {
  margin: 0 0 7px;
  color: var(--muted);
  font-size: 12px;
  font-weight: 800;
}

.map-cluster-popup-select {
  width: 100%;
  margin: 0 0 9px;
  padding: 7px 9px;
  border: 2px solid var(--line);
  border-radius: 9px;
  background: var(--surface);
  color: var(--ink);
  font: inherit;
  font-weight: 800;
}

.user-dot {
  position: absolute;
}

.user-dot-core {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--sky);
  border: 3px solid #fff;
  box-shadow: 0 0 0 2px rgba(79, 182, 232, .45), 0 2px 8px rgba(51, 37, 78, .3);
  animation: user-dot-pulse 2s ease-out infinite;
}

@keyframes user-dot-pulse {
  0% { box-shadow: 0 0 0 2px rgba(79, 182, 232, .45), 0 2px 8px rgba(51, 37, 78, .3); }
  50% { box-shadow: 0 0 0 8px rgba(79, 182, 232, .18), 0 2px 8px rgba(51, 37, 78, .3); }
  100% { box-shadow: 0 0 0 2px rgba(79, 182, 232, .45), 0 2px 8px rgba(51, 37, 78, .3); }
}

.leaflet-popup-content-wrapper {
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(51, 37, 78, .22);
}

.map-popup strong {
  display: block;
  margin-bottom: 4px;
  color: var(--ink);
}

.map-popup a {
  color: var(--coral-deep);
  font-weight: 800;
}

.map-popup-detail-button {
  display: block;
  margin-top: 8px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--sunny);
  color: var(--ink);
  cursor: pointer;
  font-size: 12px;
  font-weight: 900;
  padding: 6px 14px;
  transition: transform .16s ease;
}

.map-popup-detail-button:hover {
  transform: translateY(-1px);
}

/* -------------------------------- info tab -------------------------------- */

#info-view {
  margin: 22px 5vw 0;
}

html.is-data-info-open .tab-bar,
html.is-data-info-open .mobile-filter-handles,
html.is-data-info-open .filter-bar,
html.is-data-info-open .locate-status,
html.is-data-info-open .enrichment-note {
  display: none !important;
}

.data-info-close-button {
  float: right;
  min-height: 36px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  font-size: 12px;
  font-weight: 900;
  padding: 6px 12px;
}

.go-to-current-location-button {
  align-self: flex-start;
  min-height: 42px;
  margin: 0 0 12px;
  border: 2px solid var(--sky-deep);
  border-radius: 999px;
  background: var(--sky-tint);
  color: var(--sky-deep);
  cursor: pointer;
  font-size: 13px;
  font-weight: 900;
  padding: 8px 14px;
}

.go-to-current-location-button:hover {
  background: var(--sky);
  color: var(--ink);
}

#info-view h2 {
  font-size: 22px;
  font-weight: 900;
  letter-spacing: -0.02em;
}

#info-view h3 {
  display: inline-block;
  margin-top: 22px;
  font-size: 16px;
  font-weight: 900;
  padding-bottom: 6px;
  background-image: radial-gradient(circle at 3px 3px, var(--sky) 2.5px, transparent 3px);
  background-repeat: repeat-x;
  background-size: 12px 7px;
  background-position: 0 100%;
}

#info-view p {
  color: var(--muted);
  line-height: 1.6;
}

.data-info-sources {
  margin: 0;
  padding-left: 18px;
  color: var(--muted);
  font-size: 14px;
  line-height: 1.7;
}

.data-info-sources a {
  color: var(--sky-deep);
  font-weight: 800;
}

.data-info-sources li.is-stale {
  color: var(--coral-deep);
  font-weight: 700;
}

.data-info-filters {
  margin: 0;
  color: var(--muted);
  font-size: 14px;
}

.data-info-filters dt {
  font-weight: 900;
  color: var(--ink);
  margin-top: 8px;
}

.data-info-line {
  font-size: 13px;
}

/* --------------------------------- modal ---------------------------------- */

.detail-modal {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: grid;
  place-items: center;
  padding: 20px;
}

.detail-modal[hidden] {
  display: none !important;
}

.detail-modal-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(37, 25, 60, .55);
  backdrop-filter: blur(3px);
}

.detail-modal-panel {
  position: relative;
  z-index: 1;
  width: min(560px, 100%);
  max-height: 84vh;
  overflow-y: auto;
  border-radius: 24px;
  background: #fff;
  box-shadow: 0 30px 80px rgba(23, 12, 46, .4);
  /* Reserve a dedicated top row for the close control so long titles never
     run underneath it. */
  padding: 58px 24px 24px;
  animation: modal-pop .3s cubic-bezier(.2, .9, .3, 1.2) both;
}

@keyframes modal-pop {
  from { opacity: 0; transform: translateY(16px) scale(.97); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

.detail-close-button {
  position: absolute;
  top: 8px;
  right: 14px;
  min-height: 34px;
  border: 2px solid rgba(51, 37, 78, .16);
  border-radius: 999px;
  background: #fff;
  color: var(--ink);
  cursor: pointer;
  font-size: 12px;
  font-weight: 900;
  padding: 6px 12px;
  transition: background .16s ease, transform .16s ease;
}

.detail-close-button:hover {
  background: var(--coral-tint);
  transform: rotate(4deg);
}

.detail-body img {
  width: 100%;
  border-radius: 16px;
  margin-bottom: 14px;
  border: 2px solid var(--line);
}

.detail-body h3 {
  margin: 0 0 6px;
  font-size: 19px;
  font-weight: 900;
  letter-spacing: -0.02em;
}

.detail-body dl {
  margin: 14px 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 6px 14px;
  font-size: 14px;
}

.detail-body dt {
  color: var(--muted);
  font-weight: 900;
}

.detail-body dd {
  margin: 0;
  color: var(--ink);
}

.detail-body p {
  color: var(--ink);
  line-height: 1.6;
  white-space: pre-line;
}

.detail-extra {
  margin-top: 4px;
}

.detail-loading {
  color: var(--muted);
  font-size: 13px;
  font-style: italic;
}

.detail-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
}

.official-link-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: var(--sunny);
  color: var(--ink);
  font-size: 14px;
  font-weight: 900;
  text-decoration: none;
  padding: 12px 22px;
  box-shadow: 0 5px 0 rgba(51, 37, 78, .9);
  transition: transform .16s ease, box-shadow .16s ease;
}

.official-link-button:hover {
  transform: translateY(-2px);
  box-shadow: 0 7px 0 rgba(51, 37, 78, .9);
}

.official-link-button:active {
  transform: translateY(2px);
  box-shadow: 0 2px 0 rgba(51, 37, 78, .9);
}

/* A missing municipal homepage leads to the deterministic Naver title
   search fallback. Make that distinct from an official-site action using
   Naver's signature green. */
.official-link-button.naver-search-link {
  border-color: #03c75a;
  background: #03c75a;
  color: #fff;
  box-shadow: 0 5px 0 #018d40;
}

.official-link-button.naver-search-link:hover {
  background: #02b854;
  box-shadow: 0 7px 0 #018d40;
}

.official-link-button.naver-search-link:active {
  box-shadow: 0 2px 0 #018d40;
}

/* Secondary pill — same height/shape as .official-link-button and the
   .event-card-button pattern (white bg, 2px ink border, ink text) so it
   reads as a peer action, not the primary one. */
.share-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 46px;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: #fff;
  color: var(--ink);
  cursor: pointer;
  font-size: 14px;
  font-weight: 900;
  padding: 12px 22px;
  transition: transform .16s ease, background .16s ease, box-shadow .16s ease;
}

.share-button:hover {
  background: var(--sunny);
  transform: translateY(-2px);
  box-shadow: 0 4px 0 rgba(51, 37, 78, .9);
}

.share-button:active {
  transform: translateY(1px);
  box-shadow: none;
}

.share-status {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.6;
}

.share-status[hidden] {
  display: none;
}

.share-url-input {
  flex: 1 1 200px;
  min-width: 140px;
  border: 2px solid var(--control-border);
  border-radius: 10px;
  padding: 6px 10px;
  color: var(--ink);
  background: #fff;
  font-size: 12px;
}

.item-disclaimer {
  margin-top: 16px;
  padding: 10px 12px;
  border-radius: 12px;
  background: var(--sky-tint);
  color: var(--sky-deep);
  font-size: 12px;
  line-height: 1.6;
}

[hidden] {
  display: none !important;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* -------------------------------- mobile ---------------------------------- */

@media (max-width: 640px) {
  .tab-bar {
    display: none;
  }

  .app-header {
    align-items: center;
    flex-direction: row;
    gap: 10px;
    /* Keep the title on an opaque layer above the drawer dimmer. Besides
       preserving its brightness, this removes the faint compositing seam
       that previously appeared directly above the handle edge. */
    z-index: 1051;
    background: var(--bg);
    backdrop-filter: none;
  }

  .header-tools {
    align-items: center;
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
    width: auto;
  }

  .header-tools .app-nav {
    flex: 0 0 auto;
    width: auto;
  }

  .language-select {
    flex: 0 0 auto;
  }

  .filter-bar {
    display: contents;
  }

  .locate-status:empty {
    display: none;
  }

  .event-list {
    grid-template-columns: 1fr;
  }
}

/* ------------------------- unified filter drawers --------------------------
   The top handle rows and their fixed panels are identical on desktop and
   mobile. The fieldsets remain shared, so controls and accessibility
   relationships are not duplicated between viewports. ------------------- */

.app-header {
  z-index: 1051;
  background: var(--bg);
  backdrop-filter: none;
}

.filter-bar {
  display: contents;
}

  .mobile-filter-handles {
    position: sticky;
    top: 0;
    z-index: 1070;
    display: block;
    /* Reserve the complete two-row silhouette in normal flow. This keeps
       both the first event card and the no-results message below the handles
       on initial render, while the sticky row still remains at the viewport
       top during scrolling. */
    height: 90px;
    min-height: 90px;
    margin: 0;
    padding: 0;
    overflow: visible;
    isolation: isolate;
  }

  /* Legacy inline toggles are hidden at every width; their fieldsets remain
     fixed-position drawers reached from the unified handle row. */
  .filter-group {
    display: contents;
  }

  .filter-group-toggle {
    display: none;
  }

  /* The side-menu Location handle owns this action on mobile. */
  .locate-button {
    display: none;
  }

  .mobile-filter-edge {
    display: flex;
    align-items: flex-start;
    justify-content: flex-start;
    gap: 5px;
    position: absolute;
    top: 0;
    left: 50%;
    width: max-content;
    margin: 0;
    padding: 0;
    border-top: 3px solid var(--ink);
    transform: translateX(-50%);
  }

  .mobile-filter-edge + .mobile-filter-edge {
    margin-top: 0;
  }

  .mobile-filter-edge-secondary {
    position: absolute;
    z-index: 1;
    margin-top: 0;
    border-top: 0;
    pointer-events: auto;
  }

  .mobile-filter-edge-primary {
    position: absolute;
    z-index: 2;
    border-top: 0;
    pointer-events: none;
  }

  .mobile-filter-edge-primary .mobile-filter-handle {
    pointer-events: auto;
    min-width: 44px;
    padding-right: 8px;
    padding-left: 8px;
  }

  .mobile-filter-edge-primary .mobile-filter-handle-label {
    font-size: 11px;
  }

  .mobile-filter-edge-primary .mobile-filter-handle-clear {
    padding-right: 8px;
    padding-left: 8px;
  }

  /* The primary row's own grid track is shared by both rows, while this
     top stroke still spans the full viewport width behind the tabs. */
  .mobile-filter-edge-primary::before {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    z-index: 0;
    width: 100vw;
    border-top: 3px solid var(--ink);
    transform: translateX(-50%);
    pointer-events: none;
  }

  /* Both rows begin at the exact same top-left grid cell. The secondary row
     is painted first; the foreground row stays above it, so only the lower
     row's outlines are visible through gaps between the first-row tabs. */
  .mobile-filter-edge-secondary .mobile-filter-handle {
    --handle-base-height: 84px;
    /* Keep second-row badges entirely below the foreground row. This is
       independent from the first row's upper-right badge anchor. */
    --handle-badge-top: 50px;
    --handle-badge-right: -7px;
    padding: 48px 12px 6px;
  }

  /* Each control hangs down from its row's shared top edge. This keeps the
     sticky menu flush with the viewport while the rounded tab ends remain
     visibly below the edge, like a physical pull handle. */
  .mobile-filter-handle {
    --handle-base-height: 42px;
    --handle-extension: 0px;
    /* Each row can override these anchors without changing its dimensions. */
    --handle-badge-top: -7px;
    --handle-badge-right: -7px;
    position: relative;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 52px;
    height: calc(var(--handle-base-height) + var(--handle-extension));
    min-height: calc(var(--handle-base-height) + var(--handle-extension));
    padding: 8px 12px;
    border: 3px solid var(--ink);
    border-top: 0;
    border-radius: 0 0 14px 14px;
    background: var(--surface);
    color: var(--ink);
    cursor: pointer;
    white-space: nowrap;
    box-shadow: none;
    transition: height .11s cubic-bezier(.22, .61, .36, 1), min-height .11s cubic-bezier(.22, .61, .36, 1), background .14s ease, color .14s ease;
  }

  .mobile-filter-handle::before {
    content: none;
  }

  .mobile-filter-handle.has-selection {
    background: var(--coral-tint);
    border-color: var(--coral-deep);
  }

  .mobile-filter-handle.has-selection .mobile-filter-handle-label {
    color: var(--coral-deep);
  }

  .mobile-filter-handle .mobile-filter-handle-count {
    position: absolute;
    top: var(--handle-badge-top);
    right: var(--handle-badge-right);
    z-index: 3;
  }

  .mobile-filter-handle:hover {
    --handle-extension: 3px;
  }

  .mobile-filter-handle:active {
    --handle-extension: 5px;
  }

  .mobile-filter-handle[aria-expanded="true"] {
    background: var(--sunny);
  }

  .mobile-filter-handle.is-active {
    background: var(--sunny);
  }

  .mobile-filter-handle-label,
  .mobile-filter-handle-count {
    flex-shrink: 0;
  }

  .mobile-filter-handle-label {
    font-size: 12px;
    font-weight: 900;
    letter-spacing: -0.02em;
  }

  .mobile-filter-handle-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    border-radius: 999px;
    background: var(--coral);
    color: #fff;
    font-size: 10px;
    font-weight: 900;
  }

  .mobile-filter-handle-count[hidden] {
    display: none;
  }


  .mobile-filter-handle-clear {
    color: var(--coral-deep);
    border-color: var(--coral-deep);
  }

  .mobile-filter-handle-clear:hover {
    background: var(--coral-tint);
  }

  /* Superseded on mobile by #handle-clear-all above — avoids showing the
     same action twice once the fieldsets are pulled out of the flex flow
     below. */
  #clear-filters-button {
    display: none;
  }

  /* Filter-panel surfaces begin behind the sticky handle rows. Their
     measured top padding keeps the selectable content below the handles. */
  .chip-fieldset.mobile-drawer {
    position: fixed;
    top: var(--mobile-filter-panel-top, 54px);
    bottom: auto;
    right: 5vw;
    left: 5vw;
    z-index: 1060;
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: auto;
    min-width: 0;
    max-width: none;
    /* Long option lists (notably 지역) stay compact; their existing
       overflow-y:auto makes the choices scroll inside this half-screen panel. */
    max-height: 50vh;
    max-height: 50dvh;
    margin: 0;
    /* The panel surface starts at the handle's top rule, so the measured
       clearance places its actual controls below the complete silhouette. */
    padding: calc(8px + var(--mobile-filter-panel-clearance, 0px)) 20px 20px;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    border: 3px solid var(--line);
    border-top: 0;
    border-radius: 0 0 22px 22px;
    background: var(--surface);
    box-shadow: var(--shadow-pop);
    /* Mobile drawers are always display:flex (visibility is purely via the
       transform below) and never re-trigger the desktop popover's entrance
       animation, which would otherwise briefly fight over the `transform`
       property with the slide transition on first page load. */
    animation: none;
    /* Start just behind the handles, not above the viewport. Because this
       panel sits below the handles' z-index, its short downward reveal is
       visible only below the buttons and never crosses the title area. */
    opacity: 0;
    pointer-events: none;
    transform: translateY(-16px);
    transition: transform .22s cubic-bezier(.2, .9, .3, 1.05), opacity .16s ease;
  }

  .chip-fieldset.mobile-drawer-right,
  .chip-fieldset.mobile-drawer-left {
    right: 5vw;
    left: 5vw;
  }

  .chip-fieldset.mobile-drawer.is-drawer-open {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
  }

  .chip-fieldset.mobile-drawer .chip-fieldset-title {
    display: none;
  }

  /* Search/sort use their label as a desktop field title. A mobile handle
     already names the opened panel, so hide that duplicate visually while
     retaining its label relationship for assistive technology. */
  .mobile-utility-panel-title {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  .chip-fieldset.mobile-drawer .chip-group {
    width: 100%;
    min-width: 0;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    overflow-x: hidden;
  }

  .chip-fieldset.mobile-drawer .chip {
    max-width: 100%;
    overflow-wrap: anywhere;
    text-align: center;
  }

  /* Sort keeps its original select DOM so desktop stays unchanged; on
     mobile it becomes the same top-drop-down panel as the chip drawers. */
  .mobile-utility-panel {
    position: fixed;
    top: var(--mobile-filter-panel-top, 54px);
    bottom: auto;
    right: 5vw;
    left: 5vw;
    z-index: 1060;
    display: flex;
    flex-direction: column;
    gap: 10px;
    width: auto;
    max-width: none;
    margin: 0;
    padding: calc(8px + var(--mobile-filter-panel-clearance, 0px)) 20px 20px;
    border: 3px solid var(--line);
    border-top: 0;
    border-radius: 0 0 22px 22px;
    background: var(--surface);
    box-shadow: var(--shadow-pop);
    overflow-x: hidden;
    overflow-y: auto;
    opacity: 0;
    pointer-events: none;
    transform: translateY(-16px);
    transition: transform .22s cubic-bezier(.2, .9, .3, 1.05), opacity .16s ease;
  }

  .mobile-utility-panel-left,
  .mobile-utility-panel-right {
    right: 5vw;
    left: 5vw;
  }

  .mobile-utility-panel.is-drawer-open {
    opacity: 1;
    transform: translateY(0);
    pointer-events: auto;
  }

  .mobile-utility-panel input,
  .mobile-utility-panel select {
    width: 100%;
  }

  .mobile-utility-panel > span {
    color: var(--ink);
    font-size: 16px;
    font-weight: 900;
  }

  .mobile-filter-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 1050;
    background: rgba(37, 25, 60, .45);
  }

  .mobile-filter-backdrop[hidden] {
    display: none;
  }

/* Desktop has enough horizontal room for every handle. Flatten the two
   mobile rows into one centered line while keeping the same controls and
   drawer behaviour. */
@media (min-width: 641px) {
  .mobile-filter-handles {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 5px;
    height: 48px;
    min-height: 48px;
  }

  .mobile-filter-handles::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    z-index: 0;
    width: 100%;
    border-top: 3px solid var(--ink);
    pointer-events: none;
  }

  .mobile-filter-edge,
  .mobile-filter-edge-primary,
  .mobile-filter-edge-secondary {
    position: static;
    display: contents;
    margin: 0;
    padding: 0;
    border: 0;
    transform: none;
  }

  .mobile-filter-edge-primary::before {
    content: none;
  }

  .mobile-filter-edge-primary .mobile-filter-handle,
  .mobile-filter-edge-secondary .mobile-filter-handle {
    --handle-base-height: 42px;
    --handle-badge-top: -7px;
    --handle-badge-right: -7px;
    min-width: 44px;
    padding: 7px 9px;
  }

  .mobile-filter-handle-label {
    font-size: 11px;
  }

  /* Fixed drawers must not escape the app column on a wide monitor. The
     shared handle row is centred, so keep the desktop surface centred and
     compact as well; the previous 5vw-to-5vw span created an almost
     full-screen date panel with excessively wide calendar fields. */
  .chip-fieldset.mobile-drawer,
  .mobile-utility-panel {
    right: auto;
    left: 50%;
    width: min(760px, calc(100vw - 48px));
    max-width: min(760px, calc(100vw - 48px));
    transform: translate(-50%, -16px);
  }

  .chip-fieldset.mobile-drawer.is-drawer-open,
  .mobile-utility-panel.is-drawer-open {
    transform: translate(-50%, 0);
  }
}

@media (max-width: 640px) {
  .map-container {
    /* Keeps the map immediately below the sticky two-row handles when the
       explicit current-location action scrolls it into view. */
    scroll-margin-top: 98px;
  }

  /* Native date controls need more than a half-width cell on narrow phones.
     Stack them before they overflow horizontally; the fieldset itself grows
     until its half-viewport cap, then scrolls vertically. */
  .chip-fieldset.mobile-drawer,
  .mobile-utility-panel {
    right: 4vw;
    left: 4vw;
  }

  .date-range-fields {
    grid-template-columns: minmax(0, 1fr);
    align-items: stretch;
  }

  .date-range-divider {
    display: none;
  }

  .date-preset-button,
  .date-clear-button,
  .go-to-current-location-button {
    align-self: stretch;
    width: 100%;
    margin: 0;
  }
}
