/* Reset and base styles */
* {
	margin: 0;
	padding: 0;
	box-sizing: border-box;
}

:root {
	/* Core theme tokens: dark is the default. Light values are provided in
	   :root[data-theme="light"] below; the @media (prefers-color-scheme: light)
	   block applies the same light values when the user hasn't explicitly
	   chosen dark via the toggle. */
	--bg-color: #121212;
	--card-bg: #1e1e1e;
	--text-primary: #e8e8e8;
	--text-secondary: #888;
	--text-tertiary: #666;
	--credit-text: #b8b8b8;
	--filter-bar-bg: linear-gradient(135deg, #0a0a0a 0%, #181818 100%);
	--filter-bar-border: #2a2a2a;
	--button-bg: #333;
	--button-border: #444;
	--button-border-strong: #555;
	--border-color: #333;
	--link-active-outline: var(--text-secondary);
	--press-bg: #2a2a2a;
	--scroll-arrow-bg: rgba(51, 51, 51, 0.4);
	--scroll-arrow-border: rgba(255, 255, 255, 0.12);
	--modal-backdrop: rgba(0, 0, 0, 0.5);
	--modal-shadow: rgba(0, 0, 0, 0.4);
	--scrollbar-thumb: #333;
	/* `.show-tag` pill fill: mid-grey contrast block against card-bg. The
	   color pair flips across themes (light pill on dark card / dark pill on
	   light card). */
	--tag-fill: #aaa;
	--tag-on-fill: #1e1e1e;

	/* Per-venue brand colors. Used for `.venue-abbr` text + `.details-venue`
	   labels. Currently shared across themes; if a specific color reads poorly
	   on light, override it inside the [data-theme="light"] block. */
	--academy-color: #4a7edd;
	--bagdad-color: #da7e25;
	--cinema21-color: #e63419;
	--cinemagic-color: #d6ae29;
	--cinemark-ep-color: #5b8c5a;
	--hollywood-color: #a92028;
	--kennedy-color: #6b8e23;
	--laurelhurst-color: #f44336;
	--livingroom-color: #dd6033;
	--moreland-color: #0d8a8c;
	--omsi-color: #3aa8d8;
	--tomorrow-color: #fd26d9;
	--regal-ft-color: #c25a7c;
	--regal-lc-color: #7b2d8e;
	--regal-pp-color: #4a9b8f;
	--stjohns-color: #8b5cf6;
	--stjohnstwin-color: #d4577b;
	--studioone-color: #2196f3;
	--deadcomics-color: #c44266;
	--helium-color: #9c4878;
	--kickstand-color: #fc561e;
}

/* Light theme: explicit user choice (toggle) OR system preference when the
   user hasn't picked dark explicitly. The duplication between the two
   selectors is intentional; keeps the auto branch local to the media query
   without relying on a JS toggle to set data-theme="auto", and means the
   page renders correctly even before JS boots. The toggle writes
   data-theme="light"/"dark"/(absent) on <html> to override the media query. */
:root[data-theme="light"] {
	--bg-color: #f7f7f7;
	--card-bg: #ffffff;
	--text-primary: #1a1a1a;
	--text-secondary: #555;
	--text-tertiary: #999;
	--credit-text: #444;
	--filter-bar-bg: linear-gradient(135deg, #e8e8e8 0%, #f5f5f5 100%);
	--filter-bar-border: #d0d0d0;
	--button-bg: #f0f0f0;
	--button-border: #c8c8c8;
	--button-border-strong: #a0a0a0;
	--border-color: #dadada;
	--link-active-outline: var(--border-color);
	--press-bg: #e0e0e0;
	--scroll-arrow-bg: rgba(220, 220, 220, 0.55);
	--scroll-arrow-border: rgba(0, 0, 0, 0.06);
	--modal-backdrop: rgba(255, 255, 255, 0.25);
	--modal-shadow: rgba(0, 0, 0, 0.15);
	--scrollbar-thumb: #bbb;
	--tag-fill: #555;
	--tag-on-fill: #f5f5f5;
}

@media (prefers-color-scheme: light) {
	:root:not([data-theme="dark"]) {
		--bg-color: #f7f7f7;
		--card-bg: #ffffff;
		--text-primary: #1a1a1a;
		--text-secondary: #555;
		--text-tertiary: #999;
		--credit-text: #444;
		--filter-bar-bg: linear-gradient(135deg, #e8e8e8 0%, #f5f5f5 100%);
		--filter-bar-border: #d0d0d0;
		--button-bg: #f0f0f0;
		--button-border: #c8c8c8;
		--button-border-strong: #a0a0a0;
		--border-color: #dadada;
		--link-active-outline: var(--border-color);
		--press-bg: #e0e0e0;
		--scroll-arrow-bg: rgba(220, 220, 220, 0.55);
		--scroll-arrow-border: rgba(0, 0, 0, 0.06);
		--modal-backdrop: rgba(255, 255, 255, 0.25);
		--modal-shadow: rgba(0, 0, 0, 0.15);
		--scrollbar-thumb: #bbb;
		--tag-fill: #555;
		--tag-on-fill: #f5f5f5;
	}
}

/* Document-level scrolling, required for iOS Safari's tap-status-bar-to-scroll-top
   gesture to work. The filter bar scrolls away naturally with content; users get it
   back by scrolling up or tapping the status bar. */
html {
	scrollbar-width: thin;
	scrollbar-color: var(--scrollbar-thumb) transparent;
}

html::-webkit-scrollbar {
	width: 4px;
}

html::-webkit-scrollbar-track {
	background: transparent;
}

html::-webkit-scrollbar-thumb {
	background: var(--scrollbar-thumb);
	border-radius: 2px;
}

body {
	font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
	background: var(--bg-color);
	color: var(--text-primary);
	font-size: 14px;
	/* iOS tap-handling: disable double-tap-to-zoom while preserving pan +
	   pinch-zoom (accessibility-friendly, `user-scalable=no` is ignored
	   by iOS since iOS 10), drop the default blue-gray tap-flash overlay
	   (site has its own `:active` styling), and globally disable text
	   selection + the long-press share/copy callout. Default-deny means
	   future chrome/UI gets the right behavior without remembering to
	   opt out. The handful of places that DO want selection are opted
	   back in below. */
	touch-action: manipulation;
	-webkit-tap-highlight-color: transparent;
	-webkit-user-select: none;
	user-select: none;
	-webkit-touch-callout: none;
}

/* Opt back in to text selection only where it's wanted: form fields
   (for typing/editing) and the specific text divs of the details modal
   (titles, addresses, descriptions, credits, meta, the stuff worth
   copying). Listed explicitly rather than via a `.modal-box` parent
   rule + `.modal-box a` opt-out so that anchors, the QR `<a>`, and
   any future non-text widgets in the modal stay non-selectable without
   needing a counter-override. */
input,
textarea,
.details-venue,
.details-venue-address,
.details-title,
.details-datetime,
.details-meta,
.details-description,
.details-credits {
	-webkit-user-select: text;
	user-select: text;
	-webkit-touch-callout: default;
}

/* The filter bar's one spacing unit: its own padding on all four sides, the gap
   between its rows, and the gap between controls within a row. Responsive, and
   the only place those distances are set. The tag picker's rows and the show
   list measure against it too, so the bar, the modal it opens and the list below
   it space alike. */
:root {
	--filter-bar-gap: 8px;
}

/* Space between two rows of a list, wherever a list has rows: the menu's filter
   options, the tag picker's rows, and the show cards. Flat rather than
   responsive, and deliberately TIGHTER than --filter-bar-gap: the bar gap frames
   a list, this one separates items inside it. */
:root {
	--row-gap: 6px;
}

/* A row's own horizontal padding, i.e. how far its text sits inside its box.
   Responsive with the type. Rows across the app share it, and the date header
   adds it to --filter-bar-gap to line its label up with the card text below. */
:root {
	--card-pad-x: 10px;
}

/* The date header's vertical padding. A variable rather than a literal because
   the header's top fade has to end EXACTLY where its text begins, so the
   gradient stop and the padding must never drift apart: if the fade ran longer
   than the padding, the previous header's text would show through it. Both
   values read this, and the breakpoints below re-point it instead of setting
   padding-top/padding-bottom. */
:root {
	--date-header-pad-y: 10px;
}

/* Filter bar, column layout for both the main page and the menu overlay. Rows of
   controls come from `.filter-bar-row`; a `.search-pills` row follows the search
   row in each and collapses to nothing when the query holds no terms. */
.filter-bar {
	display: flex;
	flex-direction: column;
	gap: var(--filter-bar-gap);
	padding: var(--filter-bar-gap);
	background: var(--filter-bar-bg);
	border-top: 1px solid var(--filter-bar-border);
	border-bottom: 1px solid var(--filter-bar-border);
	flex-shrink: 0;
}

.filter-bar-row {
	display: flex;
	gap: var(--filter-bar-gap);
	width: 100%;
}

.filter-bar button,
.picker-clear {
	background: var(--button-bg);
	color: var(--text-primary);
	border: 1px solid var(--button-border);
	border-radius: 6px;
	padding: 10px 12px;
	font-size: 16px;
	font-family: inherit;
	font-weight: 700;
	cursor: pointer;
	flex-shrink: 0;
	min-width: 95px;
}

/* Theme cycle button (sun / moon / half = light / dark / auto). First child
   of the main filter-bar's top row, left of the segmented control. Hidden
   in menu mode because the whole .filter-bar is hidden then; no extra rule
   needed. Height, padding, font-size, etc. inherit from `.filter-bar button`
   so the toggle stays vertically consistent with its row neighbors. Only
   min-width is overridden: the base 95px is too wide for a single icon, and
   the explicit value here is sized to fit the widest of the three glyphs
   (☀ has the largest natural width thanks to its rays) plus the inherited
   padding, so cycling through states doesn't shift the button width and
   bump the adjacent segmented control left or right. */
.filter-bar button.theme-toggle {
	min-width: 50px;
}

/* Tag picker button. Now a real <button>, so it inherits `.filter-bar button`
   and only needs the size override; the selector has to chain `button` to beat
   that rule's `min-width: 95px`, exactly as `.theme-toggle` does. */
.filter-bar button.tag-select {
	width: 50px;
	min-width: 50px;
}

/* No tag in the query: match the unselected segmented-control cell, the same
   treatment the theme toggle uses for its "no choice made" auto state. */
.filter-bar button.tag-select-inactive {
	background: var(--card-bg);
	color: var(--text-secondary);
	font-weight: normal;
	border-color: var(--border-color);
}

/* In auto mode the toggle reads as "no choice made"; match the visual
   weight of an unselected segmented-control cell. The unselected segment's
   fill comes from its .segmented-control parent's `--card-bg` background
   showing through the segment's own transparent bg; on a standalone button
   we have to set `--card-bg` explicitly. Light and dark modes (when
   data-theme is set on <html>) fall through to the default .filter-bar
   button styling, which matches the SELECTED segmented-cell look.
   Selector specificity ties with `.filter-bar button.theme-toggle` above
   (0,2,1), so source order (this rule later) wins. */
html:not([data-theme]) .theme-toggle {
	background: var(--card-bg);
	color: var(--text-secondary);
	font-weight: normal;
	border-color: var(--border-color);
}

.filter-bar button:disabled {
	opacity: 0.3;
	cursor: default;
}

.filter-bar .filter-button {
	flex: 1;
	min-width: 0;
	background: var(--card-bg);
	border: 1px solid var(--border-color);
	font-weight: normal;
	text-align: left;
	position: relative;
	padding-right: 26px;
	display: flex;
	align-items: center;
	gap: 8px;
}

.filter-bar .filter-button::after {
	content: '\25be';
	color: var(--text-secondary);
	position: absolute;
	right: 10px;
	top: 50%;
	transform: translateY(-50%);
}

/* Filter button text mirrors the menu's bold-title + gray-sub hierarchy.
   Title fills available space (truncating if needed) so sub aligns to the right. */
.filter-button-title {
	font-weight: 700;
	flex: 1;
	min-width: 0;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.filter-button-sub {
	color: var(--text-secondary);
	flex-shrink: 0;
	white-space: nowrap;
}

/* Segmented control for the category filter (Comedy/Movies). Sized to fit its
   content, with equal-width segments via grid. align-self: flex-start prevents
   stretch when filter-bar is column-stacked. Each segment carries its own
   outer border (inactive var(--border-color) / active var(--button-border)),
   borders sit in the same layer rather than nested inside a container border. */
.segmented-control {
	flex-shrink: 0;
	align-self: flex-start;
	display: grid;
	grid-template-columns: 1fr 1fr;
	background: var(--card-bg);
	border-radius: 6px;
	overflow: hidden;
}

/* Segment label size, shared rather than restated so `.picker-heading`,
   which stands in for the whole control in the comedy category, always renders
   at the size an active segment does. Overridden per breakpoint alongside the
   segment's padding below. */
:root {
	--segment-font-size: 16px;
}

.segmented-control .segment {
	background: transparent;
	color: var(--text-secondary);
	padding: 10px 12px;
	font-size: var(--segment-font-size);
	font-family: inherit;
	font-weight: normal;
	cursor: pointer;
	border: 1px solid var(--border-color);
	/* Override .filter-bar button's border-radius: 6px (which would round
	   middle segments in N-segment controls). First/last segments re-set
	   their outer corners explicitly below; middle segments stay square. */
	border-radius: 0;
}

/* Hidden pseudo-element renders each segment's label in its bold (active)
   weight so the segment's intrinsic width stays constant regardless of which
   one is active. Without this, switching active shifts widths because bold and
   normal renderings of the words have different widths. */
.segmented-control .segment::before {
	content: attr(data-label);
	display: block;
	height: 0;
	line-height: 0;
	visibility: hidden;
	overflow: hidden;
	font-weight: 700;
	pointer-events: none;
}

/* N-segment border model. Each segment owns its right border (the divider
   with the next segment); only the first segment also owns a left border
   (the outer-left), and the last segment's right border doubles as the
   outer-right. Non-first segments suppress their own left border so the
   previous segment's right border isn't doubled. Adjacent borders at the
   active boundary are deduped by the :has() rule below. */
.segmented-control .segment:not(:first-child) {
	border-left: none;
}

.segmented-control .segment:first-child {
	border-radius: 6px 0 0 6px;
}

.segmented-control .segment:last-child {
	border-radius: 0 6px 6px 0;
}

/* Active segment has a `--button-border` outline on all four sides. The
   `.active` rule comes after `:not(:first-child)`, so its border shorthand
   restores the left border on a non-first active segment. That left border
   serves as the divider with the previous segment, paired with the :has()
   rule below which hides the previous segment's right border so the active's
   left border isn't doubled. Outer corners on first/last active stay
   rounded (border-radius isn't touched by the shorthand). */
.segmented-control .segment.active {
	background: var(--button-bg);
	color: var(--text-primary);
	font-weight: 700;
	border: 1px solid var(--button-border);
}

.segmented-control .segment:has(+ .segment.active) {
	border-right: none;
}

/* Date-range segmented control (4 segments) inside the show-filter overlay.
   Same visual language as the 2-segment category control above; the
   general N-segment border model handles the dividers between inactive
   middle segments and the active boundary (using --border-color and
   --button-border) without per-variant overrides. Only the layout differs:
   this one stretches across the row width via flex:1 (overriding the base
   align-self: flex-start). */
#date-range-filter {
	flex: 1;
	grid-template-columns: repeat(5, 1fr);
}

/* Override the 95px min-width inherited from .filter-bar button. The 5-segment
   date row is grid-driven (1fr per column) inside a flex:1 container, so the
   95px floor would force a 475px row minimum on narrow phones and overflow.
   With min-width: 0 the columns shrink to fit, with the ::before bold-width
   lock above keeping the lower bound at the widest data-label's bold width. */
#date-range-filter .segment {
	min-width: 0;
}

/* At <=450px width the day-count segments shorten ("7 Days" to "7d", etc.) so
   all 5 segments fit on a phone-width screen. Two text spans live in the DOM
   per day-count button (one full, one short); only one is visible per
   breakpoint. The ::before bold-width lock swaps its attr() source in lock
   step so the active-vs-inactive width consistency still works at narrow
   widths. Today and All have no short form and skip the wrapping. */
.segmented-control .segment .seg-short {
	display: none;
}

/* Buttons that trade their label for a shorter one on narrow screens carry
   both, and the media query below swaps which one shows. Same shape as the
   date-range segments' `.seg-full` / `.seg-short`. `.btn-glyph` marks the
   subset whose short form is an ICON rather than a word, which is what earns
   the square treatment below; "Show All" shortens to "All" and stays a normal
   button. The Any/All toggle is NOT one of these: `toggleSearchMatchMode`
   rewrites its textContent, which would delete any spans inside it. */
.btn-short {
	display: none;
}

@media (max-width: 450px) {
	.btn-full {
		display: none;
	}

	.btn-short {
		display: inline;
	}

	/* A button showing its glyph instead of its word takes the same square as
	   the theme and # buttons, so the icon-only controls all match. `:has` picks
	   them out by the markup itself, which keeps Cancel and Done (neither has a
	   class) out of a hardcoded selector list. Chains `button` to beat
	   `.filter-bar button`'s `min-width: 95px`, as `.theme-toggle` does. */
	.filter-bar button:has(.btn-glyph),
	.picker-search button:has(.btn-glyph) {
		width: 50px;
		min-width: 50px;
	}

	/* Shortened to "All", which fits the same square, so it lines up with the
	   theme toggle at the other end of its grid row. Its grid column is `auto`,
	   so narrowing the button hands the width to the category control. */
	.filter-bar button#reset-filters {
		width: 50px;
		min-width: 50px;
	}

	/* The mode toggle keeps a FIXED width rather than shrinking to its text: its
	   three labels differ by 20px at this font, so a content-sized button would
	   jump on every tap. The widest is "None" at 40.6px of text, which needs
	   67px with the 12px side padding and borders; 70px rounds that off. Same
	   reasoning that fixes `.theme-toggle`'s width across its three glyphs. */
	.filter-bar button.search-mode-toggle {
		width: 70px;
		min-width: 70px;
	}

	#date-range-filter .segment[data-label-short] .seg-full {
		display: none;
	}

	#date-range-filter .segment[data-label-short] .seg-short {
		display: inline;
	}

	#date-range-filter .segment[data-label-short]::before {
		content: attr(data-label-short);
	}
}

