/* ===== Theme & CSS variables ===== */
:root {
	/* Dark theme (default) */
	--bg: #0f1320;
	--panel: #171c2e;
	--panel-2: #1e2540;
	--ink: #eef1fb;
	--muted: #9aa3c0;
	--line: #2a3252;
	--accent: #6ea8ff;
	--gutter: 20px;             /* page side margin; tightened on mobile below */
	--content-max: 800px;       /* max width of the centered page content (header, board, minimap, footer) */
	--fade-h: 20px;             /* scroll-fade height (= land header sticky offset) */
	--shadow: 0 6px 22px rgba(0,0,0,.35);
	--placeholder: #2c3454;
	--seg-bg: #1b2138;          /* segmented park switcher track */
	--sticky-bg: rgba(15,19,32,.92);
	--live-ink: #b8ffe0;
	--live-bg: rgba(54,211,153,.12);
	--sheet-radius: 42px;        /* floating sheet/card corner radius — overridden per OS below */
	--map-dim: brightness(.8);   /* dim the bright park map in dark mode (reset in light below) */
}

/* Floating sheet/card corners differ by OS (iOS uses larger, continuous corners; Android
   smaller). The platform-* class is set on <html> in app.js. Edit these to taste. */
html.platform-ios { --sheet-radius: 42px; }
html.platform-android { --sheet-radius: 28px; }

@media (prefers-color-scheme: light) {
	:root {
		--bg: #F9F8F4;
		--panel: #ffffff;
		--panel-2: #efeee8;
		--ink: #1b2030;
		--muted: #5d6577;
		--line: #e6e3da;
		--accent: #2f6bdc;
		--map-dim: brightness(1);   /* no dimming in light mode */
		--shadow: 0 6px 22px rgba(40,38,30,.10);
		--placeholder: #ecebe4;
		--seg-bg: #e9e6dd;          /* warm track behind the active white pill */
		--sticky-bg: rgba(249,248,244,.92);
		--live-ink: #0a7d52;
		--live-bg: rgba(54,179,128,.16);
	}
}

@media (max-width: 560px) {
	:root { --gutter: 12px; }
}

/* ===== Base & reset ===== */
* { box-sizing: border-box; }

body {
	margin: 0;
	background: var(--bg);
	color: var(--ink);
	font: 15px/1.5 "Epilogue", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

a { color: inherit; }
* { -webkit-tap-highlight-color: transparent; }
:focus:not(:focus-visible) { outline: none; }

/* ===== Header (brand + "As of" stamp) ===== */
header.site {
	display: flex;
	align-items: center;
	gap: 12px;
	/* +safe-area-inset-top so the brand row clears the status bar / Dynamic Island now that the app runs edge-to-edge
	   (status-bar-style: black-translucent). On non-notch devices / default status bar the inset is 0 → unchanged. */
	padding: calc(18px + env(safe-area-inset-top, 0px)) var(--gutter) 12px;
	max-width: var(--content-max);
	margin: 0 auto;
}

.brand-row {
	display: flex;
	align-items: center;
	gap: 8px;
	flex: 1;
	min-width: 0;
}

.brand-row h1 {
	margin: 0;
	font-family: "Epilogue", sans-serif;
	font-weight: 800;
	font-size: 32px;
	letter-spacing: -1px;
	color: var(--ink);
}

.brand-row .logo-mark {
	height: 30px;
	width: auto;
	flex: none;
	display: block;
	transform: translateY(-3px);
}

@media (prefers-color-scheme: dark) {
	.brand-row .logo-mark { filter: invert(1); }
}

/* ===== Skeleton loaders ===== */
/* Shown in place of the minimap and location cards while data loads (baked into index.html so they
   paint immediately, even on a pull-to-refresh reload). They mirror the real layout's dimensions so
   nothing shifts when the content swaps in. */
.skel {
	position: relative;
	overflow: hidden;
	background: var(--placeholder);
	border-radius: 8px;
}
.skel::after {   /* a soft highlight band sweeping across */
	content: "";
	position: absolute;
	inset: 0;
	transform: translateX(-100%);
	background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .55), transparent);
	animation: skel-sweep 1.4s ease-in-out infinite;
}
@media (prefers-color-scheme: dark) { .skel::after { background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .09), transparent); } }
@keyframes skel-sweep { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .skel::after { animation: none; } }

/* Real content easing in as it replaces the skeleton (applied on the first load only). */
.reveal-in { animation: reveal-in .4s ease both; }
@keyframes reveal-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .reveal-in { animation: none; } }

/* Minimap placeholder — same box as .minimap. */
.minimap-skeleton {
	width: calc(100% - 2 * var(--gutter));
	max-width: calc(var(--content-max) - 2 * var(--gutter));
	height: 180px;
	margin: 0 auto 2px;
	border: 1px solid var(--line);
	border-radius: 16px;
	box-shadow: var(--shadow);
	/* Show the SAME pre-blurred map preview the live minimap uses (loader.webp, preloaded in <head>) instead
	   of the gray .skel fill — this element is baked into index.html and paints on the FIRST frame, before
	   Leaflet/app.js run, so it's what a refresh actually shows first. Overrides .skel's gray background. */
	background: #8ea472 url(/static/photos/map/loader.webp?v=2) center/cover no-repeat;
}
.minimap-skeleton.skel::after { display: none; }   /* no shimmer sweep over the map preview */
.minimap-skeleton[hidden] { display: none; }
@media (min-width: 560px) { .minimap-skeleton { height: 250px; } }

/* Location card placeholders — mirror .location / .loc-head / .char-row. */
.skel-loc {
	background: var(--panel);
	border-radius: 16px;
	padding: 0 18px 22px;
	margin-top: 12px;
	box-shadow: 0 1px 3px rgba(40, 38, 30, .06);
}
.skel-loc-head { display: flex; flex-direction: column; gap: 8px; padding: 18px 0 6px; }
.skel-pill { width: 108px; height: 30px; border-radius: 999px; }
.skel-title { width: 62%; height: 22px; border-radius: 7px; }
.skel-char { display: flex; gap: 12px; padding-top: 26px; }
.skel-avatar { width: 52px; height: 52px; border-radius: 50%; flex: none; }
.skel-char-main { flex: 1; min-width: 0; }
.skel-name { display: block; width: 46%; height: 18px; border-radius: 6px; }
.skel-info { display: block; width: 78%; height: 12px; margin-top: 9px; border-radius: 5px; }
.skel-times { display: flex; gap: 6px; margin-top: 13px; }
.skel-chip { width: 74px; height: 28px; border-radius: 8px; }

.updated {
	color: var(--muted);
	font-size: 12px;
	font-weight: 400;
	line-height: 1.3;
	text-align: right;
	white-space: nowrap;
	flex: none;
}
/* The "As of" stamp with a small refresh spinner to its left (shown only while a repoll runs).
   Space for the icon is always reserved so the timestamp never shifts when it appears. */
.updated-row {
	display: flex;
	align-items: flex-end;        /* align the spinner with the time line (bottom), not the 2-line block's middle */
	justify-content: flex-end;
	gap: 5px;
	flex: none;
}
.updated-spin {
	width: 12px;
	height: 12px;
	margin-bottom: 3px;           /* optically center the 12px icon on the time text (its glyphs sit above the line-box center) */
	color: var(--muted);          /* .svgicon paints with currentColor → adapts to dark mode */
	opacity: 0;                   /* hidden until loading (space stays reserved → no shift) */
	transition: opacity .15s ease;
}
.updated-row.loading .updated-spin {
	opacity: 1;
	animation: updated-spin .8s linear infinite;
}
@keyframes updated-spin { to { transform: rotate(360deg); } }

/* ===== Sticky bar & scroll fade ===== */
/* The bar sits above the content and its own bottom edge fades out (frost + blur),
   so content scrolls cleanly under it — no separate fade element. The padding-bottom
   is the fade band; the negative margin lets the content below overlap it; the mask
   feathers the band (and the backdrop blur with it). */
