/* ==========================================================================
   Bus Schedules Widget.
   Mobile-first (target 360px). Single scale-up breakpoint at 782px per the
   mobile-first rule. All touch targets ≥ 44×44 per the accessibility rule.
   ========================================================================== */

.lbs {
	--lbs-primary:   var(--primary, rgb(28, 22, 68));
	--lbs-secondary: var(--secondary, rgb(13, 126, 172));
	--lbs-accent:    var(--tertiary, rgb(223, 44, 50));
	--lbs-surface:   #ffffff;
	--lbs-surface-2: #f4f6f9;
	--lbs-border:    #dfe3ea;
	--lbs-text:      #1f2544;
	--lbs-muted:     #6a7386;
	--lbs-ok:        #1f9d55;
	--lbs-warn:      #c67600;
	--lbs-err:       #e30613;
	--lbs-radius:    14px;
	--lbs-radius-sm: 8px;
	--lbs-shadow:    0 1px 2px rgba(28, 22, 68, 0.06), 0 8px 24px rgba(28, 22, 68, 0.06);

	max-width: 980px;
	margin: 1rem auto 2rem;
	color: var(--lbs-text);
	box-sizing: border-box;
	font-family: inherit;
}
.lbs *,
.lbs *::before,
.lbs *::after { box-sizing: border-box; }

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

/* Shared blink animation — used by the live chip dot, the legend's live
 * and delayed dots, and anything else that wants to pulse subtly to
 * signal "this is moving / live data". */
@keyframes lbs-blink {
	0%, 100% { opacity: 1;    transform: scale(1); }
	50%      { opacity: 0.35; transform: scale(0.85); }
}

/* ---------- Sections (city / regional) ---------------------------------- */

.lbs__sections {
	display: flex;
	flex-direction: column;
	gap: 1.25rem;
}

.lbs-section {
	display: flex;
	flex-direction: column;
	gap: 0.6rem;
}

/* Section heading card. Two-row grid:
 *
 *   [icon]  Title                            ← row 1, flex line
 *   ───────────────────────────────────────  ← hairline divider
 *   Subtitle spans full width                ← row 2, full-bleed
 *
 * `.lbs-section__text` uses `display: contents` so its children
 * (`.lbs-section__title` + `.lbs-section__sub`) participate directly
 * in the parent grid — lets the subtitle break out to span both
 * columns (icon + text) without restructuring the HTML. The soft
 * gradient + subtle shadow + slightly larger accent disk all lift
 * the card above "plain grey block" without getting loud. */
.lbs-section__head {
	display: grid;
	grid-template-columns: auto 1fr;
	align-items: center;
	column-gap: 0.8rem;
	row-gap: 0.55rem;
	margin: 0;
	padding: 0.75rem 0.9rem;
	background:
		linear-gradient(180deg,
			rgba(255, 255, 255, 0.85) 0,
			var(--lbs-surface-2) 100%);
	border: 1px solid var(--lbs-border);
	border-left: 4px solid var(--lbs-secondary);
	border-radius: var(--lbs-radius-sm);
	box-shadow: 0 1px 2px rgba(28, 22, 68, 0.04);
}
.lbs-section--regional .lbs-section__head {
	border-left-color: var(--lbs-accent);
}
.lbs-section__accent {
	grid-column: 1;
	grid-row: 1;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 2.25rem;
	height: 2.25rem;
	border-radius: 50%;
	background: rgba(13, 126, 172, 0.12);
	color: var(--lbs-secondary);
	box-shadow: inset 0 0 0 1px rgba(13, 126, 172, 0.18);
}
.lbs-section--regional .lbs-section__accent {
	background: rgba(223, 44, 50, 0.12);
	color: var(--lbs-accent);
	box-shadow: inset 0 0 0 1px rgba(223, 44, 50, 0.18);
}
.lbs-section__icon { display: block; }
/* `display: contents` hoists the h2 + p into the parent grid so the
 * subtitle can claim a full-width second row. The element itself
 * still exists in the a11y tree, just not in layout. */
.lbs-section__text { display: contents; }
.lbs-section__title {
	grid-column: 2;
	grid-row: 1;
	margin: 0;
	font-size: 1.05rem;
	font-weight: 800;
	letter-spacing: -0.005em;
	color: var(--lbs-primary);
	line-height: 1.2;
	min-width: 0;
}
.lbs-section__sub {
	grid-column: 1 / -1;
	grid-row: 2;
	margin: 0;
	padding-top: 0.55rem;
	border-top: 1px dashed var(--lbs-border);
	font-size: 0.82rem;
	color: var(--lbs-muted);
	line-height: 1.4;
}

.lbs-section__lines {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0.55rem;
}

/* ---------- Accordion row ------------------------------------------------ */

.lbs-line {
	background: var(--lbs-surface);
	border: 1px solid var(--lbs-border);
	border-radius: var(--lbs-radius);
}

/* Focus pulse — fired by `bus-schedules.js` when a line is opened
   from the airport map (or any cross-widget focus event). On desktop
   the schedule frequently fits in the viewport already, so a scroll
   alone gives no signal that the click landed; the pulse plays
   regardless and draws the eye to the affected row. The animation is
   a brief box-shadow ring + tinted background that fades out — purely
   decorative, layout-neutral, and disabled under reduced motion. */
@keyframes lbs-line-focus-flash {
	0%   {
		box-shadow: 0 0 0 0 rgba(13, 126, 172, 0.55);
		background: color-mix(in srgb, var(--lbs-secondary) 12%, var(--lbs-surface));
	}
	60%  {
		box-shadow: 0 0 0 8px rgba(13, 126, 172, 0);
		background: color-mix(in srgb, var(--lbs-secondary) 6%, var(--lbs-surface));
	}
	100% {
		box-shadow: 0 0 0 0 rgba(13, 126, 172, 0);
		background: var(--lbs-surface);
	}
}
.lbs-line.is-focused-flash {
	animation: lbs-line-focus-flash 1.2s ease-out 1;
}
@media (prefers-reduced-motion: reduce) {
	.lbs-line.is-focused-flash { animation: none; }
}

/* Single-row header:
 *   [chev · logo · label · route] ........... [swap]
 * The swap button is a SIBLING of the toggle (nested <button>s are
 * invalid HTML). Bounds (First/Last) live in the body toolbar now. */
.lbs-line__header {
	display: flex;
	align-items: center;
	gap: 0.35rem;
	padding-right: 0.5rem;
}

.lbs-line__toggle {
	display: flex;
	align-items: center;
	gap: 0.55rem;
	flex: 1 1 auto;
	min-width: 0;
	min-height: 44px;
	padding: 0.55rem 0.6rem 0.55rem 0.6rem;
	background: transparent;
	border: 0;
	border-radius: var(--lbs-radius-sm);
	color: var(--lbs-text);
	text-align: left;
	cursor: pointer;
	font: inherit;
	-webkit-tap-highlight-color: transparent;
	transition: background 0.15s;
}
.lbs-line__toggle:hover {
	background: rgba(28, 22, 68, 0.035);
}
.lbs-line__toggle:focus { outline: none; }
.lbs-line__toggle:focus-visible {
	outline: 2px solid var(--lbs-secondary);
	outline-offset: 2px;
	background: rgba(13, 126, 172, 0.06);
}

.lbs-line__chev {
	flex: 0 0 auto;
	width: 0.55rem; height: 0.55rem;
	border-right: 2px solid var(--lbs-muted);
	border-bottom: 2px solid var(--lbs-muted);
	transform: rotate(-45deg);
	transition: transform 0.2s ease;
}
.lbs-line[data-open="true"] .lbs-line__chev {
	transform: rotate(45deg);
}