/* The filter-overlay DOM is shared between the venue and show filters; the
   date-range row only applies to the show filter (where it caps showtime
   horizon). Venue mode hides the row entirely so it doesn't take space or
   imply a date-range edit affects venue selection. */
#filter-overlay[data-filter-type="venue"] .date-range-row {
	display: none;
}

/* Full-page filter overlay, flows in normal body layout (no fixed positioning)
   so the document is the scroll container. iOS Safari's tap-status-bar gesture
   then scrolls the menu's content the same way it scrolls the show list.
   `body.menu-open` (toggled by JS) hides the body's main filter-bar + main, so
   the overlay is the only visible body content while it's shown. */
.filter-overlay {
	background: var(--bg-color);
}

.filter-overlay.hidden {
	display: none;
}

/* When the menu is open, hide the body's normal content so the overlay (now
   flowing in document layout) takes over the viewport. The overlay's filter-bar
   pins to the top via `position: sticky`. We use `display: none` despite the
   iOS Safari layout-viewport-staleness bug it triggers (paints the show list
   visibly offset on menu close, paints the menu visibly offset on menu open
   even though `window.scrollY` mechanically reports 0). We tried switching to
   `visibility: hidden; height: 0; overflow: hidden; padding: 0; border: 0;
   margin: 0` to keep the elements in the layout tree at zero rendered size,
   on the theory that `display: none` was the trigger. But the bug reproduced
   immediately, so any dramatic body-height collapse triggers the same Safari
   recompute regardless of HOW the collapse is achieved. The workaround lives
   in JS: every body-scroll write site uses `scrollTo(0, -1)` (or the
   conditional `X === 0 ? -1 : X` for restore-position cases), which clamps to
   0 visually but carries a non-zero scroll intent that flushes Safari's stale
   layout viewport. See the comment in `openFilterOverlay` and monist's 2023
   answer on Stack Overflow #24616322:
     https://stackoverflow.com/questions/24616322/mobile-safari-why-is-window-scrollto0-0-not-scrolling-to-0-0
*/
body.menu-open > .filter-bar,
body.menu-open > main {
	display: none;
}

/* The overlay's own filter-bar sticks to the top of the document scroll so it
   stays visible as the user scrolls through options. Column layout + the
   row+search structure is inherited from the base `.filter-bar` rule. */
.filter-overlay > .filter-bar {
	position: sticky;
	top: 0;
	z-index: 100;
}

/* Set by renderOverlayOptions when the Theaters picker would be empty. */
#filter-overlay.no-tag-picker #menu-tag-button {
	display: none;
}

/* Theater rows put their format badges at the RIGHT edge of the count line, so
   the badges line up down the menu instead of starting at a different offset on
   every row. Flex here replaces the shared `-webkit-box` line clamp, which these
   short one-line subs never needed. Gated on `:has(.venue-formats)` so it touches
   ONLY badge-bearing rows: applied to every sub line it also caught the "All
   Theaters" row and knocked its centered count to the left. */