.sticky {
	position: sticky;
	/* Pin BELOW the status bar / island (edge-to-edge mode) rather than at the very top. This only changes the
	   PINNED position, not the flow position, so there's no extra gap at the top of the page. Inset 0 on default. */
	top: env(safe-area-inset-top, 0px);
	z-index: 10;
	background: var(--sticky-bg);
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
	/* Top breathing room lives here as PADDING (not the minimap's margin, which collapsed up through the
	   sticky and put its real pin point 12px below any sibling edge — breaking scroll-to-top). As padding
	   it can't collapse: #eventBanners' bottom is exactly where this pins, so the scroll math is trivial. */
	padding-top: 12px;
	padding-bottom: var(--fade-h);
	margin-bottom: calc(-1 * var(--fade-h));
	-webkit-mask-image: linear-gradient(#000 calc(100% - var(--fade-h)), transparent);
	mask-image: linear-gradient(#000 calc(100% - var(--fade-h)), transparent);
}

.bar {
	max-width: var(--content-max);
	margin: 0 auto;
	padding: 10px var(--gutter) 0px var(--gutter);
}

/* ===== Minimap teaser (sticky, above the park switcher) ===== */
.minimap {
	display: block;
	position: relative;
	width: calc(100% - 2 * var(--gutter));
	max-width: calc(var(--content-max) - 2 * var(--gutter));
	height: 180px;
	margin: 0 auto 2px;
	padding: 0;
	border: 1px solid var(--line);
	border-radius: 16px;
	overflow: hidden;
	background: var(--panel-2);
	cursor: pointer;
	box-shadow: var(--shadow);
	color: var(--ink);
}
.minimap[hidden] { display: none; }
.minimap-stage { position: absolute; inset: 0; overflow: hidden; }
.minimap-pins { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
/* A locked Leaflet map (Disney's tiles) is the minimap's base. isolation:isolate contains Leaflet's own
   pane z-indexes so the .minimap-pins overlay stays on top. */
.minimap-tile { position: absolute; inset: 0; isolation: isolate; }
/* Instant preview UNDER the tiles: a pre-blurred map image (loader.webp, preloaded in <head>) shows the
   moment the strip renders — before even the z15 base tile arrives — so a cold load reads as a soft map,
   not a flat fill. Tiles paint opaquely over it as they stream in. #8ea472 (map-grass green) stays as the
   color fallback if the webp itself hasn't loaded yet. Bump ?v= when loader.webp changes (immutable cache). */
/* Green + blurred-map fallback behind the transparent MapLibre canvas, so a cold load shows soft map, never
   gray. Bump ?v= when loader.webp changes (immutable cache). */
#minimapTile { background: #8ea472 url(/static/photos/map/loader.webp?v=2) center/cover no-repeat; }
.minimap .maplibregl-map { font: inherit; }
/* Each pin is a MapLibre marker (.mm-pin-icon) positioned at the spot; centre the shared .mm-pin on that
   point. The transform is static, so it never fights the map's own pan/zoom transform. */
.minimap .mm-pin-icon .mm-pin { transform: translate(-50%, -50%); }
/* Keep the highlighted pin above overlapping neighbours in crowded areas. The class sits on the MapLibre marker
   wrapper (sibling of the other markers); !important beats any inline z-index MapLibre may set from position. */
.minimap .mm-pin-icon.mm-cur, .evt-map .mm-pin-icon.mm-cur { z-index: 1000 !important; }
/* A pin is a small stack of up to three character faces (+N badge), centered on its point.
   All pins are full-opacity until a spot is focused (.has-focus), then the others dim. */
.mm-pin {
	position: absolute;
	top: 0;
	left: 0;
	display: flex;
	align-items: center;
	opacity: 1;
}
.minimap.has-focus .mm-pin { opacity: .5; }
.mm-stack { display: flex; }
.mm-av {
	position: relative;
	width: 26px;
	height: 26px;
	border-radius: 50%;
	border: 2px solid #fff;
	overflow: hidden;
	background: var(--placeholder);   /* neutral base behind the initials — never a white blank */
	box-shadow: 0 1px 4px rgba(0,0,0,.4);
	flex: none;
}
.mm-av:not(:first-child) { margin-left: -10px; }
.mm-av:nth-child(1) { z-index: 3; }
.mm-av:nth-child(2) { z-index: 2; }
.mm-av:nth-child(3) { z-index: 1; }
.mm-av.special { border-color: #f5b53d; }
.mm-av.event { border-color: var(--evt-bg, #96C940); }   /* seasonal event: the event's theme ring instead of gold */
.mm-av img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.mm-ini { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: var(--muted); }
/* An image avatar stays fully hidden until its photo resolves, then fades in — so you never see a
   blank circle pop to a photo. JS adds .mm-loaded on the img's load event (synchronously if it was
   already cached → instant, no fade). The initials behind only surface if the photo can't load. */
.mm-av.mm-pending { opacity: 0; transition: opacity .25s ease; }
.mm-av.mm-pending.mm-loaded { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .mm-av.mm-pending { transition: none; } }
/* Same badge as the full-map pins: a small accent circle on the stack's top-right corner. */
.mm-count {
	position: absolute;
	top: -6px;
	right: -6px;
	z-index: 5;
	min-width: 16px;
	height: 16px;
	padding: 0 4px;
	border-radius: 999px;
	background: var(--accent);
	color: #fff;
	font-size: 10px;
	font-weight: 700;
	line-height: 16px;
	text-align: center;
	box-shadow: 0 1px 3px rgba(0,0,0,.35);
}
/* The location you're scrolled to: full opacity + a yellow ring hugging its stack. */
.minimap.has-focus .mm-pin.current { opacity: 1; z-index: 5; }
.mm-pin.current::before {
	content: "";
	position: absolute;
	inset: -4px;
	border-radius: 999px;
	border: 2px solid #ffd400;
	box-shadow: 0 0 8px 1px rgba(255,212,0,.6);
	pointer-events: none;
	z-index: 0;
}
/* Ease the pin highlight as you scroll; suppressed on the first frame (no .ready yet). */
.minimap.ready .mm-pin { transition: transform .4s ease, opacity .3s ease; }

.minimap-expand {
	position: absolute;
	top: 8px;
	right: 8px;
	z-index: 3;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 30px;
	height: 30px;
	border-radius: 9px;
	background: color-mix(in srgb, var(--panel) 86%, transparent);
	-webkit-backdrop-filter: blur(4px);
	backdrop-filter: blur(4px);
	box-shadow: 0 1px 4px rgba(0,0,0,.25);
}
.minimap-expand .svgicon { width: 16px; height: 16px; }
/* Hidden until the full WebGL map has warmed (its context + style are ready), then it fades in — so tapping it
   always opens onto a ready map instead of a cold-start delay. body.tm-warm-ready is set when the map fires
   tm-ready (usually during the warm), with a ~2.6s safety fallback in tile-map.js so it can never stay hidden. */
.minimap-expand { opacity: 0; pointer-events: none; }
/* Animation (not a transition) so it fades in even when the map warms BEFORE the minimap is built — on a cached
   refresh the button would otherwise mount already-visible with no transition to play, so it just popped in. */
body.tm-warm-ready .minimap.ready .minimap-expand { opacity: 1; pointer-events: auto; animation: mmExpandIn .34s ease; }
@keyframes mmExpandIn { from { opacity: 0; } to { opacity: 1; } }
@media (hover: hover) { .minimap:hover .minimap-expand { background: var(--panel); } }

/* ===== Minimap "not ready yet" loader =====
   Until BOTH the full WebGL map is warm (body.tm-warm-ready → the expand button can appear) AND the minimap's own
   pins are built (.minimap.ready), the whole minimap is a NON-INTERACTIVE loader: a blurred-map placeholder with a
   shimmer sweep covering everything. So you never see the +N badge before the pins, and can't tap into a map that
   isn't ready (the dead-tap problem). It all clears — and taps enable — the instant both are ready. */
.minimap { pointer-events: none; }
body.tm-warm-ready .minimap.ready { pointer-events: auto; }
.minimap::before {                              /* placeholder cover — the blurred map, MUTED (desaturated + dimmed)
	                                               to a calm surface so the sheen is the only motion, not competing
	                                               with a busy saturated photo (that's what reads as cheesy). */
	content: ""; position: absolute; inset: 0; z-index: 4; border-radius: inherit;
	/* Muted with a GREEN wash (the map's own #8ea472) rather than a dark dim — calms the busy colors toward the
	   map's grass tone so the sheen reads clean, without darkening it. */
	background: linear-gradient(rgba(142, 164, 114, .42), rgba(142, 164, 114, .42)), #8ea472 url(/static/photos/map/loader.webp?v=2) center/cover no-repeat;
	filter: saturate(.82);
}
.minimap::after {                               /* one wide, soft, low-contrast highlight that passes through — no
	                                               skew, no bright streak. Transform-only → GPU-composited, smooth. */
	content: ""; position: absolute; top: 0; bottom: 0; left: -70%; width: 70%; z-index: 5;
	background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .18), transparent);
	animation: mmSheen 1s ease-in-out infinite; will-change: transform;
}
body.tm-warm-ready .minimap.ready::before, body.tm-warm-ready .minimap.ready::after { content: none; }
@keyframes mmSheen { to { transform: translateX(243%); } }
/* Lock toggle in the minimap's bottom-right — stops the map from following your scroll. */
.minimap-lock {
	position: absolute;
	bottom: 8px;
	right: 8px;
	z-index: 3;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 30px;
	height: 30px;
	border-radius: 9px;
	background: color-mix(in srgb, var(--panel) 86%, transparent);
	-webkit-backdrop-filter: blur(4px);
	backdrop-filter: blur(4px);
	box-shadow: 0 1px 4px rgba(0,0,0,.25);
	color: var(--muted);
	cursor: pointer;
}
.minimap-lock .svgicon { width: 15px; height: 15px; }
.minimap-lock.locked { background: #1c1f29; color: #fff; box-shadow: 0 1px 5px rgba(0,0,0,.35); }
@media (hover: hover) {
	.minimap-lock:hover { color: var(--ink); }
	.minimap-lock.locked:hover { color: #fff; }
}
/* Twice as tall on desktop (more map context); phones keep the compact strip. The character
   pins are ~3× bigger on desktop too, so they stay legible on the larger, more-zoomed map. */
@media (min-width: 560px) {
	.minimap { height: 250px; }
	.mm-av { width: 48px; height: 48px; border-width: 3px; }
	.mm-av:not(:first-child) { margin-left: -26px; }
	.mm-ini { font-size: 24px; }
	.mm-count { min-width: 24px; height: 24px; padding: 0 6px; font-size: 13px; line-height: 24px; top: -7px; right: -7px; }
	.mm-pin.current::before { inset: -6px; border-width: 3px; }
}

/* ===== Park switcher (segmented tabs) + swipe animation ===== */
.tabs {
	position: relative;
	display: flex;
	gap: 4px;
	background: var(--seg-bg);
	padding: 5px;
	border-radius: 16px;
	min-height: 52px;   /* reserve the switcher's height so the empty→populated swap doesn't shift the board */
}

/* The white active pill — slides between tabs and tracks the swipe. */
.tab-ind {
	position: absolute;
	top: 5px;
	bottom: 5px;
	left: 5px;
	z-index: 0;
	width: calc((100% - 14px) / 2);
	background: var(--panel);
	border-radius: 12px;
	box-shadow: 0 1px 4px rgba(40,38,30,.16);
	/* width transitions too, since slideSegPill sizes the pill to the active button (for uneven tabs). */
	transition: transform .24s cubic-bezier(.2,.8,.2,1), width .24s cubic-bezier(.2,.8,.2,1);
}

.tabs.dragging .tab-ind { transition: none; }   /* follow the finger 1:1 while swiping */

.tab {
	position: relative;
	z-index: 1;
	flex: 1;
	background: none;
	border: none;
	border-radius: 12px;
	padding: 13px 10px;
	cursor: pointer;
	font-family: inherit;
	font-weight: 700;
	font-size: 15px;
	letter-spacing: -.2px;
	white-space: nowrap;
	text-align: center;
	color: var(--ink);
	transition: color .15s;
}

.tab[aria-selected="false"] { color: color-mix(in srgb, var(--ink) 78%, var(--seg-bg)); }

@media (hover: hover) {
	.tab[aria-selected="false"]:hover { color: var(--ink); }
}

.tab[aria-selected="true"] { color: var(--ink); }

/* Park switch: a quick opacity fade on the new board content. Just opacity (no
   transform) so it composites cleanly on mobile; the tab pill's slide gives direction. */
@keyframes board-fade {
	from { opacity: .4; }
	to { opacity: 1; }
}

/* ===== Filter toolbar — chips, search, multi-select dropdown ===== */
.toolbar {
	display: flex;
	gap: 8px;
	align-items: center;
	margin: 12px calc(-1 * var(--gutter)) 0;
	padding: 4px var(--gutter) 0 var(--gutter);   /* full-bleed scroll; leading/trailing inset = .bar gutter */
	flex-wrap: nowrap;
	overflow-x: auto;
	scrollbar-width: none;
	-webkit-overflow-scrolling: touch;
}

.toolbar::-webkit-scrollbar { display: none; }
.toolbar > * { flex: none; }

.search-wrap {
	display: none;
	position: relative;            /* anchors the custom caret */
	flex: 1 1 200px;
	min-width: 160px;
}

.search {
	width: 100%;
	background: var(--panel);
	border: none;
	color: var(--ink);
	caret-color: transparent;      /* hide the native caret; .search-caret replaces it */
	padding: 10px 20px 9px 20px;
	border-radius: 999px;
	font: inherit;                 /* Epilogue, to match the rest of the UI */
	font-size: 16px;               /* 16px also stops iOS Safari auto-zooming on focus */
	line-height: 1;
	box-shadow: 0 1px 2px rgba(40,38,30,.07);
}

.search:focus,
.search:focus-visible { outline: none; }

/* Custom caret. Epilogue's glyphs ride high in the line box, so the native caret
   hangs low; this one is drawn vertically centered and tracks the cursor in JS. */
.search-caret {
	position: absolute;
	top: 50%;
	transform: translateY(calc(-50% - 1.25px));   /* up onto the high-riding caps */
	width: 1px;
	height: 17px;
	border-radius: 1px;
	background: var(--ink);
	opacity: 0;                    /* shown only while focused (JS toggles .on) */
	pointer-events: none;
}
.search-caret.on { animation: caret-blink 1.06s steps(1, end) infinite; }
@keyframes caret-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

.chip[hidden] { display: none !important; }
.toolbar.searching .search-wrap { display: block; }
.toolbar.searching > .chip:not(#searchBtn):not(#eventSearchBtn),
.toolbar.searching > .ms { display: none; }

.chip.ic {
	padding: 10px 11px;
	line-height: 1;
	display: inline-flex;
	align-items: center;
	justify-content: center;
}

/* Icons are SVG files used as a mask + filled with a CSS color, so they recolor
   dynamically (theme, active, hover) from one set of files. */
.svgicon {
	display: block;
	flex: none;
	background-color: currentColor;
	-webkit-mask: var(--svg) center / contain no-repeat;
	mask: var(--svg) center / contain no-repeat;
}

.chip .svgicon { width: 16px; height: 16px; }
/* Colored filter-chip icons — shared by class, so any "Now" / "Special" chip (home OR event) is styled
   the same with no per-id rules. */
.chip-now .svgicon { background-color: #5cb672; }     /* "Now" keeps its live-green */
.chip-special .svgicon { background-color: #f5b53d; } /* "Special" keeps its gold */

/* Text-chip icons align with the high-riding text; icon-only chips (.ic) stay centered. */
.chip:not(.ic) .svgicon { transform: translateY(-1.7px); }

.ms { position: relative; flex: none; }

.chip {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	background: var(--panel);
	border: none;
	color: var(--muted);
	padding: 10px 14px 8px 14px;
	border-radius: 999px;
	cursor: pointer;
	white-space: nowrap;
	font-family: inherit;
	font-size: 12px;
	font-weight: 400;
	line-height: 1;
	box-shadow: 0 1px 2px rgba(40,38,30,.07);
}

@media (hover: hover) {
	.chip:hover { color: var(--ink); }
}

.chip.active { background: var(--ink); color: var(--bg); box-shadow: none; }

/* Lands has no leading icon, so add extra left padding to match the others. */
#landBtn { padding-left: 18px; }

/* ===== Special badge ===== */
/* Caps have ~no descender so the ink sits high — bias the padding and nudge the
   whole badge up to optically center it against the high-riding name. */
.badge-special,
.badge-unlisted {
	display: inline-flex;
	align-items: center;
	line-height: 1;
	font-size: 11px;
	font-weight: 600;
	letter-spacing: .4px;
	text-transform: uppercase;
	border-radius: 6px;
	padding: 5px 7px 3px 7px;
	transform: translateY(-1px);
	color: #2a1c00;
	background: #ffcf6b;
}

/* Unlisted = hand-added, not from Disney's feed. Neutral gray to set it apart
   from the orange "Special" badge. */
.badge-unlisted {
	color: #3a3a3a;
	background: #d8d8d4;
}

/* Seasonal-event badge: the event's own theme colors (set inline per character) replace the gold
   "Special" look — e.g. Oogie Boogie Bash green. */
.badge-event {
	color: var(--evt-ink, #12300a);
	background: var(--evt-bg, #96C940);
}

/* Entertainment type badge (Parade / Fireworks / Show / …) — neutral gray, like the Characters chip. */
.badge-ent {
	color: #3a3a3a;
	background: #d8d8d4;
}

/* ===== Layout ===== */
main { max-width: var(--content-max); margin: 0 auto; padding: 8px var(--gutter) 60px; }

/* ===== Footer — small centered contact line ===== */
.site-foot {
	max-width: var(--content-max);
	margin: 0 auto;
	padding: 0 var(--gutter) 40px;
	text-align: center;
	font-size: 12px;
	color: var(--muted);
}
.site-foot a { color: inherit; text-decoration: underline; }

/* ===== Locations — a white card holding flat character rows ===== */
.location {
	background: var(--panel);
	border-radius: 16px;
	padding: 0 18px 22px;
	margin-top: 12px;
	box-shadow: 0 1px 3px rgba(40,38,30,.06);
}

/* Tappable header: a colored land pill (with the expand button beside it in the top-right) above
   the location name, like the map card. */
.loc-head {
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding: 16px 0 4px;
	cursor: pointer;
}
.loc-head:active { opacity: .65; }
.loc-head-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.loc-head h3 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: -.2px; }
.loc-expand {
	flex: none;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 30px;
	height: 30px;
	border-radius: 9px;
	background: var(--panel-2);
	color: var(--muted);
}
.loc-expand .svgicon { width: 14px; height: 14px; }
@media (hover: hover) { .loc-head:hover .loc-expand { color: var(--ink); } }

.loc-photo {
	width: 56px;
	height: 56px;
	border-radius: 12px;
	background: var(--placeholder);
	flex: none;
	position: relative;
	overflow: hidden;
	border: none;
	padding: 0;
	cursor: pointer;
	transition: transform .12s;
}

@media (hover: hover) {
	.loc-photo:hover { transform: scale(1.04); }
}

.loc-photo:active { transform: scale(.97); }
.loc-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* The location header's map preview (replaces the photo thumbnail) — a touch larger than a
   thumbnail; the loc-mini styles (pin + expand cue) apply, just sized down from the drawer. */
.loc-head .loc-mini { width: 72px; height: 72px; }
.loc-head .loc-mini-expand { width: 22px; height: 22px; right: 5px; bottom: 5px; border-radius: 7px; }
.loc-head .loc-mini-expand .svgicon { width: 12px; height: 12px; }

/* ===== Character rows (land view) ===== */
.char-row { display: flex; gap: 12px; padding-top: 30px; }
.char-row .char-main { min-width: 0; flex: 1; }

.name {
	font-weight: 700;
	font-size: 20px;             /* match the character names in the map location card (.sheet-char .name) */
	line-height: 1.2;
	display: flex;
	align-items: center;
	gap: 8px;
	flex-wrap: wrap;
}

/* When a long name pushes the badge onto its own line, balance its spacing: the flex gap (8px) is
   fine horizontally when the badge sits on the name line, but as a row-gap it left too much space
   ABOVE the wrapped badge — so drop the row-gap and let the badge's own symmetric margin provide
   equal breathing room above and below (the pill has no line-height leading of its own). */
.char-row .name { row-gap: 0; }
.char-row .name .badge-special,
.char-row .name .badge-ent,
.char-row .name .badge-unlisted { margin: 3px 0 5px 0; }

.char-info {
	color: var(--muted);
	font-size: 12px;
	font-weight: 400;
	line-height: 1.4;
	margin: 0 0 9px;
}
/* A caveat under a character's name (unlisted-appearance conditions, etc.). Deliberately quiet —
   plain muted text with a small info icon, NOT a filled callout — so it stays subordinate to the
   name, the Unlisted/Special badge, and the times instead of out-shouting them. */
.char-subtext {
	display: flex;
	align-items: flex-start;
	gap: 3px;
	max-width: 100%;
	margin: 2px 0 4px 0;
	color: var(--muted);
	font-size: 12px;
	font-weight: 400;
	line-height: 1.4;
}
.char-subtext .svgicon { width: 13px; height: 13px; flex: none; margin-top: 0px; }
.char-head-main { min-width: 0; }

.jump {
	color: inherit;
	cursor: pointer;
	text-decoration: underline;
	text-underline-offset: 2px;
	text-decoration-color: color-mix(in srgb, currentColor 32%, transparent);
}

@media (hover: hover) {
	.jump:hover { color: var(--ink); text-decoration-color: currentColor; }
}

.avatar {
	width: 52px;
	height: 52px;
	border-radius: 50%;
	background: var(--placeholder);
	flex: none;
	display: grid;
	place-items: center;
	color: var(--muted);
	font-weight: 700;
	font-size: 15px;
	position: relative;
	overflow: hidden;
}
/* Character avatars with no photo now show the drawn placeholder instead of initials. Scoped away from activity
   avatars (they contain .evt-act-emoji and keep their emoji fallback). color:transparent hides the initials text.
   The SVG is INLINED as a data URI (not a separate file) so it ALWAYS paints the instant the avatar renders — it
   ships inside the already-loaded stylesheet, so a slow/offline network can never delay or block it. */
.avatar:not(:has(.evt-act-emoji)) {
	background: var(--placeholder) url("data:image/svg+xml,%3Csvg width='200' height='200' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cg clip-path='url(%23clip0_592_39)'%3E%3Crect width='200' height='200' fill='white'/%3E%3Crect width='200' height='200' fill='url(%23paint0_linear_592_39)'/%3E%3Ccircle cx='100.004' cy='111.545' r='35.2398' fill='%23FFFFFF'/%3E%3Cellipse cx='138.528' cy='77.2347' rx='20.4714' ry='19.1545' transform='rotate(45 138.528 77.2347)' fill='%23FFFFFF'/%3E%3Cellipse cx='60.2756' cy='77.235' rx='20.4714' ry='19.1545' transform='rotate(120 60.2756 77.235)' fill='%23FFFFFF'/%3E%3C/g%3E%3Cdefs%3E%3ClinearGradient id='paint0_linear_592_39' x1='100' y1='0' x2='100' y2='200' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='%2374A9C9'/%3E%3Cstop offset='1' stop-color='%23BDE0F4'/%3E%3C/linearGradient%3E%3CclipPath id='clip0_592_39'%3E%3Crect width='200' height='200' fill='white'/%3E%3C/clipPath%3E%3C/defs%3E%3C/svg%3E") center/cover no-repeat;
	color: transparent;
}

.avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* ===== Land pill — used only in the "By character" view ===== */
/* Text rides high in the line box, so bias the padding (top > bottom) to center it. */
.pill {
	display: inline-flex;
	align-items: center;
	height: 30px;
	line-height: 1;
	font-size: 11px;
	font-weight: 700;
	padding: 7px 12px 5.5px 12px;
	border-radius: 999px;
	margin: 4px 0;
	background: var(--land, var(--accent));
	color: #1a160f;
}
.pill[hidden] { display: none; }   /* the class's display:inline-flex would otherwise override the hidden attr (empty green dot on land-less event spots) */
.sheet-pills { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }   /* event badge + land pill, side by side */

/* ===== Times ===== */
.times { display: flex; flex-wrap: wrap; gap: 5px; }

/* Digits/caps sit ~1.3px high in the line box — bias the padding to center them. */
.time {
	display: inline-flex;
	align-items: center;
	line-height: 1;
	font-size: 13px;
	font-weight: 400;
	padding: 7.3px 9px 4.7px;
	border-radius: 7px;
	background: var(--panel-2);
	border: 1px solid var(--line);
	color: var(--ink);
	white-space: nowrap;
}
/* Seasonal-event slots stay NEUTRAL (same as a regular time chip) on purpose: an event's theme is often
   green (OBB), which collided with the green "now/live" chip and made it hard to spot what's out now. In
   the event view every slot is an event slot anyway, so tinting them all added no signal. Reserving green
   for .live (which still overrides this) means the live chip is the only green — so it reads instantly. */
.time.event {
	border-color: var(--line);
	background: var(--panel-2);
}

.time.cancelled {
	text-decoration: line-through;
	color: var(--muted);
	opacity: .6;
	border-style: dashed;
	background: transparent;
}

.time.live { border-color: #36d399; color: var(--live-ink); background: var(--live-bg); }

.time.live::before {
	content: "";
	display: inline-block;
	width: 7px;
	height: 7px;
	border-radius: 50%;
	background: #36d399;
	margin-right: 6px;
	box-shadow: 0 0 0 0 rgba(54,211,153,.6);
	transform: translateY(-1.5px);   /* align the dot with the optically-centered text */
	animation: pulse 1.6s infinite;
}

@keyframes pulse {
	0% { box-shadow: 0 0 0 0 rgba(54,211,153,.5); }
	70% { box-shadow: 0 0 0 7px rgba(54,211,153,0); }
	100% { box-shadow: 0 0 0 0 rgba(54,211,153,0); }
}

/* ===== Empty state ===== */
.empty { text-align: center; color: var(--muted); padding: 60px 20px; }
.empty .big { font-size: 40px; margin-bottom: 10px; }

/* ===== Location details — bottom sheet (mobile) / centered modal (desktop) ===== */
.sheet-backdrop {
	position: fixed;
	inset: 0;
	z-index: 210;   /* above .map-mode (200) so a pin-opened location sheet stacks over the map */
	background: rgba(20,18,12,.45);
	-webkit-backdrop-filter: blur(2px);
	backdrop-filter: blur(2px);
	animation: fadeIn .2s ease;
}

.sheet-backdrop[hidden],
.sheet[hidden] { display: none; }

@keyframes fadeIn {
	from { opacity: 0; }
	to { opacity: 1; }
}

.sheet {
	position: fixed;
	z-index: 211;
	background: var(--panel);
	color: var(--ink);
	display: flex;
	flex-direction: column;
	overflow: hidden;
	left: 0;
	right: 0;
	bottom: 0;
	border-radius: 22px 22px 0 0;
	max-height: 88dvh;   /* dvh, not vh: respect the *visible* viewport so a tall drawer (e.g. the
	                        home character sheet) doesn't overshoot up under the mobile browser bar */
	box-shadow: 0 -8px 40px rgba(0,0,0,.3);
	animation: sheetUp .26s cubic-bezier(.2,.8,.2,1);
}

@keyframes sheetUp {
	from { transform: translateY(100%); }
	to { transform: translateY(0); }
}

@media (min-width: 560px) {
	.sheet {
		left: 50%;
		right: auto;
		bottom: auto;
		top: 50%;
		transform: translate(-50%,-50%);
		width: 420px;
		max-width: calc(100vw - 32px);
		border-radius: 20px;
		box-shadow: 0 20px 60px rgba(0,0,0,.35);
		animation: sheetIn .2s ease;
	}

	@keyframes sheetIn {
		from { opacity: 0; transform: translate(-50%,-46%); }
		to { opacity: 1; transform: translate(-50%,-50%); }
	}
}

/* Opened over the live map (tapped a pin, expanded from a preview, or a roster character), the
   sheet/drawer becomes a floating card on the live map — never a full-bleed drawer or a centered
   modal. Its backdrop neither dims nor intercepts, so the map stays clickable behind it. */
.sheet-backdrop.floating,
.char-backdrop.floating {
	background: transparent;
	-webkit-backdrop-filter: none;
	backdrop-filter: none;
	animation: none;
	pointer-events: none;
}
.char-backdrop.floating { z-index: 210; }
.char-sheet.floating { z-index: 211; }   /* above the map; same layer as the location card */

/* Phones: a short card pinned to the bottom (pan-y so a pinch zooms only the map, not the card). */
@media (max-width: 559px) {
	.sheet.floating,
	.char-sheet.floating {
		left: 12px;
		right: 12px;
		bottom: max(12px, env(safe-area-inset-bottom, 0px));
		top: auto;
		transform: none;
		width: auto;
		border-radius: var(--sheet-radius);
		max-height: 92dvh;           /* cap; the actual height is set per drag-detent in JS (~50vh default) */
		box-shadow: 0 14px 44px rgba(0,0,0,.4);
		touch-action: pan-y;
		animation: sheetUp .24s cubic-bezier(.2,.8,.2,1);
	}
	/* The character drawer uses the same drag detents as the location card (expanded ≈ 0.85), so its
	   cap matches — leaving the extra 92dvh headroom for the location card's taller content. */
	.char-sheet.floating { max-height: 85dvh; }
}
/* Desktop: a floating panel in the top-left (clear of the map's pan/zoom controls on the right).
   It's only as tall as its content, capped at the viewport with the body scrolling past that. */
@media (min-width: 560px) {
	.sheet.floating,
	.char-sheet.floating {
		left: 16px;
		right: auto;
		/* Sit below the top filter tabs (centered → they can overlap the left panel on narrow windows).
		   Mirrors the tabs' own --map-bar-y math inline, since that var lives on .map-mode, not here.
		   (+70/-86 track the lowered tabs — --map-bar-y's base +36px.) */
		top: calc(max(12px, env(safe-area-inset-top, 0px)) + 70px);
		bottom: auto;
		transform: none;
		width: 380px;
		max-width: calc(100vw - 32px);
		max-height: calc(100vh - max(12px, env(safe-area-inset-top, 0px)) - 86px);
		border-radius: 20px;
		box-shadow: 0 14px 44px rgba(0,0,0,.4);
		animation: panel-in .22s ease;
	}
	@keyframes panel-in { from { opacity: 0; transform: translateX(-24px); } to { opacity: 1; transform: translateX(0); } }
}

.sheet-close {
	position: absolute;
	top: 16px;
	right: 16px;
	z-index: 2;
	width: 34px;
	height: 34px;
	border-radius: 999px;
	border: none;
	cursor: pointer;
	background: rgba(0,0,0,.5);
	color: #fff;
	display: grid;
	place-items: center;
}

.sheet-close .svgicon { width: 20px; height: 20px; }   /* currentColor = #fff */

/* ===== Character details drawer ===== */
/* Sits one layer below the location sheet so tapping a spot stacks the photo
   drawer on top, then returns here on close. */
.char-backdrop { z-index: 98; }
.char-sheet { z-index: 99; }
/* Char sheet bottom-sheet (mobile, non-floating): platform-radius on its top corners. The
   floating form already uses --sheet-radius; the desktop centered modal keeps its own. */
@media (max-width: 559px) {
	.char-sheet:not(.floating) { border-radius: var(--sheet-radius) var(--sheet-radius) 0 0; }
}

.char-sheet-head {
	display: flex;
	align-items: center;   /* avatar centered with the name (+ its subtitle, when there is one) */
	gap: 12px;
	padding: 22px 18px 14px 18px;
	flex: none;
}
.char-sheet-head .avatar { width: 56px; height: 56px; }
/* Only the name needs to clear the close button; the subtext under it then gets the full width. */
.char-sheet-head .name { font-size: 20px; font-weight: 800; letter-spacing: -.3px; padding-right: 38px; }
/* The note as a subtitle under the name. Padding-above is scoped to the character card so the
   location roster's shared .char-subtext spacing is untouched. */
.char-sheet-head .char-subtext { margin: 8px 0 0; }

.char-sheet-locs {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
	-webkit-overflow-scrolling: touch;
	padding: 0 18px calc(18px + env(safe-area-inset-bottom, 0px));
}

.char-loc {
	border-top: 1px solid var(--line);
	padding: 14px 0;
	display: flex;
	align-items: flex-start;
	gap: 14px;
}
.char-loc:first-child { border-top: none; }
/* Left column: land pill, location name, then times — stacked. */
.char-loc-info {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 7px;
}
.char-loc-name { font-size: 15px; font-weight: 700; color: var(--ink); line-height: 1.25; }
.char-loc-name[data-loctap] { cursor: pointer; width: fit-content; }
@media (hover: hover) { .char-loc-name[data-loctap]:hover { color: var(--accent); } }
.char-loc-name[data-loctap]:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 6px; }

/* Square "where on the map" preview on the right — a static crop of the park map centered on
   the spot, with a pin marker and an expand cue. Tapping it opens the full map there. */
.loc-mini {
	position: relative;
	overflow: hidden;
	flex: none;
	width: 104px;
	height: 104px;
	border-radius: 14px;
	background: transparent;   /* blend with the surroundings when there's no map to show, so a spot with no
	                              coords/tiles reads as "nothing here" rather than an empty gray box */
	cursor: pointer;
}
/* Tile-mosaic preview (mapPreviewTiles): the layer origin sits at the box centre; the
   inline transform scales + shifts it so the spot's tile-pixel lands on the centre pin. Tiles are laid out
   at their global tile-pixel inside it. */
.loc-mini-tiles { position: absolute; left: 50%; top: 50%; }
.loc-mini-tile { position: absolute; user-select: none; -webkit-user-drag: none; }
.loc-mini-pin {
	position: absolute;
	left: 50%;
	top: 50%;
	width: 16px;
	height: 16px;
	margin: -8px 0 0 -8px;
	border-radius: 50%;
	background: var(--accent);
	border: 3px solid #fff;
	box-shadow: 0 1px 5px rgba(0,0,0,.45);
}
.loc-mini-expand {
	position: absolute;
	right: 6px;
	bottom: 6px;
	width: 26px;
	height: 26px;
	border-radius: 8px;
	display: grid;
	place-items: center;
	background: color-mix(in srgb, var(--panel) 82%, transparent);
	-webkit-backdrop-filter: blur(6px);
	backdrop-filter: blur(6px);
}
.loc-mini-expand .svgicon { width: 14px; height: 14px; background-color: var(--ink); }
.loc-mini:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) {
	.loc-mini:hover .loc-mini-expand { background: var(--panel); }
}

/* Character rows in the land view are tappable (open the character drawer). */
.char-row { cursor: pointer; }
.char-row .avatar, .char-row .name { cursor: pointer; }
.char-row:active .name { opacity: .6; }

/* Swipeable photo gallery: the area photo, plus an optional "you-are-here" map shot. */
.sheet-media { position: relative; }
/* Drag handle for the floating card's height detents — shown only when the card can expand
   (>3 characters). Sits over the top of the photo. */
.sheet-grip { display: none; }
@media (max-width: 559px) {
	.sheet.floating.expandable .sheet-grip {
		display: block;
		position: absolute;
		top: 8px;
		left: 50%;
		transform: translateX(-50%);
		width: 38px;
		height: 4px;
		border-radius: 999px;
		background: rgba(217, 217, 217, 0.8);
		box-shadow: 0 1px 3px rgba(0,0,0,.35);
		z-index: 3;
	}
	/* Vertical drags anywhere on the top of the card move the sheet; the photo still pans
	   horizontally (carousel), the roster/body still scrolls vertically. Covers the location card
	   and the character drawer (its head + spot list). */
	.sheet.floating .sheet-grip,
	.sheet.floating .sheet-head,
	.sheet.floating .char-sheet-head,
	.sheet.floating .sheet-dots { touch-action: none; }
	.sheet.floating .sheet-gallery { touch-action: pan-x; }
	.sheet.floating .sheet-body,
	.sheet.floating .char-sheet-locs { touch-action: pan-y; }
}
/* Fixed-height photo banner (image cropped with object-fit: cover). No expand/collapse. */
.sheet-gallery {
	display: flex;
	width: 100%;
	height: 150px;
	background: var(--placeholder);
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
}
.sheet-gallery::-webkit-scrollbar { display: none; }
.sheet-slide {
	position: relative;
	flex: 0 0 100%;
	height: 100%;
	scroll-snap-align: start;
}
.sheet-slide img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Prev/next arrows — for mouse devices (touch swipes instead). */
.sheet-nav {
	position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
	width: 34px; height: 34px; border-radius: 999px; border: none; cursor: pointer;
	background: rgba(0,0,0,.45); color: #fff;
	display: grid; place-items: center;
	-webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
	transition: opacity .15s;
}
.sheet-nav svg { width: 20px; height: 20px; display: block; }
.sheet-prev { left: 10px; }
.sheet-next { right: 10px; }
.sheet-nav[hidden] { display: none; }
.sheet-nav.disabled { opacity: .35; pointer-events: none; }
@media (hover: none) { .sheet-nav { display: none !important; } }   /* touch: swipe, no arrows */

.sheet-dots { display: flex; justify-content: center; gap: 0px; padding: 6px 0 0; }
.sheet-dots:empty { display: none; }
/* Clickable: an 18px hit area around a 6px dot drawn with ::before. */
.sheet-dot {
	width: 14px; height: 18px; padding: 0; border: none; background: none; cursor: pointer;
	display: grid; place-items: center;
}
.sheet-dot::before {
	content: ""; width: 6px; height: 6px; border-radius: 50%;
	background: var(--line); transition: background .15s;
}
.sheet-dot.on::before { background: var(--muted); }

.sheet-body {
	padding: 0 18px 24px;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 10px;
}

/* The land + location name are a fixed bar ABOVE the scroll area (a sibling of it, not
   inside it) — so there's no position:sticky lag on momentum scroll, and the roster can
   never peek through a sliver below the dots. position/z-index keep its shadow over the
   roster. */
.sheet-head {
	flex: none;
	position: relative;
	z-index: 1;
	padding: 16px 18px 12px;
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: 10px;
	background: var(--panel);
}

/* Location name on the left, the map link on the right, both vertically centered. */
.sheet-head-row {
	display: flex;
	align-items: center;
	gap: 8px;                 /* title, then the share circle hugging it; the map link floats right on its own */
	width: 100%;
}
.sheet-head h2 { margin: 0; font-size: 20px; font-weight: 800; letter-spacing: -.3px; }
.sheet-head-row h2 { flex: 1 1 auto; min-width: 0; }   /* fill the row so the inline share circle stays on the title's last line; the map link floats right */
.sheet-head-row h2 .title-tail { white-space: nowrap; }   /* keep the last word + share icon together — the word wraps down with the icon, never the icon alone */

.sheet-map {
	flex: none;
	margin-left: auto;                 /* pushed to the far right; the share circle stays next to the title */
	display: inline-flex;
	align-items: center;
	gap: 6px;
	text-decoration: none;
	background: var(--ink);
	color: var(--bg);
	font-weight: 600;
	font-size: 12px;
	padding: 9px 13px;
	border-radius: 999px;
	white-space: nowrap;
}

.sheet-map .svgicon { width: 16px; height: 16px; transform: translateY(-1.5px); }   /* currentColor = var(--bg) */

/* Share-this-location action — a small gray circle with the link icon, flowing inline within the title
   heading so it sits right after the last word (even when the name wraps). Copies the ?open= link. */
.sheet-share {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	vertical-align: middle;            /* center on the title's text line */
	transform: translateY(-2px);       /* nudge up to optically center against the caps */
	margin-left: 8px;
	width: 28px;
	height: 28px;
	padding: 0;
	background: var(--panel-2);        /* same gray as the time chips */
	border: none;
	border-radius: 50%;
	cursor: pointer;
	color: var(--muted);
	-webkit-tap-highlight-color: transparent;
}
.sheet-share svg { transform: translateY(-1px); }   /* optically center the link glyph against the caps */
.sheet-share:active { opacity: .6; }
.sheet-share[hidden] { display: none; }

/* Imageless cards: the media area is hidden, so the header sits at the top of the card and the
   close button (absolute, top-right) lands over it. Soften the X — its dark translucent style is
   meant for photos; on the white header a light-gray circle reads better (like Apple/Google Maps). */
.sheet.no-media .sheet-close { background: var(--panel-2); color: var(--muted); }

/* Let a long character roster scroll inside the sheet instead of overflowing it. Rows
   dissolve under the sticky header (.sheet-head + its ::after fade) as they scroll. */
.sheet-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
}

/* ===== Location character roster (in the details sheet) ===== */
.sheet-chars { display: flex; flex-direction: column; width: 100%; }
.sheet-chars:empty { display: none; }
.sheet-char { display: flex; align-items: flex-start; gap: 11px; padding: 10px 0; }
.sheet-char[data-char] { cursor: pointer; }
@media (hover: hover) { .sheet-char[data-char]:hover .name { color: var(--accent); } }
.sheet-char + .sheet-char { border-top: 1px solid var(--line); }
/* Nudge the (top-aligned) avatar down a touch so a single name + one row of times reads as
   vertically centered, while taller rows keep the avatar near the top. */
.sheet-char .avatar { width: 42px; height: 42px; font-size: 15px; margin-top: 4px; }
.sheet-char-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
.sheet-char .name { font-size: 15px; }

/* ===== Stale-data banner ===== */
/* Floats over the board when a newer day's schedule is published while the tab
   was open. Bottom on mobile (thumb reach), top on desktop. */
.stale-banner {
	position: fixed;
	left: 50%;
	z-index: 90;
	display: flex;
	align-items: center;
	gap: 12px;
	width: max-content;
	max-width: calc(100vw - 24px);
	padding: 10px 10px 10px 18px;
	background: var(--panel-2);
	color: var(--ink);
	border: 1px solid var(--line);
	border-radius: 999px;
	box-shadow: var(--shadow);
}
.stale-banner[hidden] { display: none; }

/* Pinned to the bottom on all sizes, clear of the home indicator on mobile. */
.stale-banner {
	bottom: calc(14px + env(safe-area-inset-bottom, 0px));
	top: auto;
	transform: translateX(-50%) translateY(0);
	animation: staleIn-bottom .28s cubic-bezier(.2,.8,.2,1);
}
@keyframes staleIn-bottom {
	from { opacity: 0; transform: translateX(-50%) translateY(16px); }
	to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

.stale-text { font-size: 14px; white-space: nowrap; }

.stale-refresh {
	flex: none;
	background: var(--ink);
	color: var(--bg);
	border: none;
	border-radius: 999px;
	padding: 9px 16px;
	font: inherit;
	font-weight: 600;
	font-size: 14px;
	cursor: pointer;
}
@media (hover: hover) {
	.stale-refresh:hover { opacity: .9; }
}
.stale-refresh:active { transform: scale(.97); }
.sheet-map[hidden] { display: none; }

/* ===== Park map — shared chrome (the tile map overlay reuses these classes; see the tile-map section) ===== */
/* Closing a floating card fades it out. */
.sheet.closing { animation: map-fade-out .2s ease forwards; }
@keyframes map-fade-out { from { opacity: 1; } to { opacity: 0; } }

/* Standalone close of the location card (✕, backdrop tap, drag-dismiss) — reverses its entry.
   Kept on its own class so it never collides with the map-collapse's `.closing` fade above. */
.sheet.sheet-out { animation: map-fade-out .2s ease forwards; }   /* floating card / desktop modal: fade */
@media (max-width: 559px) {
	/* The bottom drawer slides back down the way it came in — snappy start (matches sheetUp's curve) so
	   the close never feels slower than the open. */
	.sheet.sheet-out:not(.floating) { animation: sheetDown .22s cubic-bezier(.2,.8,.2,1) forwards; }
}
@keyframes sheetDown { from { transform: translateY(0); } to { transform: translateY(100%); } }
.sheet-backdrop.sheet-out { animation: fadeOut .2s ease forwards; }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
	.sheet.sheet-out, .sheet.sheet-out:not(.floating), .sheet-backdrop.sheet-out { animation: none; opacity: 0; }
}

.map-close {
	position: absolute;
	top: var(--map-bar-y);
	transform: translateY(-50%);
	right: 12px;
	z-index: 2;
	width: 38px;
	height: 38px;
	border-radius: 999px;
	border: none;
	cursor: pointer;
	background: rgba(0,0,0,.5);
	color: #fff;
	display: grid;
	place-items: center;
}
.map-close .svgicon { width: 20px; height: 20px; }   /* currentColor = #fff */

/* ===== "Find my location" button + "you are here" dot ===== */
/* Top-left, mirroring the close "X" and vertically centered with the filter tabs.
   Always visible (the d-pad is desktop-only; this matters most on a phone in the park). */
/* Frosted-glass background like the pan/zoom controls (distinct from the dark close "X"). */
.map-locate {
	position: absolute;
	top: var(--map-bar-y);
	transform: translateY(-50%);
	left: 12px;
	z-index: 2;
	width: 38px;
	height: 38px;
	border-radius: 999px;
	border: 1px solid var(--line);
	cursor: pointer;
	/* Near-opaque fill, not backdrop-filter — same iOS bleed reason as .map-filter. */
	background: color-mix(in srgb, var(--panel) 95%, transparent);
	box-shadow: var(--shadow);
	display: grid;
	place-items: center;
}
/* Icon color (currentColor via the svg mask): blue when usable, gray when not. */
.map-locate .svgicon { width: 20px; height: 20px; }
.map-locate, .map-locate.located { color: var(--accent); }   /* enabled / tracking → blue */
.map-locate.unavailable { color: var(--muted); }             /* blocked / off / denied → gray */
.map-locate.locating { pointer-events: none; }   /* ignore taps while waiting (no spinner) */
@media (hover: hover) { .map-locate:hover { background: color-mix(in srgb, var(--panel) 90%, transparent); } }

.map-me {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 4;
	width: 20px;
	height: 20px;
	border-radius: 50%;
	background: var(--accent);
	border: 4px solid #fff;
	/* box-shadow: 0 0 0 1.5px rgba(0,0,0,.25), 0 2px 6px rgba(0,0,0,.45); */
	pointer-events: none;
	will-change: transform;
}
.map-me::before {   /* soft locating pulse so the dot reads as "live" */
	content: "";
	position: absolute;
	inset: -7px;
	border-radius: 50%;
	background: var(--accent);
	opacity: .3;
	animation: map-me-pulse 2s ease-out infinite;
}
@keyframes map-me-pulse {
	from { transform: scale(.5); opacity: .45; }
	to   { transform: scale(1.7); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .map-me::before { animation: none; } }

.map-toast {
	position: absolute;
	left: 50%;
	bottom: max(74px, calc(env(safe-area-inset-bottom, 0px) + 70px));
	transform: translateX(-50%);
	z-index: 5;
	max-width: calc(100% - 32px);
	width: max-content;
	padding: 9px 15px;
	border-radius: 999px;
	background: rgba(0,0,0,.8);
	color: #fff;
	font-size: 11px;
	font-weight: 400;
	line-height: 1.35;
	text-align: center;
	box-shadow: 0 6px 22px rgba(0,0,0,.4);
	opacity: 1;
	transition: opacity .35s ease;
}
.map-toast.hiding { opacity: 0; }   /* fade before it's removed */
/* Global variant (body-level, not inside the map overlay) so it shows over the board/drawer too, above
   the sheet. Keeps the .map-toast look; just switches to viewport-fixed and a top z-index. */
.app-toast { position: fixed; bottom: max(24px, calc(env(safe-area-inset-bottom, 0px) + 18px)); font-size: 13px; z-index: 2147483000; }   /* standard bottom snackbar at the app's 13px body size (the map toast's 74px/11px sits mid-card and reads small over the drawer on mobile) */

/* ===== Desktop pan + zoom controls ===== */
/* Hidden by default; shown only on devices with a mouse (touch users pinch/drag). */
.map-ctrls {
	position: absolute;
	right: 16px;
	top: 50%;
	transform: translateY(-50%);
	z-index: 2;
	display: none;
	flex-direction: column;
	align-items: center;
	gap: 12px;
}
@media (hover: hover) and (pointer: fine) {
	.map-ctrls { display: flex; }
}
.map-dpad, .map-zoom {
	background: color-mix(in srgb, var(--panel) 78%, transparent);
	border: 1px solid var(--line);
	box-shadow: var(--shadow);
	-webkit-backdrop-filter: blur(12px);
	backdrop-filter: blur(12px);
	overflow: hidden;
}
.map-dpad {
	display: grid;
	grid-template-columns: repeat(3, 34px);
	grid-template-rows: repeat(3, 34px);
	border-radius: 16px;
}
.map-dpad .up { grid-area: 1 / 2; }
.map-dpad .left { grid-area: 2 / 1; }
.map-dpad .right { grid-area: 2 / 3; }
.map-dpad .down { grid-area: 3 / 2; }
.map-zoom {
	display: flex;
	flex-direction: column;
	border-radius: 14px;
}
.map-zoom .map-ctrl + .map-ctrl { border-top: 1px solid var(--line); }
.map-ctrl {
	width: 34px;
	height: 34px;
	border: none;
	background: none;
	cursor: pointer;
	color: var(--ink);
	display: grid;
	place-items: center;
	padding: 0;
}
.map-zoom .map-ctrl { width: 40px; height: 40px; }
.map-ctrl svg {
	width: 19px;
	height: 19px;
	display: block;
	fill: none;
	stroke: currentColor;
	stroke-width: 2.4;
	stroke-linecap: round;
	stroke-linejoin: round;
}
@media (hover: hover) {
	.map-ctrl:hover { background: color-mix(in srgb, var(--ink) 9%, transparent); }
}
.map-ctrl:active { background: color-mix(in srgb, var(--ink) 15%, transparent); }

.map-pin {
	position: absolute;
	top: 0;
	left: 0;
	padding: 0;
	border: none;
	background: none;
	cursor: pointer;
	pointer-events: auto;
	will-change: transform;
	/* JS sets transform: translate(<tipX> - 50%, <tipY> - 100%) so the point sits at the tip. */
}
/* Brief yellow ring when a pin is opened via "View on map", so it's obvious which one. */
.map-pin.flash::before {
	content: "";
	position: absolute;
	inset: -5px;
	border-radius: 999px;
	border: 3px solid #ffd400;
	box-shadow: 0 0 9px 2px rgba(255,212,0,.6);
	pointer-events: none;
	z-index: 4;
	animation: pin-flash 1.3s ease-out forwards;
}
@keyframes pin-flash {
	0% { opacity: 0; }
	40% { opacity: 1; }
	65% { opacity: 1; }
	100% { opacity: 0; }
}

.map-pin.route-hl { z-index: 4; }

/* Route card body (roaming show): one quiet timeline through the stops, showtimes anchored to endpoints.
   Minimal by design — small solid dots (no rings/strokes), a hairline rail, waypoints muted so only the
   two endpoints (bold, tinted dot, time chip) carry weight. */
.route-card { --route: var(--accent); }
.route-note { margin: 0 0 14px; color: var(--muted); font-size: 13px; line-height: 1.45; }
.route-times { margin-bottom: 18px; }   /* showtime chips, just under the title */
.route-dir { width: 13px; height: 13px; margin-left: 4px; transform: translateY(-1.5px); }   /* ↑/↓ arrow icon (currentColor = chip text); nudge up to sit level with the high-riding chip text */
.route-timeline { list-style: none; margin: 0; padding: 0; }
.route-stop { position: relative; display: flex; align-items: center; gap: 10px; min-height: 36px; padding-left: 22px; }
.route-stop[data-routestop] { cursor: pointer; transition: opacity .12s ease; }   /* tappable → pans the map to this stop */
.route-stop[data-routestop]:active { opacity: .5; }
@media (hover: hover) { .route-stop[data-routestop]:hover { opacity: .6; } }
.route-stop .name { flex: 1; min-width: 0; font-size: 15px; font-weight: 400; color: var(--muted); }   /* waypoints: quiet */
.route-stop.is-end .name { color: var(--ink); font-weight: 700; }                                       /* endpoints: emphasized */
/* Hairline rail behind the dots; endpoints trim it so it only points inward. */
.route-stop::after { content: ""; position: absolute; left: 4px; top: 0; bottom: 0; width: 1px; background: var(--line); }
.route-stop:first-child::after { top: 50%; }
.route-stop:last-child::after { bottom: 50%; }
/* Small solid stop dot, centered on the row — no border/halo. Endpoints tinted + a touch larger. */
.route-stop::before { content: ""; position: absolute; left: 2px; top: 50%; transform: translateY(-50%); z-index: 1; width: 5px; height: 5px; border-radius: 999px; background: var(--placeholder); }
.route-stop.is-end::before { left: 1px; width: 7px; height: 7px; background: var(--route); }

.map-spot {
	position: absolute;
	top: 0;
	left: 0;
	width: 17px;
	height: 17px;
	border-radius: 999px;
	background: var(--land, var(--accent));
	border: 3px solid #fff;
	box-shadow: 0 2px 6px rgba(0,0,0,.4);
	will-change: transform;
	animation: spot-pop .2s ease-out both;
}
@keyframes spot-pop { from { opacity: 0; } to { opacity: 1; } }
/* Closing the location card fades its alternate-spot dots out (parent opacity fades the ::before flash too). */
.map-spot.spot-out { animation: spot-fade .2s ease-out forwards; }
@keyframes spot-fade { from { opacity: 1; } to { opacity: 0; } }
/* Same yellow flash the main pin gets, so alternate spots are obvious when selected. */
.map-spot::before {
	content: "";
	position: absolute;
	inset: -5px;
	border-radius: 999px;
	border: 3px solid #ffd400;
	box-shadow: 0 0 9px 2px rgba(255,212,0,.6);
	pointer-events: none;
	animation: pin-flash 1.3s ease-out forwards;
}

.map-pin-stack { display: flex; }
.map-pin-avatar {
	position: relative;
	width: 34px;
	height: 34px;
	flex: none;
	border-radius: 50%;
	overflow: hidden;
	background: var(--placeholder);
	border: 2.5px solid #fff;
	box-shadow: 0 2px 6px rgba(0,0,0,.4);
	font-size: 12px;
	font-weight: 700;
	color: var(--muted);
	display: grid;
	place-items: center;
	transition: width .16s ease, height .16s ease;
}
.map-pin-avatar img {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	object-fit: cover;
	user-select: none;
	-webkit-user-drag: none;   /* don't let a pin avatar be dragged off as an image (desktop) */
}
/* Special/event characters recolor the single avatar ring (not a second ring outside the white one —
   that stacked read as a double border). The base drop shadow still separates them from the map. */
.map-pin-avatar.special { border-color: #f5b53d; }               /* gold for limited-time Specials */
.map-pin-avatar.event { border-color: var(--evt-bg, #96C940); }  /* the event's theme color */
/* Collapsed (low zoom): only the first avatar shows. Zoomed in (.stacked): up to three
   fan out, overlapping, leftmost on top. The transition makes the fan-out feel smooth. */
/* Collapsed by default: the extra two avatars are width 0 until the pin fans out (see the tile map's
   .tm-stacked / .tm-max rules, which drive the fan-out at zoom). */
.map-pin-avatar:not(:first-child) { width: 0; border-width: 0; margin-left: 0; opacity: 0; transition: width .16s ease, height .16s ease, margin .16s ease, opacity .16s ease; }
.map-pin-avatar:nth-child(1) { z-index: 3; }
.map-pin-avatar:nth-child(2) { z-index: 2; }
.map-pin-avatar:nth-child(3) { z-index: 1; }
/* Large pins on desktop. */
@media (min-width: 560px) {
	.map-pin-avatar { width: 64px; height: 65px; }
}
/* Upper-right corner of the last avatar in the stack. z-index clears the avatars (which
   carry their own z-index for the fan-out overlap) so it's never tucked behind an image. */
.map-pin-count {
	position: absolute;
	top: -6px;
	right: -6px;
	z-index: 5;
	min-width: 16px;
	height: 16px;
	padding: 0 4px;
	border-radius: 999px;
	background: var(--accent);
	color: #fff;
	font-size: 10px;
	font-weight: 700;
	line-height: 16px;
	text-align: center;
	box-shadow: 0 1px 3px rgba(0,0,0,.35);
}
/* Desktop: the collapsed (zoomed-out) avatar is bigger (64px), so the box-corner offset floats the
   badge off the circle. Pull it onto the circle's upper-right and scale it up a touch. */
@media (min-width: 560px) {
	.map-pin-count { top: 0; right: 0; min-width: 20px; height: 20px; font-size: 12px; line-height: 20px; }
}
/* Two badges live in the DOM; the zoom state picks which is shown (no rebuild on zoom) — see the tile
   map's .tm-stacked rules that swap them. */
.stacked-count { display: none; }

/* ===== Map time filter (Now / Upcoming / All) ===== */
/* Pinned top-center (clear of the close button at top-right), mirroring the home
   toolbar's Now/Upcoming chips with the same icons. */
/* Seasonal-event banner under the map filter bar (shown while an event filter is active). */
.map-event-banner {
	position: absolute;
	left: 50%;
	top: calc(var(--map-bar-y) + 26px);
	transform: translateX(-50%);
	z-index: 2;
	display: inline-flex;
	align-items: center;
	gap: 7px;
	max-width: calc(100% - 24px);
	padding: 7px 14px 5px;   /* bias down 1px — Epilogue rides high in its line box */
	border-radius: 999px;
	background: var(--evt-bg);
	color: var(--evt-ink);
	box-shadow: var(--shadow);
	white-space: nowrap;
}
.map-event-banner[hidden] { display: none; }
.meb-icon { flex: none; width: 15px; height: 15px; background-color: var(--evt-ink); }
.meb-emoji { font-size: 14px; line-height: 1; }
/* Match the filter selectors' text: same size + weight, uniform across name / separator / date. */
.meb-name, .meb-when, .meb-sep { font-size: 12px; font-weight: 400; }
.meb-sep { opacity: .55; }

.map-filter {
	position: absolute;
	left: 50%;
	top: var(--map-bar-y);
	transform: translate(-50%, -50%);
	z-index: 2;
	display: flex;
	gap: 2px;
	padding: 4px;
	border-radius: 999px;
	/* Near-opaque fill, NOT backdrop-filter: this bar is centered with transform: translate, and a
	   transformed element with backdrop-filter bleeds its blur into a full-width band on iOS. A solid-ish
	   fill reads the same as a control and can't bleed. (Separate from the iOS 27 status-bar blur, which
	   #sb-fill handles.) */
	background: color-mix(in srgb, var(--panel) 95%, transparent);
	border: 1px solid var(--line);
	box-shadow: var(--shadow);
}
/* Match the home filter chips: same size/weight, same icon treatment (asymmetric padding
   centers the high-riding Epilogue text; the icon nudges up to meet it). */
.map-filter button {
	display: inline-flex;
	align-items: center;
	gap: 5px;
	border: none;
	background: none;
	cursor: pointer;
	color: var(--muted);
	font-family: inherit;
	font-size: 12px;
	font-weight: 400;
	line-height: 1;
	padding: 10px 11px 8px 11px;
	border-radius: 999px;
	white-space: nowrap;
}
.map-filter .svgicon { width: 16px; height: 16px; transform: translateY(-1.7px); }
.map-filter .now-ic { background-color: #5cb672; }   /* "Now" keeps its live-green, like home */
.map-filter .star-ic { background-color: #f5b53d; }  /* "Special" keeps its gold, like home */
/* A seasonal-event chip (added by renderMapEventFilters) widens the bar past the phone edge, so when
   one is present (.has-event) collapse Now/Later to text-only to buy back that width. */
.map-filter.has-event button[data-filter="now"] .svgicon,
.map-filter.has-event button[data-filter="upcoming"] .svgicon { display: none; }
/* Icon-only tabs (Special, seasonal events): a perfect circle (aspect-ratio 1 guards against ovals),
   balanced padding, no text-baseline nudge on the icon. */
.map-filter .mf-icon { padding: 9px; aspect-ratio: 1; justify-content: center; }
.map-filter .mf-icon .svgicon { transform: none; }
.map-filter button[aria-selected="true"] { background: var(--ink); color: var(--bg); }
/* Seasonal-event filter (pumpkin, …): its own tint when unselected, themed pill when active. */
.map-filter .mf-event .svgicon { background-color: var(--evt-ic, var(--muted)); }
.map-filter .mf-event[aria-selected="true"] { background: var(--evt-bg); color: var(--evt-ink); }
.map-filter .mf-event[aria-selected="true"] .svgicon { background-color: var(--evt-ink); }

/* Lock the page behind the overlay so it doesn't scroll under the map. */
body.tile-open { overflow: hidden; }
/* iOS 26/27 standalone status-bar fix: a 1px solid bar pinned to the very top gives iOS a solid color to
   paint the status bar with (beige = var(--bg)), instead of blurring the content beneath it into a band —
   the full-screen map's image background was triggering that blur. See the matching note in index.html.
   Sits above the map overlay (z 200) so it's the top-most element iOS samples; invisible otherwise.
   TODO(ios27-statusbar): a solid status-bar strip only to dodge the iOS 27 beta PWA top-edge blur (the OS
   "Liquid Glass" effect — confirmed because Reduce Transparency removes it). Once Apple ships a fix / opt-out,
   RESTORE THE ORIGINAL by (1) deleting this #sb-fill rule, (2) deleting the <div id="sb-fill"> in index.html,
   and (3) resetting --map-bar-y below from "+ 36px" back to "+ 21px" (the filter bar was nudged down to sit
   clear of the blur zone). Nothing else references any of these. */
#sb-fill {
	position: fixed; top: 0; left: 0; width: 100%;
	height: max(1px, env(safe-area-inset-top, 0px));   /* fills the status-bar/safe area in standalone; 1px elsewhere */
	background: var(--bg); z-index: 250; pointer-events: none;
}

/* ===== "Add to home screen" install banner (mobile) ===== */
.install-banner {
	position: fixed; left: 12px; right: 12px; z-index: 60;
	bottom: calc(12px + env(safe-area-inset-bottom));
	display: flex; align-items: center; gap: 8px;
	padding: 14px 16px;
	background: var(--bg);
	color: var(--ink);
	border: 1px solid rgba(255,255,255,.08);
	border-radius: 18px;
	box-shadow: 0 10px 34px rgba(0,0,0,.4);
	animation: install-up .35s ease;
}
.install-banner[hidden] { display: none; }
@keyframes install-up { from { transform: translateY(140%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .install-banner { animation: none; } }
.install-icon { width: 50px; height: 50px; flex: none; border-radius: 12px; border-style: solid; border-width: 1.5px; border-color: var(--line);}
.install-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; line-height: 1.4; }
.install-title { font-size: 15px; font-weight: 700; }
/* Subtext: one uniform style — no mixed weights/sizes/colors. */
.install-hint { font-size: 13px; font-weight: 400; color: var(--muted); line-height: 1.2 }
.install-add {
	flex: none; background: var(--ink); color: var(--bg); border: none;
	border-radius: 999px; padding: 9px 16px; font-weight: 600; font-size: 13px; cursor: pointer;
}
.install-close {
	flex: none; background: none; border: none; color: var(--ink);
	font-size: 17px; line-height: 1; padding: 6px 8px; cursor: pointer; border-radius: 8px;
}
@media (hover: hover) { .install-close:hover { color: #fff; background: rgba(255,255,255,.12); } }
/* Keep the footer clear of the floating card while it's up. */
body.has-install-banner { padding-bottom: 100px; }

/* ===== Pull-to-refresh (installed/standalone only — browsers provide their own) ===== */
@media (display-mode: standalone) { body { overscroll-behavior-y: contain; } }
.ptr {
	position: fixed; top: -46px; left: 50%;
	transform: translateX(-50%);
	z-index: 80;
	width: 34px; height: 34px;
	display: flex; align-items: center; justify-content: center;
	border-radius: 50%;
	background: var(--panel);
	box-shadow: var(--shadow);
	opacity: 0;
	pointer-events: none;
}
.ptr-spinner {
	width: 20px; height: 20px;
	border: 3px solid color-mix(in srgb, var(--ink) 18%, transparent);   /* faint track ring */
	border-top-color: var(--accent);                                     /* one bright arc */
	border-radius: 50%;
}
/* "Release to refresh" cue — only before it starts spinning, so the spinning arc stays visible. */
.ptr.ready:not(.spinning) .ptr-spinner { border-color: var(--accent); }
.ptr.spinning .ptr-spinner {
	border-color: color-mix(in srgb, var(--ink) 18%, transparent);
	border-top-color: var(--accent);
	animation: ptr-spin .7s linear infinite;
}
@keyframes ptr-spin { to { transform: rotate(360deg); } }

/* ===== Seasonal event banner + lineup page (Oogie Boogie Bash, ...) ===== */
/* Each banner is themed via --evt-bg / --evt-ink set inline from the event's config color. */
/* ===== Seasonal-event banner (home) — a quiet pill sized to the filter chips ===== */
.event-banners { max-width: var(--content-max); margin: 0 auto; padding: 0 var(--gutter); }
.event-banner {
	display: flex; align-items: center; gap: 9px; width: 100%;
	margin: 10px 0 2px; padding: 7px 12px 7px 7px; border: none; border-radius: 14px; cursor: pointer;   /* rounded rect (not a pill) so it reads with the squared date chip + event-page cards */
	background: var(--panel); color: var(--ink); text-align: left; line-height: 1;
	box-shadow: 0 1px 2px rgba(40,38,30,.07); transition: transform .1s ease;
}
.event-banner:active { transform: scale(.99); }
.evt-b-badge {
	flex: none; width: 26px; height: 26px; border-radius: 50%; position: relative; overflow: hidden;
	display: grid; place-items: center; font-size: 15px; line-height: 1;
	background: var(--evt-bg); color: var(--evt-ink);
}
.evt-b-name { font-family: "Epilogue", sans-serif; font-weight: 700; font-size: 13px; flex: 1; min-width: 0;
	overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Event badge = the event's masked icon (tinted with the theme ink), or the emoji fallback. */
.evt-badge-emoji { line-height: 1; }
.evt-badge-ic { background-color: var(--evt-ink); }
.evt-b-badge .evt-badge-ic { width: 17px; height: 17px; }   /* home banner (26px circle) */
.evt-badge-lg .evt-badge-ic { width: 34px; height: 34px; }  /* event page header (56px circle) */

/* ===== Event page — the site's cream bg + white cards, not a themed full-bleed ===== */
.event-page {
	position: fixed; inset: 0; z-index: 70; display: flex; flex-direction: column;
	background: var(--bg);
	/* Opaque "present modally" cover (à la iOS / App Store product page): the page is fully opaque and
	   SLIDES up over the board — it never fades. Fading opacity here let the board show through mid-
	   transition, so both screens' content ghosted at once. .open (added a frame after unhide) slides it
	   in; removing it slides out, then JS re-hides on transitionend (see hideEventPage). */
	transform: translateY(100%);
	transition: transform .34s cubic-bezier(.32,.72,0,1);
	will-change: transform;
}
.event-page.open { transform: translateY(0); }
.event-page[hidden] { display: none; }   /* the class's display:flex would otherwise beat the `hidden` attr */

@media (min-width: 560px) {
	/* Desktop/web: no animation. A bottom-edge slide is a touch idiom that feels wrong on a wide screen,
	   and every fade/scale/scrim variant we tried read as off. The opaque panel (its background matches
	   the board exactly) just appears/disappears instantly — seamless, no ghosting or flash. The
	   slide-up stays on mobile, where it feels native. */
	.event-page { transform: none; transition: none; }
}

/* Reduced motion: no slide — swap instantly (still opaque, so no cross-fade either). */
@media (prefers-reduced-motion: reduce) { .event-page { transition: none; } }
body.event-open { overflow: hidden; }
.event-page-head {
	position: relative; z-index: 2; flex: none; background: var(--bg);
	/* +safe-area-inset-top: the event page is a full-screen (inset:0) cover, so in edge-to-edge mode its header must
	   clear the status bar / island too. */
	padding: calc(20px + env(safe-area-inset-top, 0px)) var(--gutter) 14px;
	max-width: var(--content-max); margin: 0 auto; width: 100%;
}
.event-close {
	position: absolute; top: calc(16px + env(safe-area-inset-top, 0px)); right: var(--gutter); width: 34px; height: 34px; border: none; border-radius: 50%;
	background: var(--panel-2); color: var(--muted); cursor: pointer;
	display: flex; align-items: center; justify-content: center;
}
.event-close .svgicon { width: 16px; height: 16px; }
/* Header: the (larger) event image on the left, title + one details line stacked on the right. */
.evt-head-row { display: flex; align-items: center; gap: 14px; padding-right: 44px; }
.evt-badge-lg { flex: none; width: 56px; height: 56px; border-radius: 50%; position: relative; overflow: hidden;
	display: grid; place-items: center; font-size: 30px; line-height: 1; background: var(--evt-bg); color: var(--evt-ink); }
.evt-head-text { flex: 1; min-width: 0; }
.evt-title { font-family: "Epilogue", sans-serif; font-weight: 800; font-size: 23px; letter-spacing: -.4px; color: var(--ink); }
.evt-head-details { margin-top: 2px; font-size: 13px; color: var(--muted);
	display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.evt-sep { color: var(--muted); opacity: .55; }
/* "Tonight" / next-date badge — same all-caps chip style as the site's Special/OBB badges. */
.evt-next { display: inline-flex; align-items: center; line-height: 1; font-size: 11px; font-weight: 600;
	letter-spacing: .4px; text-transform: uppercase; border-radius: 6px; padding: 4px 7px 4px 7px;
	color: var(--muted); background: var(--panel-2); }
.evt-next.on { color: var(--evt-ink); background: var(--evt-bg); }
/* No top padding: it would sit between the head and the sticky map (which sticks at top:0), leaving a
   gap where scrolled content bleeds through. The initial top gap lives on the first child instead. */
.event-page-body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; position: relative;   /* offsetParent for scrollEventTop's sticky-top measurement */
	overscroll-behavior: contain;   /* keep scroll within the drawer so hitting an edge can't trigger the browser's pull-to-refresh */
	padding: 0 var(--gutter) 48px; max-width: var(--content-max); margin: 0 auto; width: 100%;
	scrollbar-width: none; }   /* the centered body's scrollbar floats mid-page on desktop — hide it */
.event-page-body::-webkit-scrollbar { display: none; }
.event-page-body > :first-child { margin-top: 14px; }

/* Cards + section headers (headers sit OUTSIDE the cards, like the board) */
.evt-card { background: var(--panel); border-radius: 16px; padding: 16px 18px; margin-bottom: 12px;
	box-shadow: 0 1px 3px rgba(40,38,30,.06); }
.evt-section-title { font-family: "Epilogue", sans-serif; font-weight: 700; font-size: 13px; letter-spacing: .2px;
	color: var(--ink); }
.evt-block-title { margin: 30px 2px 10px; }   /* generous space before each section (Activities, Characters) */

/* Event-nights banner → opens the calendar drawer */
/* The event-page "Event nights" banner reuses the home .event-banner look (above) so the two match.
   Only the event-page spacing differs: a bit of breathing room below it before the map. */
.evt-nights-btn { margin-bottom: 12px; }

/* Where-to-find-them map — full-width like the home minimap: sticky, follows scroll, lockable. Opaque
   cream bg so scrolled content is hidden behind it. The scroll-fade lives on the sticky FILTER bar
   below (see .evt-filters), not here, so the map + filters read as one solid unit. */
/* No margin-bottom: it would leave a 12px gap the sticky filter bar only closes once you over-scroll
   past it, so scroll-to-top (which lands where the map first pins) and natural scrolling would settle
   12px apart. Flush = both land at the same spot. Spacing before the chips lives in .evt-filters padding. */
.evt-mapsec { position: sticky; top: 0; z-index: 12; background: var(--bg); margin: 0; padding: 4px 0 0; }
.evt-map { position: relative; overflow: hidden; width: 100%; height: 180px; border-radius: 14px;
	border: 1px solid var(--line); background: var(--placeholder); cursor: pointer; box-shadow: var(--shadow); }
/* Pins are the shared avatar-stack .mm-pin (see the home minimap) — character faces, not dots. */
.evt-map-pins { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.evt-map .mm-pin { pointer-events: auto; cursor: pointer; }
/* A locked Leaflet map (Disney's tiles) is the event map's base. isolation:isolate contains Leaflet's pane
   z-indexes so the pins overlay stays on top; the pin transform is static so pins ride the map's own transform. */
.evt-map-tile { position: absolute; inset: 0; isolation: isolate; }
.evt-map-tile { background: #8ea472 url(/static/photos/map/loader.webp?v=2) center/cover no-repeat; }
.evt-map .maplibregl-map { font: inherit; }
.evt-map .mm-pin-icon .mm-pin { transform: translate(-50%, -50%); }
/* The pins ease when the followed spot changes (the map itself pans via Leaflet). */
.evt-map.pins-ready .mm-pin { transition: transform .4s cubic-bezier(.4,0,.2,1), opacity .3s ease; }
.evt-map.has-focus .mm-pin { opacity: .4; }
.evt-map.has-focus .mm-pin.current { opacity: 1; z-index: 5; }
.evt-map-expand { position: absolute; right: 8px; top: 8px; width: 30px; height: 30px; border-radius: 9px;
	display: grid; place-items: center; background: color-mix(in srgb, var(--panel) 86%, transparent);
	-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); box-shadow: 0 1px 4px rgba(0,0,0,.25); }
.evt-map-expand .svgicon { width: 15px; height: 15px; background-color: var(--ink); }
.evt-map-lock { position: absolute; right: 8px; bottom: 8px; width: 30px; height: 30px; border-radius: 9px;
	display: grid; place-items: center; background: color-mix(in srgb, var(--panel) 86%, transparent);
	-webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); box-shadow: 0 1px 4px rgba(0,0,0,.25);
	color: var(--muted); cursor: pointer; }
.evt-map-lock .svgicon { width: 15px; height: 15px; }
.evt-map-lock.locked { background: #1c1f29; color: #fff; box-shadow: 0 1px 5px rgba(0,0,0,.35); }
@media (min-width: 560px) { .evt-map { height: 240px; } }

/* The event's Characters/Activities switcher reuses the home park switcher's .tabs/.tab/.tab-ind CSS
   (above) verbatim, so the two are identical and can't diverge — no separate rules here. */

/* Sticky filter bar under the map — a vertical stack: the seg control on top, the Characters-only
   search / Now / Later toolbar below (hidden on the Activities tab), and the scroll-fade on its bottom
   so the list fades under it. */
.evt-filters { position: sticky; top: 184px; z-index: 6; background: var(--bg);
	display: flex; flex-direction: column;   /* no gap: the tools row's own .toolbar margin-top spaces it,
	   exactly like the home .bar — adding a flex gap here would double it (that was the 22px vs 12px). */
	margin: 0 calc(-1 * var(--gutter)); padding: 16px var(--gutter) var(--fade-h);
	-webkit-mask-image: linear-gradient(#000 calc(100% - var(--fade-h)), transparent);
	mask-image: linear-gradient(#000 calc(100% - var(--fade-h)), transparent); }
@media (min-width: 560px) { .evt-filters { top: 244px; } }   /* desktop map is 240px tall */
.evt-filters[data-tab="activities"] .evt-lineup-tools,
.evt-filters[data-tab="characters"] .evt-act-tools { display: none; }
.evt-filters .search { caret-color: auto; }   /* native caret — no custom-caret JS on the event page */

/* Activity badges ("🍬 Treat Trail", "🌲 Walkthrough", …) reuse the time-chip look (.time.event) and
   sit in the times row. The Activities tab reuses the character card/row styles below. */
.avatar .evt-act-emoji { display: grid; place-items: center; width: 100%; height: 100%; font-size: 22px; }
.sheet-char .avatar .evt-act-emoji { font-size: 18px; }   /* the location card uses smaller (42px) avatars */

/* Emoji fallback inside an avatar pin when an activity spot has no image. */
.mm-emoji { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 13px; }
@media (min-width: 560px) { .mm-emoji { font-size: 24px; } }
.map-pin-emoji { display: grid; place-items: center; width: 100%; height: 100%; font-size: 18px; }

/* Lineup — one card per spot */
.evt-spot-name { font-family: "Epilogue", sans-serif; font-weight: 700; font-size: 16px; color: var(--ink);
	display: flex; align-items: center; gap: 6px; margin-bottom: 6px; cursor: pointer; }
.evt-spot-name:active { opacity: .6; }
.evt-spot-pin { width: 14px; height: 14px; background-color: var(--muted); flex: none; }
.evt-char { display: flex; align-items: flex-start; gap: 12px; padding: 8px 0; cursor: pointer; }
.evt-char:active { opacity: .6; }   /* rows open the full map on that location */
.evt-char:not(:last-child) { border-bottom: 1px solid var(--line); }
.evt-char-main { flex: 1; min-width: 0; }
.evt-char .name { font-weight: 600; font-size: 15px; margin-bottom: 6px; color: var(--ink); }
.evt-char .times { display: flex; flex-wrap: wrap; gap: 6px; }
.evt-empty { padding: 28px 8px; text-align: center; color: var(--muted); }

/* ===== Event-nights calendar — its CHROME is a shared .sheet (radius, padding, animation, backdrop;
   see index.html #eventCal + openEventCal). Only the calendar-specific content is styled here. ===== */
.evt-cal-titles { display: flex; flex-direction: column; }   /* title + subtext, tight (the .sheet-head gap is for its own children) */
.evt-cal-sub { font-size: 13px; color: var(--muted); margin-top: 2px; }
/* A little more breathing room above the title (clears the rounded top) and below the subtext. */
.evt-cal-drawer .sheet-head { padding: 24px 18px 14px; }
/* The shared .sheet-body is align-items:flex-start (for the character roster); the calendar's pager +
   grid need the full width instead, so stretch its children. */
.evt-cal-drawer .sheet-body { align-items: stretch; }
/* Month pager — a full-width row inside the flex-column .sheet-body (which supplies the side padding). */
.evt-cal-pager { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0 0; }
.evt-cal-month-name { font-family: "Epilogue", sans-serif; font-weight: 700; font-size: 15px; color: var(--ink); }
.evt-cal-nav { flex: none; width: 34px; height: 34px; border: none; border-radius: 50%; cursor: pointer;
	background: var(--panel-2); color: var(--muted); display: grid; place-items: center; }
.evt-cal-nav.prev { transform: scaleX(-1); }   /* reuse chevron-right, flipped for "previous" */
.evt-cal-nav:disabled { opacity: .35; cursor: default; }
.evt-cal-nav .svgicon { width: 16px; height: 16px; }
.evt-cal-body { width: 100%; }
.ec-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
/* The day grid always reserves the tallest month's week-row count (--ec-rows, set in JS) — short
   months get blank trailing rows — so paging between months never changes the drawer height. */
.ec-grid:not(.ec-dow) { grid-template-rows: repeat(var(--ec-rows, 6), 38px); }
.ec-dow { margin-bottom: 4px; }
.ec-cell { height: 38px; display: grid; place-items: center; font-size: 13px; color: var(--ink); border-radius: 9px; }
.ec-dow-c { height: 20px; font-size: 11px; font-weight: 600; color: var(--muted); }
.ec-empty { visibility: hidden; }
.ec-on { background: color-mix(in srgb, var(--evt-bg) 22%, var(--panel-2)); font-weight: 600; }
.ec-next { background: var(--evt-bg); color: var(--evt-ink); font-weight: 800; }
.ec-past { background: var(--panel-2); color: var(--muted); opacity: .55; }
/* Match the CHARACTER drawer's platform radius on the top corners (base .sheet is only 22px). */
@media (max-width: 559px) {
	.evt-cal-drawer:not(.floating) { border-radius: var(--sheet-radius) var(--sheet-radius) 0 0; }
}

/* ===== Tile map (Disney live tiles — the park map) ===== */
.tile-mode {
	/* Full-screen via a DYNAMIC-viewport height, not inset:0. On iOS Safari a fixed inset:0 pins to the *visual*
	   viewport and left the canvas short (green gaps); a static 100lvh covered the top but overshot/undershot the
	   bottom depending on toolbar state, showing the board below. 100dvh tracks the actual visible area top-to-bottom
	   and auto-updates as Safari's toolbars slide, so the map fills the glass exactly. (100vh fallback for old UAs.) */
	position: fixed; top: 0; left: 0; right: 0; height: 100vh; height: 100lvh; z-index: 200;
	/* Backdrop = the pre-blurred map (loader.webp), not a flat colour, so wherever the GL canvas is transparent
	   (tiles mid-load on a jump/pan, out of bounds, under the island) a soft blurred map shows through instead of
	   a green flash. #8ea472 is just the fallback colour until the webp paints. */
	background: #8ea472 url(/static/photos/map/loader.webp?v=2) center/cover no-repeat;
	/* Reveal = a pure fade + a subtle grow over an ALREADY-framed, static map (see openTileMap). The map
	   content doesn't move during this, so the CSS transform stays perfectly smooth — no Leaflet zoom
	   fighting it, no tiles refreshing behind it. */
	opacity: 0; transform: scale(1.02); transform-origin: center;
	transition: opacity .24s ease, transform .34s cubic-bezier(.2, .8, .2, 1);
	/* the relocated filter bar / event banner read this (it normally lives on .map-mode) */
	--map-bar-y: calc(max(12px, env(safe-area-inset-top, 0px)) + 36px);
}
.tile-mode.in { opacity: 1; transform: none; }
/* CRITICAL: while the map is only WARMING (laid out full-screen but not actually open — no .in), it must never
   eat taps meant for the board/minimap underneath. visibility:hidden alone wasn't reliably click-through in iOS
   Safari (the GL canvas still caught touches), which made minimap pin taps do nothing during the warm window. */
.tile-mode:not(.in) { pointer-events: none; }
.tile-mode.in { pointer-events: auto; }
/* Cold tap right after load: the overlay fades up on the same blurred-map placeholder the minimap uses (green +
   loader.webp) so the tap is acknowledged, until the real tiles + pins are ready. */
.tile-mode.tm-loading { background: #8ea472 url(/static/photos/map/loader.webp?v=2) center/cover no-repeat; }
/* Spinner shown ONLY on a cold open that's taking more than a moment (JS adds .tm-spin after ~450ms if the map
   still isn't ready, and removes it the instant it is). Warm/fast opens never get it. A white ring on the blurred
   map, with a soft dark scrim behind so it reads clearly. */
.tile-mode.tm-spin::before { content: ""; position: absolute; inset: 0; background: rgba(20, 26, 16, .28); z-index: 5; }
.tile-mode.tm-spin::after {
	content: ""; position: absolute; top: 50%; left: 50%; width: 40px; height: 40px; margin: -20px 0 0 -20px;
	border: 3px solid rgba(255, 255, 255, .35); border-top-color: #fff; border-radius: 50%;
	animation: tmSpin .8s linear infinite; z-index: 6; filter: drop-shadow(0 1px 4px rgba(0, 0, 0, .35));
}
@keyframes tmSpin { to { transform: rotate(360deg); } }
/* z-index:1 makes the MapLibre map its own stacking context, so its internal canvas/controls stay contained
   and the relocated chrome (filter bar, close) layers above the whole map.
   The canvas starts hidden and fades in only once its tiles are loaded (body/JS adds .tm-shown on map idle) —
   so an open reads as blur-placeholder → sharp map, never tiles visibly streaming in (which read as the delay). */
.tile-map { position: absolute; inset: 0; z-index: 1; opacity: 0; transition: opacity .22s ease; }
.tile-mode.tm-shown .tile-map { opacity: 1; }
.tile-mode .map-filter, .tile-mode .map-event-banner, .tile-mode .map-close,
.tile-mode .map-locate, .tile-mode .map-toast { z-index: 10; }