/* Logos render at their intrinsic aspect ratio, height-capped at 22px,
 * width-capped at 44px. Lets each operator logo take only as much
 * horizontal space as its artwork actually needs — AVL (~1.16:1) shows
 * at ~25px wide, RGTR (~2.26:1) at 44px, Luxtram is cropped to a 24×24
 * icon square. This removes the "empty space" gap that used to appear
 * between a narrow bus logo and the line label. */
.lbs-line__logo {
	flex: 0 0 auto;
	width: auto;
	height: 22px;
	max-width: 44px;
	object-fit: contain;
	object-position: left center;
}
/* Tram: crop the Luxtram SVG to just the four-colored square icon on
 * the left of its 376×94 viewBox. */
.lbs-line[data-mode="tram"] .lbs-line__logo {
	width: 24px;
	height: 24px;
	max-width: none;
	object-fit: cover;
	object-position: left center;
}

.lbs-line__label {
	flex: 0 0 auto;
	font-weight: 800;
	color: var(--lbs-primary);
	white-space: nowrap;
	font-size: 0.95rem;
}
.lbs-line[data-mode="tram"] .lbs-line__label {
	text-transform: uppercase;
	letter-spacing: 0.04em;
}

/* Route title lives inside the toggle, to the right of the label.
 * Muted + slightly smaller so it reads as secondary info. Ellipses on
 * narrow viewports so it never wraps the header to two lines. Uses a
 * left border as a subtle separator from the label. */
.lbs-line__route {
	flex: 1 1 auto;
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	padding-left: 0.5rem;
	border-left: 1px solid var(--lbs-border);
	font-weight: 500;
	font-size: 0.85rem;
	color: var(--lbs-muted);
}
.lbs-line__route-pane { display: none; }
.lbs-line[data-direction="to"]   .lbs-line__route-pane[data-direction="to"],
.lbs-line[data-direction="from"] .lbs-line__route-pane[data-direction="from"] {
	display: inline;
}

@media (max-width: 420px) {
	.lbs-line__toggle { gap: 0.4rem; padding: 0.45rem 0.45rem; }
	.lbs-line__route  { font-size: 0.78rem; padding-left: 0.4rem; }
}

/* ---------- Body (smooth accordion) ------------------------------------
 * The accordion opens / closes by transitioning `grid-template-rows`
 * from `0fr` → `1fr`. The outer `.lbs-line__body` is the grid container
 * (and the only animated element); the inner `.lbs-line__body-inner`
 * holds ALL the visual styling (border-top, background, padding, etc.)
 * so that when the grid row collapses to 0fr, nothing visible remains —
 * no hairline border, no stray background colour.
 *
 * `min-height: 0` on the inner is the critical bit: it lets the grid
 * row actually shrink to 0. Without it, the inner's min-content height
 * would force the row open.
 *
 * We avoid `display: none` on collapse (no `[hidden]` attribute either)
 * because it would kill the transition AND zero out
 * `viewport.clientWidth`, which broke the chip-carousel arrow
 * visibility on initial hydrate.
 * ------------------------------------------------------------------------ */
.lbs-line__body {
	display: grid;
	grid-template-rows: 0fr;
	overflow: hidden;
	transition: grid-template-rows 280ms cubic-bezier(0.4, 0, 0.2, 1);
}
.lbs-line[data-open="true"] .lbs-line__body {
	grid-template-rows: 1fr;
}
/* Grid item: MUST be chrome-free (no padding, no border, no background)
 * so `grid-template-rows: 0fr` can collapse the row to exactly 0. Any
 * padding here would set an intrinsic min-content floor and leave a
 * visible sliver of the next-level content poking through the header.
 * All the styled-block visuals live on `.lbs-line__content` one level
 * deeper. */
.lbs-line__body-inner {
	min-height: 0;
	overflow: hidden;
}
.lbs-line__content {
	padding: 0.5rem 0.6rem 0.6rem;
	border-top: 1px solid var(--lbs-border);
	background: var(--lbs-surface-2);
	border-bottom-left-radius: var(--lbs-radius);
	border-bottom-right-radius: var(--lbs-radius);
	display: flex;
	flex-direction: column;
	gap: 0.45rem;
}

@media (prefers-reduced-motion: reduce) {
	.lbs-line__body { transition: none; }
}

/* Direction swap — single icon button, sits inline right next to the
 * destination text on row 2. Its SVG icon rotates 180° cumulatively
 * per click (JS-driven so it never snaps back). The button itself
 * never moves; only the icon inside it spins. */
/* Swap button sits immediately next to the route text on row 2 — small
 * pill that flips the direction. Its icon rotates 180° cumulatively per
 * click (JS-driven). The button itself never moves. */