#filter-overlay[data-filter-type="venue"] .show-description:has(.venue-formats) {
	display: flex;
	align-items: center;
	/* The shared rule clips with `overflow: hidden` to serve its 2-line clamp.
	   A badge is taller than the 10px text line it sits on, so that clipping
	   shaved the top pixels off every pill. Nothing to clamp on a one-line
	   count, so the clipping goes too. */
	overflow: visible;
}

.venue-formats {
	margin-left: auto;
	/* Flex, so the badges are laid out as flex items and `.show-tag`'s
	   `vertical-align: 1px` (which aligns pills against title TEXT in the shows
	   list) stops applying and leaving them sitting a pixel high. `gap` replaces
	   the pill's own left margin for the same reason. */
	display: flex;
	align-items: center;
	gap: 4px;
}

.venue-formats .show-tag {
	margin-left: 0;
}

/* Type-to-filter search row is hidden on the touchscreen kiosk (no keyboard
   there). Hiding the whole row collapses both the mode toggle and the input
   plus removes the row's gap. Body class is added by JS when `isKiosk()` is
   true. The pill row goes with it: without a keyboard there is no way to put a
   term into the query in the first place. */
body.kiosk .search-row,
body.kiosk .search-pills {
	display: none;
}

/* Selected `((tag))` / `[[genre]]` / `{{rating}}` terms, shown under the
   shows-list search row once they are lifted out of the field. All three kinds
   share ONE centered, wrapping row; the pill styling is what distinguishes a
   tag from a genre from a rating, so no headings are needed. An all-words query
   renders nothing at all, so the container is empty and `:empty` collapses it. */
.search-pills {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	/* The bar gap, not --row-gap: pills sit beside each other the way the bar's
	   buttons do, not stacked the way list rows do. */
	gap: var(--filter-bar-gap);
}

.search-pills:empty {
	display: none;
}

/* Tags and ratings reuse `.show-tag` / `.show-tag-rating` verbatim, so this
   only has to undo the one part that assumes an inline run inside a title: the
   pill's own left margin, since the row spaces with `gap`. */
/* Scoped to the container so these beat `.show-tag`, which is declared LATER
   at equal specificity and would otherwise win font-size, padding and the
   margin this rule is trying to undo. */
.search-pills .search-pill {
	margin-left: 0;
	cursor: pointer;
	font-size: 13.5px;
	padding: 2px 8px;
	border-radius: 5px;
}

/* Genres and ratings carry a 1px border, so they trade a pixel of padding for
   it and all three pills end up the same outer size. Must stay AFTER the rule
   above: same specificity, so source order is what decides. */
.search-pills .search-pill-genre,
.search-pills .show-tag-rating {
	padding: 1px 7px;
}

.search-pill-x {
	margin-left: 6px;
	opacity: 0.65;
}

/* Side buttons of the search row: Any/All mode toggle on the left, Clear on
   the right. Same visual styling as the primary filter-bar buttons so the row
   reads as a single control strip. iOS Safari doesn't render the native X on
   `<input type="search">`, so the Clear button covers that gap on all
   platforms. flex-shrink: 0 keeps them from collapsing when the input grows. */
.search-mode-toggle,
.search-clear-button {
	flex-shrink: 0;
	background: var(--button-bg);
	color: var(--text-primary);
	border: 1px solid var(--button-border);
	border-radius: 6px;
	padding: 10px 12px;
	font-size: 16px;
	font-family: inherit;
	font-weight: 700;
	cursor: pointer;
	min-width: 70px;
}

/* Hide both side buttons while the search input is empty (placeholder
   showing). Once the user types, both buttons appear, flex layout lets the
   input expand to fill the row in the meantime. */
.search-row:has(.filter-overlay-search:placeholder-shown) .search-mode-toggle,
.search-row:has(.filter-overlay-search:placeholder-shown) .search-clear-button {
	display: none;
}

/* ...but terms live OUTSIDE the field now, so an empty field no longer means an
   empty search. With pills on screen both buttons stay, since the mode applies
   to the terms and Clear is the only way to drop them all at once. Set by
   renderSearchPills on the OWNING bar rather than on body: the shows row and the
   menu overlay's row hold independent queries, so each carries its own state. */
.filter-bar.has-terms > .search-row > .search-mode-toggle,
.filter-bar.has-terms > .search-row > .search-clear-button {
	display: block;
}

/* Type-to-filter search input on the search row of the filter-bar. font-size
   >=16px to prevent iOS Safari from auto-zooming on focus. Centered text +
   placeholder so the input visually balances with the centered title above.
   flex: 1 + min-width: 0 so it fills the rest of the row next to the mode
   toggle without overflowing. */
.filter-overlay-search {
	flex: 1;
	min-width: 0;
	background: var(--card-bg);
	color: var(--text-primary);
	border: 1px solid var(--border-color);
	border-radius: 6px;
	padding: 10px 12px;
	font-size: 16px;
	font-family: inherit;
	text-align: center;
}

.filter-overlay-search::placeholder {
	color: var(--text-tertiary);
}

/* Hide the native WebKit search clear (X) button, we render our own Clear
   button on the right of the input for cross-platform parity (iOS Safari
   doesn't render the native one). Without this, desktop Safari/Chrome show
   both. Firefox has no native clear button so it's a no-op there. */
.filter-overlay-search::-webkit-search-cancel-button {
	-webkit-appearance: none;
	display: none;
}

.filter-overlay-search:focus {
	outline: none;
	border-color: var(--button-border-strong);
}

/* Shown in the option list when a search query filters every row out. */
/* .filter-overlay-empty styling lives in the shared status-messages
   block (see ".loading, .error, .no-shows, ..."). */

.filter-overlay-title {
	flex: 1;
	min-width: 0;
	font-size: 20px;
	font-weight: 600;
	color: var(--text-primary);
	text-align: center;
	align-self: center;
}

/* Options flow in body scroll, no own scrollable area. The padding frames the
   list the same way the bar above it frames its controls, so both inset by
   --filter-bar-gap and the option boxes line up with the bar's buttons. */
.filter-overlay-list {
	padding: var(--filter-bar-gap);
}

/* Footer at the bottom of the venue menu. Always contains the "Scraper
   Summary" link to scraper-summary.html. When every visible venue shares the
   same lastUpdated timestamp, also contains a two-line "Last Updated X"
   block: relative time on line 1, absolute time on line 2 (darker, matches
   .show-last-updated / .show-venue-address). */
.filter-overlay-footer {
	text-align: center;
	color: var(--text-secondary);
	font-size: 13px;
	padding: 12px 14px 8px;
	line-height: 1.4;
}

.filter-overlay-footer > div:last-of-type {
	color: var(--text-tertiary);
}

/* Scraper Summary link at the bottom of the venue-menu footer. Inherits the
   footer's muted text color, underlined so it reads as a link. Block-display
   with a small top margin so it reads as its own row beneath the Last
   Updated lines (when present). */
.filter-overlay-footer-link {
	display: block;
	margin-top: 6px;
	color: var(--text-secondary);
	text-decoration: underline;
}

/* Close button, tucked into the modal's top-left padding. A child of the modal
   rather than of either mode's content, so it shows in both and, more to the
   point, sits OUTSIDE whichever element is scrolling. That is what stops it
   drifting during elastic overscroll; it used to be sticky here with offsets
   derived from the padding, all of which the scroll wrapper made unnecessary. */
.modal-close {
	position: absolute;
	top: 4px;
	left: 4px;
	width: 24px;
	height: 24px;
	padding: 0;
	background: none;
	border: none;
	border-radius: 6px;
	color: var(--text-tertiary);
	opacity: 0.6;
	font-family: inherit;
	font-size: 18px;
	line-height: 1;
	cursor: pointer;
}

/* Same box in the opposite corner, with nothing drawn in it. Both live inside
   the modal's 28px padding, so neither reaches the tag-mode segmented control
   or any details content. */
.modal-close-mirror {
	position: absolute;
	top: 4px;
	right: 4px;
	width: 24px;
	height: 24px;
	padding: 0;
	background: none;
	border: none;
	cursor: pointer;
}

/* Remembered side. Absent means left, which is why the rules above hardcode the
   default corner and only the swap is conditional: nothing has to run before
   first paint for the page to look right. Both offsets are reset to `auto`
   rather than only setting the new one, since `left: 4px; right: 4px` on a
   24px-wide absolute box resolves to the LEFT edge and the swap would silently
   do nothing. */
:root[data-close-side="right"] .modal-close {
	left: auto;
	right: 4px;
}

:root[data-close-side="right"] .modal-close-mirror {
	right: auto;
	left: 4px;
}

/* Tag picker, rendered inside `.modal-box`. Tag mode hides the show-detail
   children and shows the list instead, so the overlay's tuned dismissal
   behavior is reused rather than duplicated. */
.picker-list,
.picker-search,
.picker-header,
.picker-footer {
	display: none;
}

/* One element to hide, now that the detail children live in a wrapper. This
   used to be a `:not()` exception list naming every tag-mode child, plus a
   second rule for `#details-tickets` alone because an ID outweighs a class
   selector. Both collapse into this. */
.modal-box.tag-mode > .details-scroll {
	display: none;
}

/* Top-anchored instead of centered by the overlay, so switching sections does
   not move the modal: 81 tags fills the 88vh cap while 4 ratings is a fraction
   of it, and centering would slide the whole box each time. The offset is half
   the leftover after the cap, which is where the overlay already places a modal
   that hits it, so the tall case renders exactly as if centered and the short
   ones hang from that same line. Keep the two in step: a change to max-height
   has to be mirrored here as (100 - cap) / 2. */
.modal-box.tag-mode {
	align-self: flex-start;
	margin-top: 6vh;
}

/* Above the segmented control, because it is what changes the counts on it:
   reading top-down, you type and then see which section your words hit. Reuses
   the filter-bar search row wholesale, so the mode toggle and Clear hide
   themselves while the field is empty. */
.modal-box.tag-mode > .picker-search {
	display: flex;
	flex: none;
	margin-bottom: var(--filter-bar-gap);
}

/* The kiosk rule that hides every other search row is less specific than the
   one above, so it needs restating to reach this one. Same reason: no keyboard. */
body.kiosk .modal-box.tag-mode > .picker-search {
	display: none;
}

/* One row: the segmented control. Stays a COLUMN flex so `align-self: stretch`
   on the control still means "fill the width"; as a row it would mean "fill the
   height" and the control would collapse to its content width again. */
.modal-box.tag-mode > .picker-header {
	display: flex;
	flex-direction: column;
	align-items: stretch;
	flex: none;
	margin-bottom: calc(var(--filter-bar-gap) * 2);
}

/* Below the list: Clear on the left, Done on the right. */
.modal-box.tag-mode > .picker-footer {
	display: flex;
	flex: none;
	margin-top: calc(var(--filter-bar-gap) * 2);
}

/* Auto margin rather than `justify-content: space-between`, which would put Done
   on the LEFT whenever Clear is absent. */
.picker-done {
	margin-left: auto;
}

/* Three equal segments spanning the header. Both base-rule defaults have to be
   overridden: `1fr 1fr` wraps the third segment onto a second row, and
   `align-self: flex-start` shrinks the control to its content instead of
   filling the row. No inset is needed to clear the close X, which is absolutely
   positioned inside the modal's 28px left padding and never reaches the
   content box. */
.picker-segments {
	grid-template-columns: repeat(3, 1fr);
	align-self: stretch;
}

/* Both properties exist to keep the three columns equal and one line tall, and
   both are consequences of `1fr` meaning `minmax(auto, 1fr)`.

   `nowrap`: the row is sized against each label's MIN-content width, so a
   two-word label wraps during that intrinsic pass and the row comes out two
   lines tall even at widths where the used column fits it on one line. The
   category control never hits this because its labels are one word.

   The narrow padding: the `auto` floor is the label plus its horizontal
   padding, and at the large-display breakpoints the widest label's floor
   exceeds its 1fr share and takes width from the other two, so the control
   renders with a fat middle cell. Trimming the padding lowers the floor below
   the share at every breakpoint. It costs nothing visually because the labels
   are centered in columns whose width the grid, not the padding, decides. The
   vertical padding is untouched and still tracks `.filter-bar button`.

   Both class names are on the same element, purely to outweigh the
   `.segmented-control .segment` padding in the large-display media queries,
   which match this weight and come later in the file. */
.segmented-control.picker-segments .segment {
	white-space: nowrap;
	padding-left: 6px;
	padding-right: 6px;
}

/* Same treatment as `.filter-overlay-title`, the site's existing menu heading. */
/* Matches an ACTIVE segment: same size via the shared token, same 700 weight and
   primary colour. It replaces the control outright in comedy, so the two should
   never read as different type. */
.picker-heading {
	font-size: var(--segment-font-size);
	font-weight: 700;
	color: var(--text-primary);
}

/* Clear and Done take their styling from `.filter-bar button`, which they are
   listed in rather than copying, so the two can never drift apart. */

/* One scroller shape, shared by the two elements that use it: the tag list in
   tag mode and the details wrapper otherwise. Everything below was tuned on the
   tag list first; the details wrapper inherits it rather than rediscovering it,
   including the scrollbar gutter that would otherwise put a scrollbar inside the
   modal padding. */