.lbs-dir-swap {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 28px;
	height: 28px;
	padding: 0;
	border: 1px solid var(--lbs-border);
	border-radius: 999px;
	background: var(--lbs-surface);
	color: var(--lbs-muted);
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
	transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.lbs-dir-swap:hover {
	color: var(--lbs-primary);
	border-color: var(--lbs-primary);
}
.lbs-dir-swap:focus-visible {
	outline: 2px solid var(--lbs-secondary);
	outline-offset: 2px;
}
.lbs-dir-swap__icon {
	display: block;
	width: 18px;
	height: 18px;
	transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
	will-change: transform;
}

/* Only the panel matching the row's current direction is visible. The
 * `[hidden]` attribute is also kept in sync by JS, so CSS-disabled
 * browsers still work. */
.lbs-line__directions { display: block; }
.lbs-line__direction[hidden] { display: none; }

/* Two-pill secondary actions bar (Timetable + Route map).
 *
 * Right-aligned so the eye lands on the stops list first and the
 * external-link actions read as supporting affordances. Both glyph AND
 * text are ALWAYS visible — emoji (📄 / 🗺️) proved unreadable across
 * OSes, and icon-only rendering required `aria-label` decoding to be
 * understood. With only two links we have plenty of room at 360px. */
.lbs-toolbar {
	display: flex;
	flex-wrap: wrap;
	justify-content: flex-end;
	align-items: center;
	gap: 0.35rem 0.5rem;
	padding: 0 0.1rem;
	min-width: 0;
}
.lbs-toolbar__link {
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
	flex: 0 0 auto;
	min-height: 32px;
	padding: 0.3rem 0.7rem;
	border: 1px solid var(--lbs-border);
	border-radius: 999px;
	background: var(--lbs-surface);
	white-space: nowrap;
	color: var(--lbs-secondary);
	font-size: 0.78rem;
	font-weight: 600;
	text-decoration: none;
	-webkit-tap-highlight-color: transparent;
	transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.lbs-toolbar__link:hover,
.lbs-toolbar__link:focus-visible {
	background: var(--lbs-secondary);
	border-color: var(--lbs-secondary);
	color: #fff;
}
.lbs-toolbar__link:focus-visible {
	outline: 2px solid var(--lbs-secondary);
	outline-offset: 2px;
}
.lbs-toolbar__icon {
	flex: 0 0 auto;
	width: 14px;
	height: 14px;
	display: block;
}

.lbs-line__notice {
	margin: 0;
	padding: 0.55rem 0.7rem;
	background: rgba(198, 118, 0, 0.08);
	border: 1px solid rgba(198, 118, 0, 0.25);
	border-radius: var(--lbs-radius-sm);
	color: var(--lbs-warn);
	font-size: 0.8rem;
}

/* ---------- Stops list (subway-map) -------------------------------------
 * Each stop is a row with a small dot on the left, connected top-to-bottom
 * by a thin vertical line — just like a transit map. Airport stops get
 * the accent colour to mark the entry/exit of the trip.
 *
 * Layout uses two CSS custom properties per stop — `--lbs-stop-dot-x` and
 * `--lbs-stop-dot-y` — that define the dot's center. The indicator is
 * positioned off them, and so is the connecting line (drawn per-stop
 * via `::before`), so they're guaranteed to agree no matter how the
 * breakpoint moves them. The first stop's line starts AT the dot (no
 * segment above), the last stop's line ends AT the dot (no segment
 * below) — so the line never overshoots the route's endpoints.
 * ------------------------------------------------------------------------ */

.lbs-stops {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: 0;
}
.lbs-stop {
	/* Dot center, in the stop's own coordinate space. Tweak these two
	 * to realign the dot/line without touching anything else. */
	--lbs-stop-dot-x: 0.85rem;
	/* Dot vertical center = row top-padding (0.35rem) + half the
	 * name's line-box (~0.85rem * 1.4 / 2 ≈ 0.6rem) ≈ 0.95rem. The
	 * old 0.88rem assumed the indicator's half-size was 0.45rem
	 * (when it was 0.9rem); now that the indicator is a fixed 14px
	 * (7px half), the dot sat ~1px high relative to the text. */
	--lbs-stop-dot-y: 1.05rem;

	display: flex;
	flex-wrap: wrap;
	align-items: center;
	column-gap: 0.5rem;
	row-gap: 0.3rem;
	padding: 0.35rem 0 0.35rem 1.8rem;
	position: relative;
}
.lbs-stop::before {
	content: '';
	position: absolute;
	left: var(--lbs-stop-dot-x);
	top: 0;
	bottom: 0;
	width: 2px;
	transform: translateX(-1px);
	background: var(--lbs-border);
	z-index: 0;
	pointer-events: none;
}
.lbs-stop:first-child::before { top: var(--lbs-stop-dot-y); bottom: 0; }
.lbs-stop:last-child::before  { top: 0; bottom: auto; height: var(--lbs-stop-dot-y); }
.lbs-stop:only-child::before  { display: none; }

.lbs-stop__indicator {
	position: absolute;
	left: calc(var(--lbs-stop-dot-x) - 7px);
	top:  calc(var(--lbs-stop-dot-y) - 7px);
	/* Fixed even-pixel size + aspect-ratio guard. `0.9rem` (14.4px) was
	 * being sub-pixel rounded asymmetrically (e.g. 14×15) which made the
	 * circle look squashed — most visible on the static red airport
	 * indicator, where there's no blink animation to hide it. */
	width: 14px;
	height: 14px;
	aspect-ratio: 1 / 1;
	border-radius: 50%;
	background: var(--lbs-surface);
	border: 2px solid var(--lbs-muted);
	box-sizing: border-box;
	z-index: 1;
}
.lbs-stop--airport .lbs-stop__indicator {
	background: var(--lbs-accent);
	border-color: var(--lbs-accent);
	box-shadow: 0 0 0 3px rgba(223, 44, 50, 0.15);
}
.lbs-stop__place {
	display: inline-flex;
	align-items: center;
	gap: 0.15rem;
	flex: 0 1 100%;
	min-width: 0;
}
.lbs-stop__name {
	flex: 0 1 auto;
	min-width: 0;
	font-size: 0.85rem;
	font-weight: 600;
	color: var(--lbs-text);
	line-height: 1.25;
	margin: 0;
	text-align: left;
}
.lbs-stop--airport .lbs-stop__name {
	color: var(--lbs-primary);
}
.lbs-stop__map {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	flex: 0 0 auto;
	width: 28px;
	height: 28px;
	margin: -0.25rem 0;
	padding: 0;
	border: 0;
	border-radius: 999px;
	background: transparent;
	color: var(--lbs-secondary);
	cursor: pointer;
	font: inherit;
	-webkit-tap-highlight-color: transparent;
	transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.lbs-stop__map:hover,
.lbs-stop__map:focus-visible {
	background: rgba(13, 126, 172, 0.1);
	color: var(--lbs-primary);
}
.lbs-stop__map:focus-visible {
	outline: 2px solid var(--lbs-secondary);
	outline-offset: 2px;
}
.lbs-stop__map-icon {
	display: block;
	width: 14px;
	height: 14px;
}
.lbs-stop__chips-slot {
	flex: 1 1 100%;
	min-width: 0;
	/* Cross-fade the chip area whenever the day/time stepper rolls
	 * `refTs`: bus-schedules.js sets `data-loading="true"` on the
	 * direction panel for ~110 ms while it tears down + rebuilds
	 * every stop's chip list. Without this transition, a click on
	 * `→ 09:00` flashes the new chips into place, which reads as a
	 * layout jump rather than a deliberate UI response. The very
	 * short duration keeps the widget feeling snappy — long fades
	 * make a "loading" UI feel slow even when the work is instant. */
	transition: opacity 90ms ease, filter 90ms ease;
}
.lbs-line__direction[data-loading="true"] .lbs-stop__chips-slot {
	opacity: 0.35;
	/* Tiny blur sells the "settling into place" beat without
	 * obscuring the content the user is looking at. Above 1 px and
	 * the text becomes unreadable, which defeats the purpose. */
	filter: blur(0.5px);
}
@media (prefers-reduced-motion: reduce) {
	/* Honour OS-level motion-reduction: no fades, no blur — the
	 * chip swap happens instantly. The widget still flips
	 * `data-loading` on/off, just nothing visual hangs off it.
	 * Users on this setting prefer "less motion" over "smoother
	 * transition", so the abrupt swap is the correct call. */
	.lbs-stop__chips-slot { transition: none; }
	.lbs-line__direction[data-loading="true"] .lbs-stop__chips-slot {
		opacity: 1;
		filter: none;
	}
}

/* ---------- Chip carousel (3 visible, scroll-by-one) -------------------
 * Each stop is a horizontal scroll carousel of the in-hour departures.
 * Three chips fit visually at once; extras live in the scroll track.
 * Circular arrow buttons float over the faded edges (mask gradient on
 * the viewport), and one click scrolls by exactly one chip — so chips
 * slide on/off smoothly, overlapping briefly rather than swapping in a
 * jump of three. Arrows fade out at the scroll extremes so the
 * passenger always knows there's nothing more on that side.
 *
 * Swipe / keyboard scrolling on the viewport works natively too.
 * ------------------------------------------------------------------------ */
.lbs-chips {
	position: relative;
	min-width: 0;
}

/* The viewport is also a size container, so chip children can size
 * themselves off its inline dimension via `cqi` without circular
 * dependencies (can't use `%` here — flex children of the list would
 * resolve `%` against the list's intrinsic width, which depends on
 * the children, which depends on `%`, ad nauseam). */
.lbs-chips__viewport {
	container-type: inline-size;
	overflow-x: auto;
	overflow-y: hidden;
	-webkit-overflow-scrolling: touch;
	scroll-snap-type: x mandatory;
	scroll-behavior: smooth;
	scrollbar-width: none;
	-ms-overflow-style: none;
	/* 1.5rem of scroll padding at each edge so `scroll-snap-align: start`
	 * lands chips offset from the viewport's left edge, creating room
	 * on the LEFT for the previous chip's peek to show. The matching
	 * padding on the list below provides the equivalent space for the
	 * first and last chips not to butt up against the edge. */
	scroll-padding-inline: 1.5rem;
}
.lbs-chips__viewport::-webkit-scrollbar { display: none; }

/* Edge fade + arrow overlay only apply when there's actually something
 * off-screen to scroll to. A non-scrollable row stays crisp and clean.
 *
 * The fade is directional: JS toggles `--at-start` when scrollLeft is 0
 * and `--at-end` when fully scrolled, and each class drops the fade on
 * that side. The fade should only hint at hidden content — the first
 * and last chips themselves never need to fade. */
/* Fade band tuning:
 *   - width 1rem (was 1.5rem): keeps the hint visible but only softens
 *     a narrow sliver of the adjacent chip.
 *   - end alpha 0.35 (via rgba alpha on the mask stop): the partially
 *     hidden chip never fully disappears, so the "N min" label stays
 *     legible even when it's under the fade. Readers still perceive
 *     the gradient as "there's more that way". */
.lbs-chips--scrollable .lbs-chips__viewport {
	-webkit-mask-image: linear-gradient(90deg,
		rgba(0,0,0,0.35) 0,
		#000 1rem,
		#000 calc(100% - 1rem),
		rgba(0,0,0,0.35) 100%);
	mask-image: linear-gradient(90deg,
		rgba(0,0,0,0.35) 0,
		#000 1rem,
		#000 calc(100% - 1rem),
		rgba(0,0,0,0.35) 100%);
}
.lbs-chips--scrollable.lbs-chips--at-start .lbs-chips__viewport {
	-webkit-mask-image: linear-gradient(90deg,
		#000 0,
		#000 calc(100% - 1rem),
		rgba(0,0,0,0.35) 100%);
	mask-image: linear-gradient(90deg,
		#000 0,
		#000 calc(100% - 1rem),
		rgba(0,0,0,0.35) 100%);
}
.lbs-chips--scrollable.lbs-chips--at-end .lbs-chips__viewport {
	-webkit-mask-image: linear-gradient(90deg,
		rgba(0,0,0,0.35) 0,
		#000 1rem,
		#000 100%);
	mask-image: linear-gradient(90deg,
		rgba(0,0,0,0.35) 0,
		#000 1rem,
		#000 100%);
}
.lbs-chips--scrollable.lbs-chips--at-start.lbs-chips--at-end .lbs-chips__viewport {
	-webkit-mask-image: none;
	mask-image: none;
}

/* `padding-block` here gives the viewport's clip box a few pixels of
 * headroom above and below the chips. Without it the list's height
 * collapses to the chip height and the viewport's `overflow-y: hidden`
 * slices off any focus ring (or any other outset decoration) drawn
 * outside the chip's border box. 4px fits a 2px outline with 2px
 * offset on either side. */
.lbs-chips__list {
	display: flex;
	gap: 0.3rem;
	padding-inline: 1.5rem;
	padding-block: 4px;
	min-width: 0;
}

/* Chip sizing:
 *   min  = (100cqi - 3rem padding - 0.6rem gap) / 3  → "3 fit" floor on
 *          mobile-sized viewports.
 *   pref = …/ 4                                      → ~4 chips on mid
 *          viewports — roomier than the old /6 formula.
 *   max  = 110px                                     → cap width on
 *          wide viewports so chips stay readable but don't stretch.
 * `100cqi` is the viewport's inline size; the chip never grows past the
 * cap even when the viewport is 800px wide. */
.lbs-chips__list > .lbs-chip {
	flex: 0 0 clamp(
		calc((100cqi - 3rem - 0.6rem) / 3),
		calc((100cqi - 3rem - 3 * 0.3rem) / 4),
		110px
	);
	min-width: 0;
	max-width: 110px;
	scroll-snap-align: start;
	animation: lbs-chip-in 180ms ease-out both;
}
@keyframes lbs-chip-in {
	from { opacity: 0; transform: translateY(2px); }
	to   { opacity: 1; transform: none; }
}

/* Arrow buttons: circular, floating, centred vertically on the row.
 *
 * HIDDEN ON MOBILE (default): on touch viewports the fade mask alone
 * signals "there's more chips in that direction" and users swipe
 * horizontally — the expected gesture on a mobile carousel. The
 * arrow overlays were covering the first / last chip, making the
 * visible time hard to read. On touch there's also the row-level
 * `‹ 20:00 ›` time stepper which advances all stops in sync — that's
 * the keyboard / non-swipe fallback.
 *
 * SHOWN ON DESKTOP (≥ 782px): cursor users can't swipe, so the
 * circular arrow buttons re-appear over the faded edges. See the
 * desktop scale-up block at the bottom of this file.
 *
 * 32×32 visual, +pseudo-element hit-zone pushing the touch target to
 * 44×44 per the mobile-first / accessibility rules. */
.lbs-chips__arrow {
	display: none;
	position: absolute;
	top: 50%;
	transform: translateY(-50%);
	z-index: 2;
	width: 32px;
	height: 32px;
	padding: 0;
	align-items: center;
	justify-content: center;
	background: var(--lbs-surface);
	border: 1px solid var(--lbs-border);
	border-radius: 50%;
	color: var(--lbs-secondary);
	box-shadow: 0 2px 6px rgba(28, 22, 68, 0.18);
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
	opacity: 1;
	transition: opacity 0.2s ease, background 0.15s, color 0.15s, border-color 0.15s;
}
.lbs-chips__arrow::before {
	content: '';
	position: absolute;
	inset: -6px;
}
.lbs-chips__arrow--prev { left: -2px; }
.lbs-chips__arrow--next { right: -2px; }
.lbs-chips__arrow:hover,
.lbs-chips__arrow:focus-visible {
	background: var(--lbs-secondary);
	border-color: var(--lbs-secondary);
	color: #fff;
	transform: translateY(-50%) scale(1.05);
}
.lbs-chips__arrow:focus-visible {
	outline: 2px solid var(--lbs-secondary);
	outline-offset: 2px;
}
.lbs-chips__arrow--hidden {
	opacity: 0;
	pointer-events: none;
}
.lbs-chips__arrow-icon {
	width: 0.45rem;
	height: 0.45rem;
	border-top: 2px solid currentColor;
	border-right: 2px solid currentColor;
}
.lbs-chips__arrow--prev .lbs-chips__arrow-icon {
	transform: rotate(-135deg);
	margin-left: 2px;
}
.lbs-chips__arrow--next .lbs-chips__arrow-icon {
	transform: rotate(45deg);
	margin-right: 2px;
}

@media (prefers-reduced-motion: reduce) {
	.lbs-chips__viewport        { scroll-behavior: auto; }
	.lbs-chips__arrow           { transition: none; }
	.lbs-chips__list > .lbs-chip { animation: none; }
}

/* ---------- Time nav (per direction box) ------------------------------
 * Sits at the top of each `.lbs-line__direction`. One row, two steppers:
 *
 *   ‹  Today  ›        ‹  09:00  ›
 *
 * Day stepper moves ± 1 calendar day (clamped to today..+6), preserving
 * time-of-day. Time stepper moves ± one 3-departure window on the
 * reference stop, strictly WITHIN the selected day (no auto day
 * crossing — that's what the day stepper is for).
 *
 * Hidden by default (`[hidden]`) — the JS reveals it on hydrate so the
 * controls never appear without working handlers.
 * ------------------------------------------------------------------------ */

.lbs-timenav {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 0.75rem;
	margin: 0 0 0.6rem;
	padding: 0.35rem 0.6rem;
	border: 1px solid var(--lbs-border);
	border-radius: 0.6rem;
	background: var(--lbs-surface);
}
.lbs-timenav[hidden] { display: none; }

.lbs-timenav__stepper {
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
}

.lbs-timenav__label {
	font-size: 0.9rem;
	font-weight: 700;
	color: var(--lbs-text);
	padding: 0 0.25rem;
	min-width: 3.75rem;
	text-align: center;
	font-variant-numeric: tabular-nums;
}
.lbs-timenav__stepper--time .lbs-timenav__label {
	min-width: 3.25rem;
	color: var(--lbs-secondary);
}

/* Day stepper label is stacked: the day name on top ("Today" / "Thu"),
 * the selected day's service-window underneath ("05:17 – 23:45"). The
 * sub-line is muted + smaller so the day name stays the primary read.
 * If the API has no bounds for that day (future day beyond the feed,
 * line not running) the sub-line simply stays empty — the stepper
 * stays the same height because `min-height` clamps to the two-line
 * baseline. */
.lbs-timenav__label--day {
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	line-height: 1.1;
	min-width: 5.75rem;
	min-height: 2.4em;
}
.lbs-timenav__label-main {
	font-weight: 700;
	color: var(--lbs-text);
}
.lbs-timenav__label-sub {
	font-size: 0.7rem;
	font-weight: 500;
	color: var(--lbs-muted);
	font-variant-numeric: tabular-nums;
	margin-top: 0.1rem;
	letter-spacing: 0.01em;
}
.lbs-timenav__label-sub:empty {
	min-height: 0.9em;
}

.lbs-timenav__arrow {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 36px;
	height: 36px;
	padding: 0;
	border: 1px solid var(--lbs-border);
	background: var(--lbs-surface);
	border-radius: 50%;
	color: var(--lbs-secondary);
	cursor: pointer;
	-webkit-tap-highlight-color: transparent;
	transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.lbs-timenav__arrow:hover:not(:disabled),
.lbs-timenav__arrow:focus-visible {
	background: var(--lbs-secondary);
	border-color: var(--lbs-secondary);
	color: #fff;
}
.lbs-timenav__arrow:focus-visible {
	outline: 2px solid var(--lbs-secondary);
	outline-offset: 2px;
}
.lbs-timenav__arrow:disabled {
	opacity: 0.35;
	cursor: not-allowed;
}
.lbs-timenav__arrow-icon {
	width: 0.5rem; height: 0.5rem;
	border-top: 2px solid currentColor;
	border-right: 2px solid currentColor;
}
.lbs-timenav__arrow--prev .lbs-timenav__arrow-icon {
	transform: rotate(-135deg);
	margin-left: 3px;
}
.lbs-timenav__arrow--next .lbs-timenav__arrow-icon {
	transform: rotate(45deg);
	margin-right: 3px;
}

@media (max-width: 560px) {
	.lbs-timenav         { padding: 0.3rem 0.4rem; gap: 0.4rem; }
	.lbs-timenav__label  { font-size: 0.82rem; min-width: 3rem; }
	.lbs-timenav__stepper--time .lbs-timenav__label { min-width: 2.75rem; }
	.lbs-timenav__label--day { min-width: 5rem; }
	.lbs-timenav__label-sub  { font-size: 0.65rem; }
	.lbs-timenav__arrow  { width: 32px; height: 32px; }
}

/* ---------- Chip (compact) ---------------------------------------------
 * One line by default: [dot] + time. A second line appears only when the
 * live platform (Quai) differs from the scheduled one, via
 * `.lbs-chip__changes`. Cancellations are shown by the red dot +
 * strikethrough time — no separate badge. Dot colour encodes live status
 * — paired with a legend row below the stops so colour is never the only
 * signal.
 * ------------------------------------------------------------------------ */

.lbs-chip {
	display: grid;
	/* Two auto-sized columns, with the whole track centred inside the
	 * chip via `justify-content: center`. This keeps the dot and the
	 * "5 min" label at a fixed ~0.35rem apart instead of pushed to
	 * opposite ends of the chip by a `1fr` spacer. */
	grid-template-columns: auto auto;
	justify-content: center;
	align-items: center;
	column-gap: 0.35rem;
	row-gap: 0.1rem;
	/* 36px + 1px top/bottom border = 38px, + `.lbs-chips__list` 4px top/bottom
	 * padding = 46px total — matches the empty-state row (44px min-height +
	 * 1px top/bottom border) so the slot doesn't jump when chips hydrate
	 * in/out. Well above WCAG 2.1 AA's 24×24 target-size minimum; the chip
	 * is also ~72–88px wide, so the hit area stays generous. */
	min-height: 36px;
	padding: 0.25rem 0.4rem;
	background: var(--lbs-surface);
	border: 1px solid var(--lbs-border);
	border-radius: var(--lbs-radius-sm);
	color: var(--lbs-text);
	font: inherit;
	cursor: pointer;
	text-align: center;
	-webkit-tap-highlight-color: transparent;
	/* Chip labels ("N min" / "now") are controls, not quotable copy —
	 * long-press shouldn't pop the text-selection menu and a double
	 * click shouldn't highlight them. */
	-webkit-user-select: none;
	user-select: none;
	position: relative;
	/* NOTE: no `overflow: hidden` here. It used to clip an inset focus
	 * outline on some Chromium builds, leaving the ring visible on the
	 * left / right sides only. The chip has nothing to clip (dot + time
	 * + optional platform row all fit inside by construction), so the
	 * cleaner fix is to just not clip. */
}
/* Inset focus ring. Lives inside the chip's padding, so it is never
 * clipped by the viewport's `overflow-y: hidden`. 2px solid meets the
 * WCAG 2.2 focus-visible thickness + contrast bar against the chip's
 * surface background. */
.lbs-chip:focus-visible {
	outline: 2px solid var(--lbs-secondary);
	outline-offset: -2px;
	z-index: 1;
}
.lbs-chip__dot {
	/* `block` (not inline-block) so the grid cell doesn't introduce a
	 * baseline-aligned inline box that can shift the dot off-pixel.
	 * Fixed even-pixel size + aspect-ratio guard avoids the sub-pixel
	 * asymmetry (e.g. 9×10) that made the static red dot look oval —
	 * green/amber pulse, so the squash was masked there. */
	display: block;
	grid-column: 1;
	grid-row: 1;
	width: 10px;
	height: 10px;
	aspect-ratio: 1 / 1;
	flex-shrink: 0;
	border-radius: 50%;
	background: var(--lbs-muted);
	opacity: 0.7;
}
.lbs-chip[data-live="green"] .lbs-chip__dot,
.lbs-chip[data-live="amber"] .lbs-chip__dot,
.lbs-chip[data-live="red"]   .lbs-chip__dot {
	opacity: 1;
}
.lbs-chip__time {
	grid-column: 2;
	grid-row: 1;
	display: inline-flex;
	align-items: baseline;
	gap: 0.2rem;
	font-size: 0.88rem;
	font-weight: 700;
	font-variant-numeric: tabular-nums;
	line-height: 1.1;
	white-space: nowrap;
	/* The label is a single text node like "17 min" — tighten the
	 * gap between the number and the unit so the chip reads as one
	 * value. Default word-spacing is ≈ one space char (~3.5px at our
	 * font size); a slight negative brings it closer without
	 * colliding. */
	word-spacing: -0.12em;
}
.lbs-chip__changes {
	grid-column: 1 / -1;
	grid-row: 2;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 0.25rem;
	font-size: 0.6rem;
	line-height: 1.1;
}

/* Dot colour = live status. Green AND amber both blink so any live-
 * sourced chip pulses visibly, regardless of whether the bus is on
 * time or running late — both states are "tracked in real time" and
 * the user should see at a glance that the value is updating, not
 * stale. Red (cancelled) and neutral (pure scheduled) stay static:
 * they're end states, nothing is moving. */
.lbs-chip[data-live="green"] .lbs-chip__dot {
	background: var(--lbs-ok);
	animation: lbs-blink 1.4s ease-in-out infinite;
}
.lbs-chip[data-live="amber"] .lbs-chip__dot {
	background: var(--lbs-warn);
	animation: lbs-blink 1.4s ease-in-out infinite;
}
.lbs-chip[data-live="red"]   .lbs-chip__dot { background: var(--lbs-err); }

/* NOW grace window. The chip stays on screen for up to GRACE_MS
 * (120s) past its scheduled time so a passenger sprinting up to the
 * stop doesn't second-guess themselves when the dot suddenly
 * vanishes. `data-now="now"` is set for the full window; the dot
 * keeps blinking with its last known live colour throughout (no CSS
 * override needed — the `data-live` rules above apply). Past
 * GRACE_MS the chip is filtered out on the next render.
 */

/* Cancelled chip: always the scheduled 24h time (never "N min" — a
 * countdown for a bus that isn't coming is misleading). A solid
 * strike-through over the digits makes the cancellation unmistakable
 * without relying on the red dot alone. */
.lbs-chip[data-cancelled="true"] .lbs-chip__time {
	text-decoration: line-through;
	text-decoration-thickness: 0.12em;
	text-decoration-color: var(--lbs-err);
	color: var(--lbs-muted);
}
.lbs-chip[data-cancelled="true"] {
	border-color: rgba(227, 6, 19, 0.35);
	background: rgba(227, 6, 19, 0.05);
}
.lbs-chip__platform {
	font-weight: 700;
	color: var(--lbs-accent);
	white-space: nowrap;
}

/* ---------- Legend ------------------------------------------------------
 * Static per-line key for the chip dot colours. Sits at the bottom of
 * the expanded body, above nothing (it IS the bottom). Wraps to two
 * rows on very narrow viewports instead of truncating.
 * ------------------------------------------------------------------------ */

.lbs-legend {
	display: flex;
	flex-wrap: wrap;
	gap: 0.2rem 0.75rem;
	margin: 0.25rem 0 0;
	padding: 0.45rem 0.15rem 0;
	border-top: 1px dashed var(--lbs-border);
	color: var(--lbs-muted);
	font-size: 0.7rem;
	line-height: 1.3;
}
.lbs-legend__item {
	display: inline-flex;
	align-items: center;
	gap: 0.3rem;
}
.lbs-legend__dot {
	display: inline-block;
	width: 0.5rem;
	height: 0.5rem;
	border-radius: 50%;
	background: var(--lbs-muted);
	flex-shrink: 0;
}
/* Live and late dots both blink — they both represent REAL-TIME state
 * that's actively updating, so the pulse is an honest signal ("this
 * value is live, not cached"). Only `scheduled` and `cancelled` stay
 * static: those are end states, not in motion. */
.lbs-legend__dot--live {
	background: var(--lbs-ok);
	animation: lbs-blink 1.4s ease-in-out infinite;
}
.lbs-legend__dot--late {
	background: var(--lbs-warn);
	animation: lbs-blink 1.4s ease-in-out infinite;
}
.lbs-legend__dot--cancelled { background: var(--lbs-err); }
/* Match the actual scheduled chip-dot rendering (`.lbs-chip__dot`
 * sits at opacity 0.7 when no live data is available, and only goes
 * full-opacity when promoted to live/late). Without this the legend
 * swatch was visibly darker than the dot it claims to describe — the
 * "scheduled" colour the user sees on screen is the muted colour at
 * 0.7, not at 1.0. */
.lbs-legend__dot--scheduled {
	background: var(--lbs-muted);
	opacity: 0.7;
}

/* ---------- Empty / no-service state ------------------------------------ */

.lbs-stop__empty {
	display: flex;
	align-items: center;
	justify-content: center;
	min-height: 44px;
	padding: 0.4rem;
	color: var(--lbs-muted);
	font-size: 0.8rem;
	font-style: italic;
	border: 1px dashed var(--lbs-border);
	border-radius: var(--lbs-radius-sm);
}
/* Server-rendered placeholder before hydration — quieter than the
 * "no service" / "offline" states so it doesn't look alarming. */
.lbs-stop__empty--pending {
	font-style: normal;
	border-style: dotted;
	opacity: 0.6;
}

/* Terminus row: SSR'd in PHP and never replaced by JS. Shares the
 * same dashed-border / italic / muted layout as the "No service on
 * this day" empty state for visual consistency across all empty
 * chip rows. */
.lbs-stop__empty--final {
	/* Inherits .lbs-stop__empty layout. */
}

/* Per-line disruption / advisory pane (inside the expanded body,
 * just under the toolbar). Severity-driven colour, but kept low-key
 * so it informs without overwhelming the chip rows below it. */
.lbs-line__alerts {
	display: flex;
	flex-direction: column;
	gap: 0.4rem;
	margin: 0.25rem 0 0.6rem 0;
}
.lbs-line__alerts[hidden] { display: none; }
.lbs-line__alert-item {
	border: 1px solid var(--lbs-border);
	border-left-width: 3px;
	border-radius: var(--lbs-radius-sm);
	background: var(--lbs-surface-2);
	padding: 0.5rem 0.7rem;
}
.lbs-line__alert-item--critical { border-left-color: var(--lbs-err);  }
.lbs-line__alert-item--warning  { border-left-color: var(--lbs-warn); }
.lbs-line__alert-head {
	display: flex;
	align-items: baseline;
	gap: 0.5rem;
	flex-wrap: wrap;
}
.lbs-line__alert-tag {
	font-size: 0.7rem;
	font-weight: 600;
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--lbs-muted);
}
.lbs-line__alert-item--critical .lbs-line__alert-tag { color: var(--lbs-err); }
.lbs-line__alert-item--warning  .lbs-line__alert-tag { color: var(--lbs-warn); }
.lbs-line__alert-title {
	font-size: 0.9rem;
	font-weight: 600;
	color: var(--lbs-text);
}
.lbs-line__alert-body {
	margin: 0.3rem 0 0 0;
	font-size: 0.85rem;
	line-height: 1.4;
	color: var(--lbs-text);
}

/* ---------- Data-source disclaimer -------------------------------------- */

/* Sits at the bottom of the widget, below both sections. Kept muted so
 * it doesn't compete with live data, but readable (not a tooltip). On
 * desktop it visually caps the two-column grid above it. */
.lbs-disclaimer {
	margin-top: 1rem;
	padding: 0.65rem 0.85rem;
	background: var(--lbs-surface-2);
	border: 1px solid var(--lbs-border);
	border-radius: var(--lbs-radius-sm);
	color: var(--lbs-muted);
	font-size: 0.78rem;
	line-height: 1.5;
	text-align: center;
}
.lbs-disclaimer a {
	color: var(--lbs-secondary);
	text-decoration: underline;
	text-underline-offset: 2px;
}
.lbs-disclaimer a:hover,
.lbs-disclaimer a:focus-visible {
	color: var(--lbs-primary);
}

/* ---------- Bus-links (point A → point B) ------------------------------- */

/* Replacement for the legacy 3-column Gutenberg block (text | img | img)
 * that used to live at the bottom of the Bus page. The `the_content`
 * filter rewrites the columns into two CTA cards: each card is a single
 * <a> pointing at the external service, with a cropped hero preview, a
 * translated description, and the host as a visual CTA hint. */
.bus-links {
	/* Self-contained: the `--lbs-*` tokens are scoped to `.lbs` and this
	 * block sits outside that scope, so we redeclare what we need. */
	--bl-border: #dfe3ea;
	--bl-radius: 14px;
	--bl-text:   #1f2544;
	--bl-muted:  #3b4252;
	--bl-accent: var(--primary, rgb(28, 22, 68));

	max-width: 980px;
	margin: 2.5rem auto;
	color: var(--bl-text);
}
.bus-links > h2 {
	margin: 0 0 1.25rem;
	text-align: center;
	font-size: 1.25rem;
}
.bus-links__grid {
	display: grid;
	grid-template-columns: 1fr;
	gap: 1rem;
}

.bus-links__card {
	display: flex;
	flex-direction: column;
	background: #fff;
	border: 1px solid var(--bl-border);
	border-radius: var(--bl-radius);
	overflow: hidden;
	text-decoration: none;
	color: inherit;
	transition:
		transform .18s ease,
		box-shadow .18s ease,
		border-color .18s ease;
}
.bus-links__card:hover,
.bus-links__card:focus-visible {
	transform: translateY(-2px);
	box-shadow: 0 12px 28px -10px rgba(15, 23, 42, .22);
	border-color: transparent;
	outline: none;
}

.bus-links__hero {
	position: relative;
	aspect-ratio: 16 / 9;
	overflow: hidden;
	background: linear-gradient(135deg, #f6f7fb 0%, #eef0f6 100%);
}
.bus-links__hero figure { margin: 0; height: 100%; }
.bus-links__hero img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
	object-position: center top;
	transition: transform .4s ease;
}
.bus-links__card:hover .bus-links__hero img,
.bus-links__card:focus-visible .bus-links__hero img {
	transform: scale(1.03);
}

/* Brand-tinted hero backgrounds (shown behind letterboxed images). */
.bus-links__card--mobiliteit .bus-links__hero {
	background: linear-gradient(135deg, #fff2fa 0%, #ffe2f1 100%);
}
.bus-links__card--cfl .bus-links__hero {
	background: linear-gradient(135deg, #fff0f2 0%, #ffdde1 100%);
}

.bus-links__body {
	padding: 1rem 1.15rem 1.15rem;
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	flex: 1 1 auto;
}
.bus-links__body p {
	margin: 0;
	font-size: 0.95rem;
	line-height: 1.45;
	color: var(--bl-muted);
}
.bus-links__body p strong {
	color: #111827;
}

.bus-links__cta {
	margin-top: auto;
	padding-top: 0.4rem;
	display: inline-flex;
	align-items: center;
	gap: 0.4rem;
	font-size: 0.88rem;
	font-weight: 600;
	color: var(--bl-accent);
}
.bus-links__cta::after {
	content: "→";
	transition: transform .18s ease;
}
.bus-links__card:hover .bus-links__cta::after,
.bus-links__card:focus-visible .bus-links__cta::after {
	transform: translateX(4px);
}

@media (prefers-reduced-motion: reduce) {
	.bus-links__card,
	.bus-links__hero img,
	.bus-links__cta::after {
		transition: none;
	}
	.bus-links__card:hover,
	.bus-links__card:focus-visible,
	.bus-links__card:hover .bus-links__hero img,
	.bus-links__card:focus-visible .bus-links__hero img,
	.bus-links__card:hover .bus-links__cta::after,
	.bus-links__card:focus-visible .bus-links__cta::after {
		transform: none;
	}
}

/* ---------- Desktop scale-up -------------------------------------------- */

@media (min-width: 782px) {
	.lbs { margin: 1.5rem auto 2.5rem; }

	.lbs__sections { gap: 1.75rem; }

	.lbs-section__head {
		padding: 0.9rem 1.1rem;
		column-gap: 0.95rem;
		row-gap: 0.7rem;
	}
	.lbs-section__accent { width: 2.5rem; height: 2.5rem; }
	.lbs-section__title { font-size: 1.25rem; }
	.lbs-section__sub   {
		font-size: 0.9rem;
		padding-top: 0.7rem;
	}

	.lbs-disclaimer {
		margin-top: 1.5rem;
		font-size: 0.82rem;
	}

	.bus-links__grid {
		grid-template-columns: 1fr 1fr;
		gap: 1.5rem;
		align-items: stretch;
	}
	.bus-links > h2 { font-size: 1.5rem; margin-bottom: 1.5rem; }
	.bus-links__body { padding: 1.1rem 1.35rem 1.35rem; }

	.lbs-line__toggle { padding: 0.7rem 0.75rem; gap: 0.7rem; }
	.lbs-line__label  { font-size: 1rem; }
	.lbs-line__route  { font-size: 0.92rem; padding-left: 0.7rem; }
	.lbs-line__bounds { font-size: 0.85rem; }

	.lbs-line__content {
		padding: 0.7rem 0.9rem 0.85rem;
		gap: 0.55rem;
	}

	/* Stop becomes a true single row on desktop: dot | name | chips.
	 * The indicator is vertically centred (50%) so when a stop name
	 * wraps to two lines (e.g. "Kirchberg, Gare routière Luxexpo") the
	 * dot tracks with the text instead of sticking to the top. The
	 * connecting line's first/last clips switch to 50% too so the line
	 * always stops at the dot's center regardless of row height. */
	.lbs-stop {
		--lbs-stop-dot-x: 0.9rem;
		padding: 0.45rem 0 0.45rem 2rem;
		column-gap: 0.85rem;
	}
	.lbs-stop__place      { flex: 0 1 12rem; }
	.lbs-stop__name       { font-size: 0.92rem; }
	.lbs-stop__chips-slot { flex: 1 1 auto; min-width: 14rem; }

	.lbs-stop__indicator {
		top: 50%;
		transform: translateY(-50%);
	}
	.lbs-stop:first-child::before { top: 50%; bottom: 0; height: auto; }
	.lbs-stop:last-child::before  { top: 0; bottom: 50%; height: auto; }

	.lbs-chip__time { font-size: 0.95rem; }
	.lbs-legend     { font-size: 0.76rem; }

	/* ---------- Desktop chip row: arrows OUTSIDE, no fade ---------------
	 * Touch viewports use a swipe + mask-fade affordance. On desktop
	 * there's no horizontal swipe, so arrows need an explicit click
	 * target. Rather than floating them over the faded edges (which
	 * covers the first / last chip), on desktop we:
	 *   1. Lay out `.lbs-chips` as a flex row: [prev] [viewport] [next]
	 *   2. Drop the edge-fade entirely — arrows alone signal "more".
	 *   3. Pull the list's edge padding (which existed to give the
	 *      fade something to fade against).
	 *
	 * When `needArrows` is false the JS never creates the buttons, so
	 * the flex row collapses back to `[viewport]` at full width. When
	 * arrows exist but one end is `--hidden`, we reserve its space
	 * via `visibility: hidden` so the viewport doesn't jitter wider /
	 * narrower as the user scrolls to an edge.
	 * -------------------------------------------------------------------- */
	.lbs-chips {
		display: flex;
		align-items: center;
		gap: 0.4rem;
	}
	.lbs-chips__viewport {
		flex: 1 1 auto;
		min-width: 0;
	}
	.lbs-chips__list { padding-inline: 0; }
	.lbs-chips__viewport { scroll-padding-inline: 0; }

	/* Integer-fit chip widths on desktop.
	 *
	 * Without a fade to soften the viewport's right edge, a chip that
	 * only half-fits looks broken — sliced text like "48 m" where
	 * "min" got clipped by the viewport boundary. We don't want
	 * tiny chips either.
	 *
	 * JS (`wireCarousel` → `fitChips`) measures the viewport and
	 * picks the LARGEST chip width (≤ 88px, ≥ 72px) for which an
	 * integer number of chips plus gaps fits exactly into the
	 * available space. It writes that pixel value into the
	 * `--lbs-chip-w` custom property on the list. The chip then
	 * sizes itself off that var and ditches the mobile `max-width`
	 * cap so the computed width wins.
	 *
	 * The `clamp()` fallback keeps chips sensible during the tiny
	 * window between first paint and JS running, and for any
	 * future non-JS path. */
	.lbs-chips__list > .lbs-chip {
		flex: 0 0 var(--lbs-chip-w, clamp(72px, calc((100cqi - 3 * 0.3rem) / 4), 110px));
		max-width: none;
	}
	.lbs-chips--scrollable .lbs-chips__viewport,
	.lbs-chips--scrollable.lbs-chips--at-start .lbs-chips__viewport,
	.lbs-chips--scrollable.lbs-chips--at-end .lbs-chips__viewport {
		-webkit-mask-image: none;
		mask-image: none;
	}
	.lbs-chips__arrow {
		display: inline-flex;
		position: static;
		transform: none;
		flex-shrink: 0;
	}
	.lbs-chips__arrow:hover,
	.lbs-chips__arrow:focus-visible {
		transform: scale(1.05);
	}
	/* On desktop the arrows are part of the row's layout: [‹] [chips] [›].
	 * At a scroll extreme we DIM the arrow instead of hiding it so the
	 * user still sees both affordances on both sides of the chips.
	 * `visibility: visible` overrides the mobile default; pointer-events
	 * off + cursor:default makes the dimmed arrow non-interactive. */
	.lbs-chips__arrow--hidden {
		opacity: 0.3;
		visibility: visible;
		pointer-events: none;
		cursor: default;
		box-shadow: none;
	}
	.lbs-chips__arrow--hidden:hover,
	.lbs-chips__arrow--hidden:focus-visible {
		background: var(--lbs-surface);
		border-color: var(--lbs-border);
		color: var(--lbs-secondary);
		transform: none;
	}
}

/* ---------- Wide-desktop two-column grid --------------------------------
 * Two-column layout (city | regional) only kicks in once there's enough
 * horizontal room for each column to fully render a bus-line row without
 * clipping the route text. Below this threshold (≈ 780–1020 px) each
 * column was narrow enough that the RIGHT column (regional buses) got
 * visibly truncated — the line labels + route title no longer fit on a
 * single row and the ellipsis ate the destination text.
 *
 * The outer grid defines TWO rows — row 1 for the section headers,
 * row 2 for the line lists — and each `.lbs-section` uses
 * `grid-template-rows: subgrid` to inherit them, keeping the two
 * section headers at IDENTICAL heights regardless of heading length.
 * The disclaimer is a sibling of `.lbs__sections` so it spans full
 * width underneath.
 * ------------------------------------------------------------------------ */
@media (min-width: 1024px) {
	.lbs[data-sections-count="2"] .lbs__sections {
		display: grid;
		grid-template-columns: 1fr 1fr;
		grid-template-rows: auto 1fr;
		align-items: start;
		gap: 1.5rem 1.75rem;
	}
	.lbs[data-sections-count="2"] .lbs-section {
		display: grid;
		grid-row: span 2;
		grid-template-rows: subgrid;
		gap: 0.6rem;
	}
	.lbs[data-sections-count="2"] .lbs-section__head {
		align-self: stretch;
	}
}

/* ---------- Reduced motion ---------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	.lbs-chip__dot          { animation: none; }
	.lbs-legend__dot--live,
	.lbs-legend__dot--late  { animation: none; }
	.lbs-line__chev         { transition: none; }
	.lbs-timenav__arrow     { transition: none; }
	.lbs-toolbar__link      { transition: none; }
	.lbs-dir-swap__icon     { transition: none; }
}

/* =========================================================================
 * Alerts: page-top banner, per-line header badge, per-row chip pill.
 *
 * All three scopes share the same severity palette (critical=red,
 * warning=amber, info=blue) and honour `prefers-reduced-motion` — no
 * pulse/shimmer when the user prefers reduced motion.
 *
 * The banner self-mounts as `<aside id="lux-bus-alerts-banner">` just
 * before the widget (or the page's <main> / #content) so it's visible
 * above the fold on a cold load. It stays hidden until `bus-alerts-
 * banner.js` decides there's something worth surfacing (warning or
 * worse — `info`-only alerts never trigger the banner).
 * ========================================================================= */

.lux-bus-alerts-banner {
	--lbs-alert-fg:    #1f1f1f;
	--lbs-alert-bg:    #fff4e5;
	--lbs-alert-border:#f4a100;
	margin: 0.75rem 0 1rem;
	padding: 0.75rem 1rem;
	border-left: 4px solid var(--lbs-alert-border);
	background: var(--lbs-alert-bg);
	color: var(--lbs-alert-fg);
	border-radius: 6px;
	font: 500 0.95rem/1.35 var(--arhs-font, inherit);
}
.lux-bus-alerts-banner[data-severity="critical"] {
	--lbs-alert-bg:    #fde8e8;
	--lbs-alert-border:#c8102e;
}
.lux-bus-alerts-banner[hidden] { display: none !important; }

.lux-bus-alerts-banner__header {
	font-weight: 700;
	margin-bottom: 0.35rem;
	display: flex;
	align-items: center;
	gap: 0.45rem;
}
.lux-bus-alerts-banner__header::before {
	content: '';
	display: inline-block;
	width: 0.7rem;
	height: 0.7rem;
	border-radius: 50%;
	background: var(--lbs-alert-border);
	animation: lbs-alert-pulse 1.6s ease-in-out infinite;
}
@keyframes lbs-alert-pulse {
	0%, 100% { transform: scale(1);   opacity: 1; }
	50%      { transform: scale(1.3); opacity: .65; }
}
.lux-bus-alerts-banner__list {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 0.25rem;
}
.lux-bus-alerts-banner__item {
	display: flex;
	align-items: flex-start;
	gap: 0.5rem;
	padding-right: 1.75rem;
	position: relative;
}
.lux-bus-alerts-banner__dismiss {
	position: absolute;
	top: -0.15rem;
	right: 0;
	background: none;
	border: 0;
	cursor: pointer;
	font-size: 1.2rem;
	line-height: 1;
	color: inherit;
	padding: 0.25rem 0.4rem;
	min-width: 1.75rem;
	min-height: 1.75rem;
	border-radius: 4px;
}
.lux-bus-alerts-banner__dismiss:hover,
.lux-bus-alerts-banner__dismiss:focus-visible {
	background: rgba(0, 0, 0, 0.08);
	outline: none;
}

/* Per-line disruption badge, inside the accordion header. `info` stays
 * hidden; only warning + critical surface here. */
.lbs-line__alert {
	display: inline-block;
	flex: 0 0 auto;
	padding: 0.1rem 0.45rem;
	border-radius: 999px;
	font: 600 0.72rem/1.25 var(--arhs-font, inherit);
	text-transform: uppercase;
	letter-spacing: 0.02em;
	color: #fff;
	background: #f4a100;
	vertical-align: middle;
}
.lbs-line__alert[data-severity="critical"] { background: #c8102e; }
.lbs-line__alert[data-severity="warning"]  { background: #f4a100; }
.lbs-line__alert[hidden]                   { display: none !important; }

@media (max-width: 420px) {
	.lbs-line__alert {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		width: 1.35rem;
		height: 1.35rem;
		padding: 0;
		font-size: 0;
		line-height: 1;
	}
	.lbs-line__alert::before {
		content: "!";
		font-size: 0.78rem;
		font-weight: 800;
	}
}

/* Per-row alert pill on individual chips. Small, high-contrast, never
 * line-wraps. Full text is exposed via the tooltip + aria-label on the
 * chip; the visible text is intentionally compact for mobile carousels. */
.lbs-chip__alert {
	display: inline-block;
	grid-column: 1 / -1;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	margin-top: 0.15rem;
	padding: 0.05rem 0.4rem;
	border-radius: 999px;
	font: 600 0.65rem/1.25 var(--arhs-font, inherit);
	color: #fff;
	background: #1f6feb;
}
.lbs-chip__alert--warning  { background: #f4a100; color: #1f1f1f; }
.lbs-chip__alert--critical { background: #c8102e; }

@media (prefers-reduced-motion: reduce) {
	.lux-bus-alerts-banner__header::before { animation: none; }
}