.modal-scroller {
	--fade-top: 0px;
	--fade-bottom: 0px;
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
	/* Gutter for the scrollbar so it sits clear of the row borders instead of
	   floating over them (macOS/iOS overlay scrollbars do not reserve space).
	   The negative margin pulls that gutter into the modal's own padding, so
	   the rows keep their full width. */
	padding-right: 10px;
	margin-right: -10px;
	/* Row heights are fractional, so the content's true height ends past the
	   integer `scrollHeight` the scroll layer is sized to. That last fraction is
	   clipped by the layer's own bounds, which is why the final row's 1px border
	   rendered half-height and stayed clipped even while rubberband-scrolled away
	   from the container edge. One pixel of slack puts the border inside the
	   bounds. Pairs with `.tag-row:last-child { margin-bottom: 0 }`, whose whole
	   point is that the list ends flush; 1px reads as flush, 6px did not. The
	   negative margin pulls that pixel back into the modal's own bottom padding,
	   the same trick the scrollbar gutter uses above, so the layer keeps its extra
	   pixel while the gap below stays even with the other three sides. */
	padding-bottom: 1px;
	margin-bottom: -1px;
	/* Reaching either end of the list stops there instead of chaining into body
	   scroll. Besides the obvious, that chained scroll moved `window.scrollY`,
	   which could cross the dismiss threshold and close the overlay mid-read.
	   Scoped to the LIST on purpose: `.modal-overlay` deliberately stays
	   unconstrained so a drag on the backdrop still chains and dismisses. Inner
	   scroller only, so the document-level body-scroll model is untouched. */
	overscroll-behavior: contain;
	scrollbar-width: thin;
	scrollbar-color: var(--scrollbar-thumb) transparent;
	/* Fade the scrolled content at whichever edge has more content past it.
	   Masking the LIST rather than the modal leaves the card background, border
	   and shadow untouched. The widths default to 0 (see `.picker-list`
	   above) so a scroller sitting at the top has a hard top edge; JS raises them
	   by toggling `.fade-top` / `.fade-bottom`, whose extra class outweighs the
	   defaults. They used to need a separate lower-specificity rule; sharing one
	   class made that unnecessary. */
	-webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
	mask-image: linear-gradient(to bottom, transparent 0, #000 var(--fade-top), #000 calc(100% - var(--fade-bottom)), transparent 100%);
}

/* Which scroller is live, per mode. `display` cannot join the shared rule:
   each element has to be hidden in the OTHER mode. Tag rows also read as a
   list rather than as the centred detail card around them. */
.modal-box.tag-mode > .picker-list {
	display: block;
	text-align: left;
}

.modal-box:not(.tag-mode) > .details-scroll {
	display: block;
}


/* Match the page scrollbar (html::-webkit-scrollbar) rather than the platform default. */
.modal-scroller::-webkit-scrollbar {
	width: 4px;
}

.modal-scroller::-webkit-scrollbar-track {
	background: transparent;
}

.modal-scroller::-webkit-scrollbar-thumb {
	background: var(--scrollbar-thumb);
	border-radius: 2px;
}

.modal-scroller.fade-top {
	--fade-top: 16px;
}

.modal-scroller.fade-bottom {
	--fade-bottom: 16px;
}

/* Matches the size/weight a menu row's title renders at, without `.show-title`
   itself, whose nowrap + mask truncation would stop long tags from wrapping. */
.tag-row-label {
	flex: 1;
	min-width: 0;
	font-size: 15px;
	font-weight: 500;
}

/* Weight and color match `.show-tag-genre`; nothing else from it. The row is a
   centered flex line and the label takes the free space, so the count lands
   right-aligned and vertically centered on its own. */
.tag-row-count {
	flex-shrink: 0;
	margin-left: 10px;
	font-size: 11px;
	font-weight: 400;
	color: var(--text-tertiary);
}

.show-card.filter-overlay-option.tag-row {
	display: flex;
	align-items: center;
}

/* Empty state: same row shape, dimmed because it is not a tag, but it keeps
   the row's press feedback and closes the modal when tapped. */
.tag-row-empty {
	opacity: 0.6;
}

/* Narrower checkbox cell than the venue/show menus use. */
.tag-row .filter-overlay-option-checkbox {
	padding: 0 20px;
}

/* The --row-gap inherited from `.show-card.filter-overlay-option` separates rows,
   so on the last one it is just a trailing gap. Dropping it lets the list end
   flush the way it starts. */
.picker-list .tag-row:last-child {
	margin-bottom: 0;
}


/* Filter options reuse .show-card; only the button-specific resets,
   sizing for menu context, and the selected indicator live here.
   Selectors chain .show-card to beat .show-card's later source position
   on padding/background overrides (specificity 0,2,0 vs 0,1,0). */
.show-card.filter-overlay-option {
	width: 100%;
	color: var(--text-primary);
	border: 1px solid var(--border-color);
	font-size: 16px;
	font-family: inherit;
	text-align: left;
	padding: 12px var(--card-pad-x);
	margin-bottom: var(--row-gap);
}

.show-card.filter-overlay-option .show-title {
	font-size: 15px;
}

.show-card.filter-overlay-option .show-description {
	font-size: 12px;
}

/* +2 scale-up over the show-card baseline so credits, tags, count, runtime,
   and venue abbr stay proportional. */
.show-card.filter-overlay-option .show-credits {
	font-size: 12px;
}

.show-card.filter-overlay-option .show-tag {
	font-size: 11px;
}

.show-card.filter-overlay-option .show-time {
	font-size: 13px;
	min-width: 70px;
}

.show-card.filter-overlay-option .show-runtime {
	font-size: 11px;
}

.show-card.filter-overlay-option .venue-abbr {
	font-size: 16px;
}

/* All-row only (show-filter menu): two subtext lines stacked below the
   title. Showtimes count (secondary tone, the primary stat) and theater/
   venue count (tertiary tone, the quieter supplement). Margin-top on the
   first line spaces it from the title; the second sits flush below via
   normal flow. */
.show-card.filter-overlay-option .show-all-showtimes {
	font-size: 12px;
	color: var(--text-secondary);
	margin-top: 2px;
}

.show-card.filter-overlay-option .show-all-venues {
	font-size: 12px;
	color: var(--text-tertiary);
}

/* All-row (both venue and show menus): visually distinguish the "select
   all" entry from the regular rows by centering its text. The venue All
   row keeps its leading stale-emoji column (the worst-case across all
   venues), so the .show-info text centers within the remaining row width;
   the show All row has no left column so the centering is symmetric. */
.show-card.filter-overlay-option-all {
	text-align: center;
}

.show-card.filter-overlay-option-selected {
	background: var(--button-bg);
	border: 1px solid var(--button-border);
}

/* Rows visible only because the user has them checked, even though they don't
   match the current search text and/or fall outside the date-range filter.
   Dim the row so it's distinguishable from "naturally in scope" rows. */
.show-card.filter-overlay-option.out-of-scope {
	opacity: 0.6;
}

/* Multi-select checkbox on each non-"All" filter option. Bordered like its own
   segment within the row: top/right/bottom shared with the row's outer border
   (via negative margins + align-self: stretch); left edge is a divider line.
   Horizontal padding gives the hit area extra width beyond the mark itself. */
.filter-overlay-option-checkbox {
	flex-shrink: 0;
	align-self: stretch;
	margin: -12px -14px -12px 14px;
	padding: 0 30px;
	display: flex;
	align-items: center;
	justify-content: center;
	border-left: 1px solid var(--border-color);
}

.filter-overlay-option-selected .filter-overlay-option-checkbox {
	border-left-color: var(--button-border);
}

.filter-overlay-option-checkbox-mark {
	width: 22px;
	height: 22px;
	border: 2px solid var(--button-border-strong);
	border-radius: 4px;
	display: flex;
	align-items: center;
	justify-content: center;
	color: transparent;
	font-size: 16px;
	font-weight: 700;
	line-height: 1;
}

.filter-overlay-option-selected .filter-overlay-option-checkbox-mark {
	background: var(--text-primary);
	border-color: var(--text-primary);
	color: var(--bg-color);
}

/* Main container */
/* Shows container, scrolls with the document (no own overflow). No top padding:
   the buffer is baked into .date-group-header's own padding-top instead, so it
   stays visible when the header is stuck at viewport top. Sides and bottom frame
   the list at --filter-bar-gap, matching the bar above. */
.category-container {
	padding: 0 var(--filter-bar-gap) var(--filter-bar-gap);
}

/* Date group headers, sticky iOS-style section headers. Tappable to scroll
   that day's content to the top of the viewport. Symmetric vertical padding so
   the date text is vertically centered in the header; the 11px also gives the
   text breathing room above when the header is stuck at viewport top, without
   it the text would butt right up against the viewport edge. */
.date-group-header {
	font-size: 12px;
	font-weight: 600;
	color: var(--text-secondary);
	padding: var(--date-header-pad-y) calc(var(--filter-bar-gap) + var(--card-pad-x));
	text-transform: uppercase;
	letter-spacing: 0.5px;
	border-bottom: 1px solid var(--filter-bar-border);
	margin: 0 calc(var(--filter-bar-gap) * -1) calc(var(--filter-bar-gap) - var(--row-gap));
	position: sticky;
	top: 0;
	background: var(--bg-color);
	z-index: 10;
	cursor: pointer;
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 12px;
}

/* Every header after the first fades its background in across its top padding,
   so the header it is sliding over dissolves instead of being cut off. The fade
   sits on the header's OWN background rather than in a strip above it: there are
   only --row-gap px of clear space above a header, so a taller strip would wash
   over the bottom of the previous group's last card, permanently and at rest.
   Stopping at --date-header-pad-y is what keeps it safe. Every header sticks at
   top 0, so the one underneath is pinned in exactly the same place; what shows
   through the transparent part is its blank top padding, the same --bg-color
   fading in, while its text stays under solid background. Cards passing behind
   the band do show through, which is the same effect and wanted.
   First header excluded: nothing is ever behind it. */
.date-group-header:not(:first-child) {
	background: linear-gradient(to bottom, transparent, var(--bg-color) var(--date-header-pad-y));
}

/* Per-day movie count, right-aligned within .date-group-header. Smaller and
   lighter than the day label so the eye lands on the label first; the count
   is supporting info. Header itself is --text-secondary, so the count uses
   --text-tertiary to step the visual hierarchy down one more notch. Strips
   uppercase + letter-spacing (those are label-only). */
.date-group-header .day-count {
	font-size: 0.85em;
	font-weight: 400;
	text-transform: none;
	letter-spacing: 0;
	color: var(--text-tertiary);
}

/* Show cards. Rows sit at --row-gap, the same distance the menu's filter options
   use, so the two lists read alike. The space UNDER a date header is the wider
   --filter-bar-gap instead, since that boundary frames a group rather than
   separating two items; `.date-group-header` makes up the difference. */
.shows-list {
	display: flex;
	flex-direction: column;
	gap: var(--row-gap);
}

.show-card {
	display: flex;
	background: var(--card-bg);
	border-radius: 6px;
	padding: 8px var(--card-pad-x);
	gap: 10px;
	align-items: center;
	cursor: pointer;
	transition: transform 0.1s ease, background 0.1s ease;
}

.show-card.daytime {
	opacity: 0.55;
}

.show-card.sold-out {
	opacity: 0.3;
}

/* Started within the last hour: still listed, see PAST_SHOW_GRACE_MS. */
.show-card.past {
	opacity: 0.3;
}

/* Collapse a past show to its time and title: the description, credits and
   runtime are the bulk of a card's height, and a show already started is
   reference, not a choice. The details modal still carries all three, since it
   reads the data attributes rather than these elements. */
.show-card.past .show-description,
.show-card.past .show-credits,
.show-card.past .show-runtime {
	display: none;
}

.show-card:active {
	background: var(--press-bg);
	transform: scale(0.99);
}

.venue-abbr {
	font-size: 14px;
	font-weight: 700;
	min-width: 42px;
	text-align: center;
	flex-shrink: 0;
}

/* Stale-flag emoji at the leftmost edge of a venue-menu row (when the venue's
   lastUpdated is older than 24h). Quick visual indicator without reading text. */
.show-stale-emoji {
	flex-shrink: 0;
	font-size: 16px;
	text-align: center;
}

.show-card.academy .venue-abbr {
	color: var(--academy-color);
}

.show-card.bagdad .venue-abbr {
	color: var(--bagdad-color);
}

.show-card.cinema21 .venue-abbr {
	color: var(--cinema21-color);
}

.show-card.cinemagic .venue-abbr {
	color: var(--cinemagic-color);
}

.show-card.cinemark-ep .venue-abbr {
	color: var(--cinemark-ep-color);
}

.show-card.hollywood .venue-abbr {
	color: var(--hollywood-color);
}

.show-card.kennedy .venue-abbr {
	color: var(--kennedy-color);
}

.show-card.laurelhurst .venue-abbr {
	color: var(--laurelhurst-color);
}

.show-card.livingroom .venue-abbr {
	color: var(--livingroom-color);
}

.show-card.moreland .venue-abbr {
	color: var(--moreland-color);
}

.show-card.omsi .venue-abbr {
	color: var(--omsi-color);
}

.show-card.tomorrow .venue-abbr {
	color: var(--tomorrow-color);
}

.show-card.regal-ft .venue-abbr {
	color: var(--regal-ft-color);
}

.show-card.regal-lc .venue-abbr {
	color: var(--regal-lc-color);
}

.show-card.regal-pp .venue-abbr {
	color: var(--regal-pp-color);
}

.show-card.stjohns .venue-abbr {
	color: var(--stjohns-color);
}

.show-card.stjohnstwin .venue-abbr {
	color: var(--stjohnstwin-color);
}

.show-card.studioone .venue-abbr {
	color: var(--studioone-color);
}

.show-card.deadcomics .venue-abbr {
	color: var(--deadcomics-color);
}

.show-card.helium .venue-abbr {
	color: var(--helium-color);
}

.show-card.kickstand .venue-abbr {
	color: var(--kickstand-color);
}

.show-time {
	font-size: 11px;
	font-weight: 600;
	color: var(--text-secondary);
	min-width: 60px;
	text-align: center;
	line-height: 1.2;
}

.show-runtime {
	font-size: 9px;
	font-weight: 400;
	color: var(--text-tertiary);
	margin-top: 1px;
}

/* Show-menu only: distinct-venue count below the showtime count in .show-time.
   Inherits color from .show-time (var(--text-secondary)). */
.show-venue-count {
	font-size: 11px;
	font-weight: 400;
	margin-top: 1px;
}

.show-info {
	flex: 1;
	min-width: 0;
}

.show-title {
	font-size: 13px;
	font-weight: 500;
	white-space: nowrap;
	overflow: hidden;
	-webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
	mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
}

.show-tag {
	font-size: 9px;
	font-weight: 600;
	background: var(--tag-fill);
	color: var(--tag-on-fill);
	padding: 1px 5px;
	border-radius: 3px;
	/* The separating space renders at ~4px, so this carries the other half of the
	   8px gap the margin used to hold on its own. */
	margin-left: 4px;
	vertical-align: 1px;
	/* A pill is one unit: it wraps to the next line whole rather than splitting
	   its own text across two pill backgrounds. */
	white-space: nowrap;
}

.show-tag-year {
	background: transparent;
	font-weight: 500;
	color: var(--text-secondary);
	padding: 0;
}

/* No margin-left of its own, so the FIRST genre sits at `.show-tag`'s 4px, the
   same as the first tag and the rating. */
.show-tag-genre {
	background: transparent;
	font-weight: 400;
	color: var(--text-tertiary);
	padding: 0;
}

/* Consecutive genres drop the pill margin and let the separating space alone hold
   the gap, so it reads as spacing around the dot rather than as a full pill gap. */
.show-tag-genre + .show-tag-genre {
	margin-left: 0;
}

/* In the details modal the genres open the line, so the leading pill margin is a
   spurious indent: `.details-meta` is centered, and a margin inside the centered
   line pushes the whole run off center by half of it. */
.details-meta .show-tag-genre:first-child {
	margin-left: 0;
}

.show-tag-genre + .show-tag-genre::before {
	content: '\b7';
	margin-right: 4px;
	vertical-align: 0;
}

.show-tag-rating {
	background: transparent;
	color: var(--text-secondary);
	border: 1px solid var(--button-border-strong);
	border-radius: 3px;
	padding: 0 3px;
}

/* Genre PILL (the removable chip under the search row, not the bare genre text
   in a list). Carries `.show-tag` so it inherits the pill font-size at every
   breakpoint; hardcoding 9px here made it the one pill that stopped matching
   its neighbours above 1200px. Only fill, weight and border differ: one step
   stronger than the list's tertiary/hairline pair, which was too faint to read,
   and a lighter border than a rating's so the two stay apart. Padding matches
   `.show-tag-rating` so every pill is the same height: 1px of border top and
   bottom where the filled tag has 1px of padding. */
/* No padding of its own: it takes `.search-pills .search-pill`s, so all three
   kinds are the same size and only fill, weight and border tell them apart. */
.search-pills .search-pill-genre {
	font-weight: 400;
	background: transparent;
	color: var(--text-secondary);
	border: 1px solid var(--button-border);
}

.show-description {
	font-size: 10px;
	color: var(--text-secondary);
	line-height: 1.3;
	margin-top: 2px;
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

.show-credits {
	font-size: 10px;
	color: var(--credit-text);
	margin-top: 2px;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* Per-venue address (street + ZIP), below the count sub in the venue menu. */
.show-venue-address {
	font-size: 12px;
	color: var(--text-tertiary);
	margin-top: 2px;
}

/* Per-venue "Updated X" line in the venue menu (when timestamps differ across
   venues; otherwise the .filter-overlay-footer covers it). */
.show-last-updated {
	font-size: 11px;
	color: var(--text-tertiary);
	margin-top: 2px;
}

/* Modal overlay (the dim backdrop) with a very subtle backdrop blur so
   the show list behind is softened, not just dimmed. No `overscroll-behavior:
   contain` so a touch+drag on the backdrop chains to body scroll, which fires
   a window scroll event the close handler listens for (scroll-to-dismiss).
   Internal `.modal-box` scrolling (`overflow-y: auto`) stays contained to
   the modal, so reading long descriptions doesn't dismiss. */
/* Visible / hidden states are differentiated by opacity (overlay
   backdrop fade) + transform/opacity (modal scale + fade) + pointer-
   events (so the invisible overlay doesn't catch taps while it's
   hidden, even though it remains in the DOM at position: fixed). Same
   CSS rules drive both entry and exit by virtue of `.hidden` being the
   only state difference, opening a modal scales up + fades in, closing
   scales down + fades out, both over 0.2s. */
.modal-overlay {
	position: fixed;
	inset: 0;
	background: var(--modal-backdrop);
	-webkit-backdrop-filter: blur(2px);
	backdrop-filter: blur(2px);
	display: flex;
	align-items: center;
	justify-content: center;
	/* Above the filter menu's sticky bar (z-index 100) so the tag picker can
	   open over the menu. `.filter-overlay` itself sets no z-index, so that bar
	   competes at the root. */
	z-index: 200;
	opacity: 1;
	pointer-events: auto;
	/* `will-change` keeps the overlay on its own compositor layer so the
	   opacity / backdrop-filter transitions run on the GPU thread,
	   guards against iOS Safari deferring them during momentum scroll. */
	will-change: opacity, backdrop-filter;
	transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1),
		-webkit-backdrop-filter 0.25s cubic-bezier(0.4, 0, 0.2, 1),
		backdrop-filter 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

.modal-overlay.hidden {
	opacity: 0;
	pointer-events: none;
	-webkit-backdrop-filter: blur(0);
	backdrop-filter: blur(0);
}

/* The modal never scrolls: `.details-scroll` does in details mode and
   `.picker-list` in tag mode. Any descendant of a scroll container
   translates with elastic overscroll, so a scrolling modal dragged the close X
   along during the bounce whatever its positioning. A flex column in both modes
   lets whichever child is the scroller take the leftover height. */
.modal-box {
	position: relative;
	display: flex;
	flex-direction: column;
	background: var(--card-bg);
	border-radius: 16px;
	padding: 28px;
	text-align: center;
	/* Without a width the modal sizes to its content, so a short description or
	   a small tag list renders a narrow box. This pins every modal to the same
	   width; max-width still caps it. */
	width: 100%;
	max-width: 350px;
	max-height: 88vh;
	overflow: hidden;
	box-shadow: 0 8px 32px var(--modal-shadow);
	border: 1px solid var(--border-color);
	transform: scale(1);
	opacity: 1;
	transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
		opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

.modal-overlay.hidden .modal-box {
	transform: scale(0.9);
	opacity: 0;
}

/* Details-modal vertical rhythm. The modal reads as a stack of zones, and the
   spacing is what marks them: lines inside a zone sit at --details-line-gap,
   every boundary between zones is --details-zone-gap. Zones, top to bottom:
   venue+address / title+meta+tags / datetime / tickets / links / separator /
   description / credits. Every margin in the modal comes from one of these two
   tokens, so a spacing change is one edit and the zones stay even. Tag mode is
   the exception: it is a filter control, not a stack of detail zones, so its
   rows measure against --filter-bar-gap and match the bar they opened from. */
.modal-box {
	--details-zone-gap: 16px;
	--details-line-gap: 4px;
	/* The tickets button is the modal's primary action, so its zone is set off
	   with a wider gap. Derived from the base gap rather than hardcoded, so
	   changing --details-zone-gap still moves the whole modal together. */
	--details-tickets-gap: calc(var(--details-zone-gap) * 1.5);
}

.details-venue {
	font-size: 12px;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 1px;
	margin-bottom: var(--details-line-gap);
}

/* The venue name links to the venue's site, the address opens Apple Maps, the
   title links to the venue's page for the film, and each credit name opens an
   IMDb search. All keep their surrounding appearance: the brand color lives on
   the parent's venue class, so the anchor inherits rather than overriding it.
   These four are the modal's only PLAIN-TEXT links; everything else clickable in
   there carries its own affordance (filled tag pills, the bordered rating, the
   IMDb/TMDB/YouTube buttons, the tickets button). */
.details-venue a,
.details-venue-address a,
.details-title a,
.details-credits a {
	color: inherit;
	text-decoration: none;
}

/* Hover reveals the affordance those four do without at rest. Genres are the one
   other plain-text link in the modal (`.show-tag-genre` is transparent and
   padding-free) and are deliberately left out; they live in `#details-meta`, so
   the selector list above already excludes them.
   `hover: hover` keeps this off touch devices: iOS applies `:hover` on tap and
   leaves it stuck until something else is tapped, so a phone would otherwise
   show an underline that outlives the tap. */
@media (hover: hover) {
	.details-venue a:hover,
	.details-venue-address a:hover,
	.details-title a:hover,
	.details-credits a:hover {
		text-decoration: underline;
	}
}

/* Street + ZIP shown directly under the venue name. Hidden when empty
   (`:empty`, `textContent = ''` leaves no children so the pseudo
   applies) so the layout still works for any venue without an address
   configured. */
.details-venue-address {
	font-size: 11px;
	color: var(--text-tertiary);
	margin-bottom: var(--details-zone-gap);
}

.details-venue-address:empty {
	display: none;
}

.details-venue.academy {
	color: var(--academy-color);
}

.details-venue.bagdad {
	color: var(--bagdad-color);
}

.details-venue.cinema21 {
	color: var(--cinema21-color);
}

.details-venue.cinemagic {
	color: var(--cinemagic-color);
}

.details-venue.cinemark-ep {
	color: var(--cinemark-ep-color);
}

.details-venue.hollywood {
	color: var(--hollywood-color);
}

.details-venue.kennedy {
	color: var(--kennedy-color);
}

.details-venue.laurelhurst {
	color: var(--laurelhurst-color);
}

.details-venue.livingroom {
	color: var(--livingroom-color);
}

.details-venue.moreland {
	color: var(--moreland-color);
}

.details-venue.omsi {
	color: var(--omsi-color);
}

.details-venue.tomorrow {
	color: var(--tomorrow-color);
}

.details-venue.regal-ft {
	color: var(--regal-ft-color);
}

.details-venue.regal-lc {
	color: var(--regal-lc-color);
}

.details-venue.regal-pp {
	color: var(--regal-pp-color);
}

.details-venue.stjohns {
	color: var(--stjohns-color);
}

.details-venue.stjohnstwin {
	color: var(--stjohnstwin-color);
}

.details-venue.studioone {
	color: var(--studioone-color);
}

.details-venue.deadcomics {
	color: var(--deadcomics-color);
}

.details-venue.helium {
	color: var(--helium-color);
}

.details-venue.kickstand {
	color: var(--kickstand-color);
}

.details-title {
	font-size: 14px;
	font-weight: 600;
	color: var(--text-primary);
	margin-bottom: var(--details-line-gap);
}

.details-datetime {
	font-size: 11px;
	color: var(--text-secondary);
	/* Carried on the date itself rather than on the line above it, so the gap
	   holds whether or not the tags and meta lines are present. */
	margin-top: var(--details-zone-gap);
	margin-bottom: var(--details-tickets-gap);
}

.details-datetime:empty {
	display: none;
}

/* Same pairing as the list card: the time in --text-secondary (.show-time),
   the runtime a step back in --text-tertiary (.show-runtime). */
.details-runtime {
	color: var(--text-tertiary);
}

.details-meta {
	font-size: 10px;
	color: var(--text-secondary);
	margin-bottom: var(--details-line-gap);
}

.details-meta:empty {
	display: none;
}

.details-tags {
	margin-bottom: 0;
}

.details-tags:empty {
	display: none;
}

/* Pills normally trail a title, so they carry a left margin. On these lines a
   pill comes first and the margin would read as an indent. */
.details-meta > .show-tag:first-child,
.details-tags > .show-tag:first-child {
	margin-left: 0;
}

.show-tag-link {
	cursor: pointer;
}

.details-description {
	font-size: 11px;
	color: var(--text-secondary);
	line-height: 1.4;
	margin-top: var(--details-tickets-gap);
	padding-top: var(--details-zone-gap);
	text-align: left;
}

.details-description:empty {
	display: none;
}

.details-credits {
	font-size: 10px;
	color: var(--credit-text);
	/* Half gap: the credits are a continuation of the description block, not a
	   zone of their own. The separator case below keeps the full gap, since
	   there the credits ARE the block under the line. */
	margin-top: calc(var(--details-zone-gap) / 2);
	text-align: left;
}

.details-credits:empty {
	display: none;
}

.details-description:empty + .details-credits {
	margin-top: var(--details-tickets-gap);
	padding-top: var(--details-zone-gap);
}

/* The zone line above the description, fading out at both ends instead of
   butting into the modal's padding. Painted as a background rather than a
   border so it needs no pseudo-element and no positioning context, and it lands
   on the same pixel row the border did: the background box includes the border
   edge. Whichever element is first under the tickets/links zone carries it, so
   a film with no description hands the line to its credits. */
.details-description,
.details-description:empty + .details-credits {
	background-image: linear-gradient(to right, transparent, var(--border-color) 20%, var(--border-color) 80%, transparent);
	background-repeat: no-repeat;
	background-size: 100% 1px;
	background-position: top;
}

/* Flex row rather than inline-blocks with margins: `gap` spaces the buttons
   horizontally AND spaces wrapped rows, without any margin that would also
   read as the gap below the tickets button. That gap belongs to the tickets
   element alone, so wrapping can never change it. */
.details-links {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 10px;
}

.details-imdb, .details-tmdb, .details-youtube {
	display: inline-block;
	padding: 3px 6px;
	font-size: 13px;
	font-weight: 900;
	font-family: Arial, sans-serif;
	letter-spacing: -0.5px;
	border-radius: 4px;
	text-decoration: none;
}

.details-imdb {
	color: #161616;       /* IMDb logo near-black, per official brand toolkit SVG from https://brand.imdb.com */
	background: #f5c518;  /* IMDb gold */
}

.details-tmdb {
	color: #0d253f;       /* TMDB primary dark navy, per https://www.themoviedb.org/about/logos-attribution */
	background: #01b4e4;  /* TMDB secondary light blue */
}

.details-youtube {
	color: #fff;          /* play-triangle white, per YouTube's logo SVG */
	background: #ff0033;  /* YouTube play-button red, per the official SVG on https://www.youtube.com */
}

.details-link-active {
	outline: 2px solid var(--link-active-outline);
	outline-offset: 1px;
}

#details-tickets {
	background: #fff;
	/* Theme-independent light-grey border. The QR card's background is
	   hardcoded white (QR scan reliability), so a theme-aware border
	   that flips to #333 in dark mode would read as a stark dark line
	   against the white card, much more prominent than the modal's
	   border looks in dark mode (where #333 on #1e1e1e is low-contrast
	   and subtle). Using #dadada always keeps the visual weight similar
	   to the modal border in both themes: in light mode it literally
	   matches --border-color; in dark mode it's a different value but
	   the same low-contrast-against-its-background subtlety. */
	border: 1px solid #dadada;
	border-radius: 8px;
	padding: 12px;
	/* The gap above comes from the datetime's bottom margin; the gap below lives
	   here so it holds for both the Buy Tickets button and the kiosk QR card. */
	margin-top: 0;
	margin-bottom: var(--details-tickets-gap);
	display: block;
	width: 160px;
	height: 160px;
	margin-left: auto;
	margin-right: auto;
}

#details-tickets svg {
	display: block;
	margin: 0 auto;
}

/* When #details-tickets is holding a Buy Tickets button (non-kiosk view)
   instead of a QR SVG, drop the white box dimensions so the button can size
   to its content. Also drop the border so the bare Buy Tickets button
   isn't framed by a phantom card outline. */
#details-tickets:has(.tickets-button) {
	background: transparent;
	border: none;
	width: auto;
	height: auto;
	padding: 0;
}

/* Ticket-stub shape: square corners with a semicircular notch punched into each
   side. Built as two stacked masked layers rather than a border, because a real
   `border` is drawn on the rectangle and would cut straight across the notch
   openings. The outer element paints the outline colour and is masked at the
   notch radius; the inner element paints the fill and is masked 1px LARGER and
   1px further out (`at -1px`), so a 1px band of the outer colour survives along
   each arc. That is what carries the outline around the curve.
   Notch radius is in `em` so it tracks the responsive font-size overrides below
   instead of shrinking relatively as the button grows. Where mask-composite is
   unsupported the shape degrades to a plain rectangle, which is acceptable. */
.tickets-button {
	--notch: 0.56em;
	display: block;
	width: fit-content;
	/* No vertical margin of its own: the datetime above and the link row below
	   each carry the zone gap. */
	margin: 0 auto;
	padding: 1px;
	background: var(--button-border);
	font-size: 16px;
	text-decoration: none;
	-webkit-mask-image: radial-gradient(circle var(--notch) at 0 50%, transparent 98%, #000 100%), radial-gradient(circle var(--notch) at 100% 50%, transparent 98%, #000 100%);
	-webkit-mask-composite: source-in;
	mask-image: radial-gradient(circle var(--notch) at 0 50%, transparent 98%, #000 100%), radial-gradient(circle var(--notch) at 100% 50%, transparent 98%, #000 100%);
	mask-composite: intersect;
}

.tickets-button-inner {
	--notch: calc(0.56em + 1px);
	display: block;
	padding: 17px 31px;
	background: var(--button-bg);
	color: var(--text-primary);
	text-align: center;
	font-weight: 600;
	-webkit-mask-image: radial-gradient(circle var(--notch) at -1px 50%, transparent 98%, #000 100%), radial-gradient(circle var(--notch) at calc(100% + 1px) 50%, transparent 98%, #000 100%);
	-webkit-mask-composite: source-in;
	mask-image: radial-gradient(circle var(--notch) at -1px 50%, transparent 98%, #000 100%), radial-gradient(circle var(--notch) at calc(100% + 1px) 50%, transparent 98%, #000 100%);
	mask-composite: intersect;
}

/* Loading and error states */
/* Shared style for full-width status messages: the "Loading…", "No
   Shows…", "Error…" states in the shows-container, plus the menu's
   empty-state ("No Matches for …"). Large + bold + centered so each
   reads as an obvious state, tertiary text color so they feel like
   one family, generous vertical padding so they anchor at the visual
   center of the area they occupy. */
.loading, .error, .no-shows, .filter-overlay-empty {
	text-align: center;
	font-size: 20px;
	font-weight: 700;
	letter-spacing: 0.3px;
	padding: 60px 20px;
	color: var(--text-tertiary);
}

/* The current search shown under a no-results message: the words still in the
   field, the terms lifted out into pills, and below a gap the raw field content
   when it still differs from those two. Much smaller and lighter than the message above it, since it
   answers a question only asked when something looks wrong. Unlabelled and
   centered; the three lines read as one query in its parts. */
.no-shows-query {
	margin-top: 14px;
	font-size: 13px;
	font-weight: 400;
	letter-spacing: normal;
	line-height: 1.6;
	text-align: center;
	overflow-wrap: anywhere;
}

/* The raw line only shows while the field holds something the two lines above
   it do not, i.e. an edit not yet committed by Enter or blur, so it sits apart
   from them. Pre-wrap because runs of spaces are exactly the kind of
   uncommitted state worth seeing, and monospace so a run of them is countable
   rather than just wide. */
.no-shows-query-raw {
	margin-top: 8px;
	white-space: pre-wrap;
	font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* Inline lazy-load indicator sits at the bottom of the rendered list.
   Deliberately smaller and darker than the full-width status messages
   above, it's transient and only shows while the lazy-render queue is
   non-empty, so it should feel like a subtle hint, not a prominent
   state. Padding-top is 8px larger than padding-bottom to compensate
   for the parent `.category-container`'s own `padding-bottom: 8px`
   below, net visual breathing room above and below the text is then
   equal at 28px. */
.scroll-sentinel {
	text-align: center;
	font-size: 14px;
	font-weight: 700;
	color: var(--text-tertiary);
	opacity: 0.55;
	padding: 28px 20px 20px;
}

/* .scroll-sentinel doubles as the lazy-load progress indicator + the
   IntersectionObserver target. It lives at the bottom of the shows-list
   and gets `.remove()`'d when the lazy-render queue empties, so its
   presence naturally communicates "there's more to come" and its absence
   communicates "you've reached the end." */

.error {
	color: #ff6b6b;
}

/* Sentinel anchored at the top of the document, observed by the scroll
   buttons' shared IntersectionObserver to gate the "past top" signal
   (both buttons appear/disappear together at the top). Height of 200px
   means the buttons stay hidden until the user has scrolled past that
   distance, keeps them out of the way for near-top scrolls without
   requiring a full viewport of scrolling on large screens. Fixed pixel
   value (not vh) so the threshold doesn't grow with viewport height.
   position: absolute keeps it out of normal flow; pointer-events: none +
   no background make it invisible. The mirrored bottom-edge check (for
   hiding the down arrow) is done via scroll math instead, a
   `bottom: 0` absolute sentinel can't reliably anchor to the document
   bottom (the Initial Containing Block is viewport-sized). */
#page-top-sentinel {
	position: absolute;
	top: 0;
	left: 0;
	width: 1px;
	height: 200px;
	pointer-events: none;
}

/* Floating "scroll to top" / "scroll to next day" buttons, fixed
   horizontally centered, anchored to the top/bottom of the viewport. The
   scroll-top button sits just below the sticky header (the `.date-group-header`
   at `top: 50px` in shows mode, or the taller two-row overlay filter-bar in
   menu mode, overridden via `body.menu-open .scroll-top` below). The
   scroll-bottom button has no sticky obstruction at the bottom and sits
   close to the edge (`bottom: 20px`) in all modes. Both share visibility:
   visible only once the document has scrolled past `#page-top-sentinel`
   (200px from the top) AND the user has scrolled within the last 5s.
   Touch-friendly 48x48 hit target. Translucent background + backdrop-filter
   blur for a frosted-glass effect over whatever content sits behind it. */
.scroll-top,
.scroll-bottom {
	position: fixed;
	left: 50%;
	transform: translateX(-50%);
	z-index: 50;
	width: 48px;
	height: 48px;
	border-radius: 50%;
	background: var(--scroll-arrow-bg);
	-webkit-backdrop-filter: blur(10px);
	backdrop-filter: blur(10px);
	color: var(--text-primary);
	border: 1px solid var(--scroll-arrow-border);
	font-size: 24px;
	font-weight: 700;
	font-family: inherit;
	line-height: 1;
	cursor: pointer;
	opacity: 0;
	visibility: hidden;
	transition: opacity 0.2s ease, visibility 0.2s ease;
}

.scroll-top {
	top: 50px;
}

.scroll-bottom {
	bottom: 20px;
}

.scroll-top.visible,
.scroll-bottom.visible {
	opacity: 1;
	visibility: visible;
}

/* Menu mode: the overlay's sticky filter-bar is two rows (action buttons +
   search input) and thus taller. Push the scroll-top button down enough to
   clear it. The scroll-bottom button is unaffected (nothing sticky at the
   bottom). */
body.menu-open .scroll-top {
	top: 120px;
}

/* Show-filter menu open: the date-range segmented control adds a third row
   between the header and search rows (hidden in venue mode via the
   data-filter-type="venue" rule). Push the scroll-top down by another ~50px
   to clear the extra row. The :has() selector lets a body-level rule react
   to the filter-overlay's data-filter-type attribute. */
body.menu-open:has(#filter-overlay[data-filter-type="show"]) .scroll-top {
	top: 170px;
}

/* Kiosk + menu open: the search input row is hidden (no keyboard on kiosk),
   so the filter-bar collapses back to a single row, shift the scroll-top
   button up accordingly. */
body.kiosk.menu-open .scroll-top {
	top: 70px;
}

/* Kiosk + show-filter menu: search row hidden but date-range row visible
   (date-range doesn't need a keyboard), so 2 rows total. */
body.kiosk.menu-open:has(#filter-overlay[data-filter-type="show"]) .scroll-top {
	top: 120px;
}

/* Modal open: hide both scroll buttons so they don't float over
   the modal backdrop. Uses `display: none` rather than fighting the JS
   `.visible` toggle, the scroll-to-dismiss gesture still emits scroll
   events that would otherwise keep `recentlyScrolled` true and the
   buttons visible. */
body.modal-open .scroll-top,
body.modal-open .scroll-bottom {
	display: none;
}

/* Stack the main filter bar's row 1 on narrow screens. Sub-row 1 has the
   segmented toggle filling the left and Show All at its natural width on the
   right; venue and show buttons get their own full-width sub-rows below. The
   search input (sibling of `.filter-bar-row`) stays as the bottom row of the
   filter-bar regardless of viewport. */
@media (max-width: 750px) {
	body > .filter-bar > .filter-bar-row:not(.search-row) {
		display: grid;
		grid-template-columns: auto 1fr auto;
		grid-template-areas:
			"theme category showAll"
			"venue venue venue"
			"show show show";
	}

	body > .filter-bar > .filter-bar-row:not(.search-row) > #theme-toggle { grid-area: theme; }
	body > .filter-bar > .filter-bar-row:not(.search-row) > #category-filter { grid-area: category; }
	body > .filter-bar > .filter-bar-row:not(.search-row) > #venue-filter { grid-area: venue; }
	body > .filter-bar > .filter-bar-row:not(.search-row) > #show-filter { grid-area: show; }
	body > .filter-bar > .filter-bar-row:not(.search-row) > #reset-filters { grid-area: showAll; }
}

/* Responsive adjustments for very small screens */
@media (max-width: 400px) {
	body {
		font-size: 12px;
	}


	.filter-bar button,
	.picker-clear {
		font-size: 14px;
		padding: 8px 6px;
	}

	:root {
		--segment-font-size: 14px;
		--filter-bar-gap: 6px;
		--card-pad-x: 8px;
	}

	.segmented-control .segment {
		padding: 8px 6px;
	}

	.show-card {
		padding: 6px var(--card-pad-x);
	}

	.venue-abbr {
		font-size: 15px;
		min-width: 45px;
	}

	.show-time {
		font-size: 10px;
		min-width: 50px;
	}

	.show-title {
		font-size: 12px;
	}

	.show-credits {
		font-size: 9px;
	}

	.show-description {
		font-size: 9px;
	}

	.show-runtime {
		font-size: 8px;
	}

	.date-group-header {
		font-size: 11px;
		--date-header-pad-y: 5px;
	}
}

/* For 3.5" displays (typically 480x320) */
@media (max-height: 320px) {

	.filter-bar button,
	.picker-clear {
		font-size: 12px;
		padding: 6px 4px;
	}

	:root {
		--segment-font-size: 12px;
		--filter-bar-gap: 4px;
		--card-pad-x: 6px;
	}

	.segmented-control .segment {
		padding: 6px 4px;
	}

	.show-card {
		padding: 4px var(--card-pad-x);
		gap: 6px;
	}

	.venue-abbr {
		font-size: 14px;
		min-width: 18px;
	}

	.show-time {
		font-size: 9px;
		min-width: 45px;
	}

	.show-title {
		font-size: 11px;
	}

	.show-credits {
		font-size: 8px;
	}

	.show-description {
		font-size: 8px;
		-webkit-line-clamp: 1;
	}

	.show-runtime {
		font-size: 7px;
	}

	.date-group-header {
		font-size: 10px;
		--date-header-pad-y: 3px;
	}

}

/* Scale text + padding UP for larger viewports. The base sizes (above) are
   tuned for the kiosk Pi touchscreen / small phone; phone, tablet, and
   typical laptop sizes all use the base. The breakpoints kick in for
   external monitors and large desktops, where the baseline reads too
   small. Two breakpoints with moderate scaling (~1.15x at 1200px,
   ~1.3x at 1600px). Padding on key containers (.show-card, .filter-bar,
   .date-group-header) grows with text so things don't feel cramped.
   body.kiosk users almost always have small viewports so these queries
   don't apply in practice; if a kiosk ever runs on a large display we
   can add body.kiosk opt-outs.

   Selectors mirror the @media (max-width: 400px) block above (which
   scales DOWN for very small screens). Same prominent surfaces, just
   moving in the opposite direction. Filter-overlay rows and details
   modal added because both become major surfaces at large viewports.
   Some baseline sizes left alone: .scroll-sentinel (deliberately subtle
   inline indicator), .filter-overlay-search (at 16px iOS zoom-prevent
   floor on small; scales up here for legibility on big screens). */

@media (min-width: 1200px) {
	body {
		font-size: 16px;
	}


	.filter-bar button,
	.picker-clear {
		font-size: 18px;
		padding: 12px 14px;
		min-width: 105px;
	}

	:root {
		--segment-font-size: 18px;
		--filter-bar-gap: 9px;
		--card-pad-x: 12px;
	}

	.segmented-control .segment {
		padding: 12px 14px;
	}

	/* Vertical padding tracks `.filter-bar button` above. The search row hides
	   its two buttons while the placeholder shows, so a mismatch here is only
	   visible as the row changing height the moment the user types. */
	.filter-overlay-search {
		font-size: 18px;
		padding: 12px 12px;
	}

	.show-card {
		padding: 9px var(--card-pad-x);
	}

	.venue-abbr {
		font-size: 16px;
		min-width: 48px;
	}

	.show-stale-emoji {
		font-size: 18px;
	}

	.show-time {
		font-size: 13px;
	}

	.show-runtime {
		font-size: 10px;
	}

	.show-venue-count {
		font-size: 13px;
	}

	.show-title {
		font-size: 15px;
	}

	.show-tag {
		font-size: 10px;
	}

	.search-pills .search-pill {
		font-size: 15px;
	}

	.show-description {
		font-size: 12px;
	}

	.show-credits {
		font-size: 12px;
	}

	.show-venue-address {
		font-size: 14px;
	}

	.show-last-updated {
		font-size: 13px;
	}

	.date-group-header {
		font-size: 14px;
		--date-header-pad-y: 12px;
	}

	/* Filter overlay menu rows */
	.show-card.filter-overlay-option {
		font-size: 18px;
	}

	.show-card.filter-overlay-option .show-title {
		font-size: 17px;
	}

	.show-card.filter-overlay-option .show-description {
		font-size: 14px;
	}

	.show-card.filter-overlay-option .show-credits {
		font-size: 14px;
	}

	.show-card.filter-overlay-option .show-tag {
		font-size: 13px;
	}

	.show-card.filter-overlay-option .show-time {
		font-size: 15px;
		min-width: 81px;
	}

	.show-card.filter-overlay-option .show-all-showtimes,
	.show-card.filter-overlay-option .show-all-venues {
		font-size: 14px;
	}

	.show-card.filter-overlay-option .show-runtime {
		font-size: 13px;
	}

	.show-card.filter-overlay-option .venue-abbr {
		font-size: 18px;
		min-width: 54px;
	}

	.filter-overlay-option-checkbox-mark {
		font-size: 18px;
	}

	.filter-overlay-title {
		font-size: 23px;
	}

	.filter-overlay-footer {
		font-size: 15px;
	}

	/* Details modal */
	.modal-box {
		max-width: 400px;
		padding: 32px;
	}

	.details-title {
		font-size: 16px;
	}

	.details-venue {
		font-size: 14px;
	}

	.details-venue-address {
		font-size: 13px;
	}

	.details-datetime {
		font-size: 13px;
	}

	.details-meta {
		font-size: 12px;
	}

	.details-tags .show-tag,
	.details-meta .show-tag {
		font-size: 11px;
	}

	.details-description {
		font-size: 13px;
	}

	.details-credits {
		font-size: 12px;
	}

	.details-imdb,
	.details-tmdb,
	.details-youtube {
		font-size: 15px;
	}

	.tickets-button {
		font-size: 18px;
	}

	/* Full-page status messages */
	.loading,
	.no-shows,
	.error,
	.filter-overlay-empty {
		font-size: 23px;
	}

	/* Scroll-top button: clears the (now taller) sticky filter-bar.
	   Values scale 1.15x from the base 50 / 70 / 120 / 170 pattern to
	   stay below the bar's expanded button padding + font size. */
	.scroll-top {
		top: 58px;
	}
	body.menu-open .scroll-top {
		top: 138px;
	}
	body.menu-open:has(#filter-overlay[data-filter-type="show"]) .scroll-top {
		top: 196px;
	}
	body.kiosk.menu-open .scroll-top {
		top: 80px;
	}
	body.kiosk.menu-open:has(#filter-overlay[data-filter-type="show"]) .scroll-top {
		top: 138px;
	}
}

@media (min-width: 1600px) {
	body {
		font-size: 18px;
	}


	.filter-bar button,
	.picker-clear {
		font-size: 21px;
		padding: 13px 16px;
		min-width: 119px;
	}

	:root {
		--segment-font-size: 21px;
		--filter-bar-gap: 10px;
		--card-pad-x: 13px;
	}

	.segmented-control .segment {
		padding: 13px 16px;
	}

	/* Vertical padding tracks `.filter-bar button` above, as at 1200px. */
	.filter-overlay-search {
		font-size: 21px;
		padding: 13px 12px;
	}

	.show-card {
		padding: 10px var(--card-pad-x);
	}

	.venue-abbr {
		font-size: 18px;
		min-width: 54px;
	}

	.show-stale-emoji {
		font-size: 21px;
	}

	.show-time {
		font-size: 14px;
	}

	.show-runtime {
		font-size: 12px;
	}

	.show-venue-count {
		font-size: 14px;
	}

	.show-title {
		font-size: 17px;
	}

	.show-tag {
		font-size: 12px;
	}

	.show-description {
		font-size: 13px;
	}

	.show-credits {
		font-size: 13px;
	}

	.show-venue-address {
		font-size: 16px;
	}

	.show-last-updated {
		font-size: 14px;
	}

	.date-group-header {
		font-size: 16px;
		--date-header-pad-y: 13px;
	}

	.show-card.filter-overlay-option {
		font-size: 21px;
	}

	.show-card.filter-overlay-option .show-title {
		font-size: 20px;
	}

	.show-card.filter-overlay-option .show-description {
		font-size: 16px;
	}

	.show-card.filter-overlay-option .show-credits {
		font-size: 16px;
	}

	.show-card.filter-overlay-option .show-tag {
		font-size: 14px;
	}

	.show-card.filter-overlay-option .show-time {
		font-size: 17px;
		min-width: 91px;
	}

	.show-card.filter-overlay-option .show-all-showtimes,
	.show-card.filter-overlay-option .show-all-venues {
		font-size: 16px;
	}

	.show-card.filter-overlay-option .show-runtime {
		font-size: 14px;
	}

	.show-card.filter-overlay-option .venue-abbr {
		font-size: 21px;
		min-width: 63px;
	}

	.filter-overlay-option-checkbox-mark {
		font-size: 21px;
	}

	.filter-overlay-title {
		font-size: 26px;
	}

	.filter-overlay-footer {
		font-size: 17px;
	}

	.modal-box {
		max-width: 450px;
		padding: 36px;
	}

	.details-title {
		font-size: 18px;
	}

	.details-venue {
		font-size: 16px;
	}

	.details-venue-address {
		font-size: 14px;
	}

	.details-datetime {
		font-size: 14px;
	}

	.details-meta {
		font-size: 13px;
	}

	.details-tags .show-tag,
	.details-meta .show-tag {
		font-size: 12px;
	}

	.details-description {
		font-size: 14px;
	}

	.details-credits {
		font-size: 13px;
	}

	.details-imdb,
	.details-tmdb,
	.details-youtube {
		font-size: 17px;
	}

	.tickets-button {
		font-size: 21px;
	}

	.loading,
	.no-shows,
	.error,
	.filter-overlay-empty {
		font-size: 26px;
	}

	/* Scroll-top button: 1.30x scaling for the largest breakpoint. See the
	   1200px block for the base-pattern rationale. */
	.scroll-top {
		top: 65px;
	}
	body.menu-open .scroll-top {
		top: 156px;
	}
	body.menu-open:has(#filter-overlay[data-filter-type="show"]) .scroll-top {
		top: 221px;
	}
	body.kiosk.menu-open .scroll-top {
		top: 91px;
	}
	body.kiosk.menu-open:has(#filter-overlay[data-filter-type="show"]) .scroll-top {
		top: 156px;
	}
}
