/*
 * MWPP Class Schedule — STRUCTURAL CORE stylesheet (Phase T1).
 *
 * D1 OWNERSHIP CONTRACT (THEME_SPEC §1): this file carries NO visual
 * opinion. It owns grid/table/float-layer MECHANICS, the geometry custom
 * properties' CONSUMPTION (not their real design values), behavior-state
 * WIRING (which elements .mwpp-show-times/:hover/.mwpp-filtering reveal
 * or hide — never what they look like once revealed), print SCAFFOLDING
 * (isolation + page geometry, never print colors), the 992px breakpoint,
 * and a NEUTRAL fallback token set so a missing/broken skin still
 * degrades to a legible, unbroken schedule.
 *
 * Every color, font, radius, shadow, scale, and spacing-rhythm value that
 * used to live in this file through 1.0.1 has moved to
 * assets/css/themes/{slug}.css, scoped under .mwpp-theme-{slug}. Classic
 * (themes/classic.css) is the exact extraction of 1.0.0's appearance —
 * loading core + classic.css must be pixel-identical to 1.0.0's single
 * file (verified: dev/verify-theme-parity.mjs).
 *
 * TEST FOR ANY FUTURE RULE ADDED HERE: "if this rule were deleted, would
 * GEOMETRY or BEHAVIOR break, or would it just look different?" Breakage
 * -> belongs here. Looks-different-only -> belongs in a skin.
 */

/* ============================================== 1. neutral fallback + geometry tokens */

.mwpp-page {
	/* NEUTRAL FALLBACK VALUES ONLY. A theme is a second, separately
	   enqueued stylesheet (assets/css/themes/{slug}.css) that loads AFTER
	   this one and restates every name below with its real design values
	   (see themes/classic.css section 2). If a skin ever fails to load —
	   a deactivated third-party theme plugin, a CDN hiccup, a broken
	   mwpp_themes filter registration dropped by MWPP_Themes's own
	   sanitizer — THESE are what actually render. They are deliberately
	   plain (generic grays, system fonts, 0 radius/shadow, scale 1) so a
	   missing skin is obviously undressed, never an accidental "looks
	   like a theme". Do not "improve" these toward any one theme's look. */
	--mwpp-dark: #333333;
	--mwpp-olive: #999999;
	--mwpp-green: #666666;
	--mwpp-light: #eeeeee;
	--mwpp-white: #ffffff;
	--mwpp-grid-line: #cccccc;

	--mwpp-font-display: system-ui, -apple-system, 'Segoe UI', sans-serif;
	--mwpp-font-body: system-ui, -apple-system, 'Segoe UI', sans-serif;

	--mwpp-sticker-border-w: 1px;
	--mwpp-sticker-shadow-offset: 0px;
	--mwpp-sticker-radius: 0px;
	--mwpp-sticker-hover-scale: 1;
	--mwpp-sticker-hover-width: auto;

	--mwpp-panel-radius: 0px;
	--mwpp-footer-radius: 0px;
	--mwpp-panel-border-w: 1px;

	--mwpp-pill-radius: 4px;

	--mwpp-logo-size: 80px;
	--mwpp-logo-size-mobile: 64px;
	--mwpp-logo-radius: 0px;
	--mwpp-logo-shadow: none;

	--mwpp-eyebrow-size: 12px;
	--mwpp-eyebrow-size-mobile: 11px;
	--mwpp-eyebrow-weight: 600;
	--mwpp-eyebrow-tracking: 0.05em;
	--mwpp-title-tracking: normal;
	--mwpp-title-line-height: 1.15;
	--mwpp-title-size-mobile: 1.5rem;

	/* STRUCTURAL, CSS-ONLY — deliberately NOT in either neighbouring
	   group. It is not an appearance token (no skin should restate it;
	   it is not a look), and it is not one of the PHP-computed geometry
	   values below (it has no MWPP_Constants counterpart — the mirror
	   image of PRINT_BASE_W_PX, which is PHP-only with no CSS var). It
	   sizes .mwpp-schedule-wrap's overflow-clip-margin: how far a
	   scale-hovered edge card may bleed past the wrap before being
	   clipped. See that rule (section 3) for the measurements. */
	--mwpp-hover-bleed: 96px;

	/* GEOMETRY DEFAULTS — structural, not theme. These match
	   MWPP_Constants exactly and exist only as a fallback for the rare
	   case the per-instance <style> block (MWPP_Renderer::render_style_block(),
	   which re-emits every one of these from MWPP_Constants::css_vars()
	   on every render, unconditional on theme) is somehow absent. No skin
	   may ever redeclare these names — geometry values are PHP-computed
	   data, never a design token (retheme contract, PLUGIN_SPEC §7.1). */
	--mwpp-row-h: 100px;
	--mwpp-spacer-h: 48px;
	--mwpp-head-h: 48px;
	--mwpp-col-time: 110px;
	--mwpp-col-day: 240px;
	--mwpp-col-day-narrow: 130px;
	--mwpp-card-h: 85px;
	--mwpp-card-inset: 4px;
	--mwpp-dual-gap: 2px;
	--mwpp-cell-gap: 1px;
	--mwpp-print-scale: 0.58;

	--mwpp-breakpoint: 992px;
	--mwpp-mat-count: 2;

	font-family: var(--mwpp-font-body);
	color: var(--mwpp-dark);
	box-sizing: border-box;
}

.mwpp-page *,
.mwpp-page *::before,
.mwpp-page *::after {
	box-sizing: inherit;
}

/* ===================================================== 2. header / legend / notes mechanics */

.mwpp-header {
	display: flex;
	/* Must hold one line at any desktop width (>=993px). flex-wrap:wrap
	   lets panels drop to their own line at ordinary narrow-desktop
	   widths, well above the mobile breakpoint (Phase 2 regression).
	   Nowrap + min-width:0 on the flex children below forces all three
	   panels to actually shrink together instead of wrapping — this is
	   mechanics, not appearance: a skin that widened a panel without
	   respecting min-width:0 would reintroduce the Phase 2 bug. */
	flex-wrap: nowrap;
	align-items: flex-start;
	justify-content: space-between;
}

.mwpp-brand {
	display: flex;
	align-items: center;
	min-width: 0;
	flex: 1 1 auto;
}

.mwpp-brand-text {
	min-width: 0;
}

.mwpp-header .mwpp-legend,
.mwpp-header .mwpp-notes {
	/* Shrink-together sizing only (same nowrap doctrine as .mwpp-header
	   above) — the panel's actual width ceiling, padding, and border are
	   a skin's appearance call. */
	flex: 1 1 200px;
	min-width: 0;
}

/* ===================================================== 3. table + cell mechanics */

.mwpp-schedule-wrap {
	/* position:relative anchors .mwpp-float-layer (absolute) to the
	   table's own box — required regardless of theme, and unaffected by
	   overflow (an abs containing block comes from `position`, not from
	   the overflow value).

	   EDGE-BLEED FIX (v1.4.1). The scale-hover themes grow a card to 1.5x
	   in place; on a RIGHT-edge (Saturday) card that pushes 15px (993px)
	   to 74px (3840px) past the wrap's right edge. Under the previous
	   `overflow-x: auto` that overhang became scrollable overflow and the
	   wrap sprouted a horizontal scrollbar. `overflow: clip` is NOT a
	   scroll container, so no scrollbar can ever render, and
	   overflow-clip-margin pushes the clip edge outward so the card
	   BLEEDS past the border instead — the intended behaviour, matching
	   what already happens vertically.

	   WHY LOSING THE HORIZONTAL SCROLL IS SAFE (measured, not assumed —
	   full numbers in CLAUDE.md): `.mwpp-schedule` is table-layout:fixed
	   + width:100% with PERCENTAGE column widths, so the table can never
	   be wider than this wrap. Verified by narrowing the container to
	   1200/1000/900/700/500/300px with the viewport held >=993px (the
	   narrow-sidebar case): the table measured exactly the wrap's client
	   width EVERY time, and at a 500px container all 8 day headers —
	   Saturday included — were inside the wrap. The old scroll therefore
	   never revealed a hidden day column; it only exposed ~8-40px of card
	   CONTENT bleed (.mwpp-name-abbr / .mwpp-mat-badge sticking out of a
	   cell), which the clip margin now simply shows instead. Do not
	   "restore" overflow-x:auto believing it was load-bearing.

	   WHY NOT `overflow-x: visible; overflow-y: clip` (the other legal
	   non-coercing pair — plain x:visible/y:hidden coerces x back to auto
	   per CSS Overflow 3): visible overflow PROPAGATES to the document,
	   so the hovered card moved the scrollbar from the wrap to the PAGE
	   (measured: documentElement scrollWidth 1024 -> 1032) rather than
	   eliminating it. `clip` does not propagate.

	   The overflow-x/-y longhands are kept ahead of the shorthand on
	   purpose: a browser without `overflow: clip` support (Safari < 16)
	   drops the two clip declarations as invalid and degrades to exactly
	   the previous auto/hidden behaviour. Do not collapse them away.

	   --mwpp-hover-bleed sizes the margin: 96px clears the 73.9px worst
	   case measured at a 3840px viewport with headroom. It only PERMITS
	   bleed — at rest nothing in any theme overflows this box on any
	   side (measured 0px top/right/bottom across all six slotted skins),
	   so a generous value costs nothing. There is no per-axis
	   overflow-clip-margin longhand in Chrome, so it applies to all four
	   sides; harmless for exactly that reason. */
	position: relative;
	overflow-x: auto;
	overflow-y: hidden;
	overflow: clip;
	overflow-clip-margin: var( --mwpp-hover-bleed, 96px );
}

.mwpp-schedule {
	/* LOAD-BEARING, not a style choice (unchanged from 1.0.0): column
	   widths are PERCENTAGES of the table's own width (--mwpp-col-*-pct,
	   emitted per-instance by MWPP_Renderer from MWPP_Layout_Engine's
	   design_width() — schedule-specific because narrow_days changes the
	   denominator). table-layout:fixed treats an explicit non-percentage
	   <col> width as a MINIMUM the table can never shrink below (CSS2.1
	   §17.5.2.1); percentage widths carry no such floor, so the table
	   genuinely fills 100% of whatever column it's placed in. */
	width: 100%;
	table-layout: fixed;
	border-collapse: separate;
	border-spacing: var(--mwpp-cell-gap);
}

/* POST-T3 FIX (CLAUDE.md): a real WordPress/Kadence environment measured
   .mwpp-schedule-wrap 38px taller than table.mwpp-schedule inside it — not
   reproducible against the seed schedule with no theme loaded, because it
   depends on a theme rule this plugin never had a reset for. Root cause,
   confirmed with a real browser (a `table { margin-bottom: ... }` rule —
   a common theme/editor default for content-area tables — added on top of
   a real render): .mwpp-schedule-wrap's own `overflow-x: auto` establishes
   a new block-formatting context, which means the table's margin no longer
   collapses OUT of the wrap the way it would against a plain block parent
   — it stays INSIDE the wrap's auto-height instead, inflating wrap's
   rendered height while table's own getBoundingClientRect() is completely
   unaffected (margin is outside the border box). Table is explicitly named
   in the 0.1.7 branding-fix "GENERAL RULE" (CLAUDE.md) as an element
   themes commonly style, so this reset needs more than one class of
   specificity to reliably beat a theme's own `table` / `.entry-content
   table` rule — same doctrine as that fix, applied to a new property.
   Does NOT touch float geometry: float slot position is computed from
   .mwpp-schedule-wrap's own box (percentages of design_width(), consumed
   via .mwpp-float-layer which is anchored to the WRAP, not the table), so
   neutralizing the table's margin here cannot shift a float slot — it can
   only prevent a theme's table margin from silently doing so. */
.mwpp-page .mwpp-schedule {
	margin: 0;
}

.mwpp-col-time {
	width: var(--mwpp-col-time-pct, var(--mwpp-col-time));
}

.mwpp-col-day {
	width: var(--mwpp-col-day-pct, var(--mwpp-col-day));
}

.mwpp-col-day-narrow {
	width: var(--mwpp-col-day-narrow-pct, var(--mwpp-col-day-narrow));
}

.mwpp-schedule thead th {
	height: var(--mwpp-head-h);
	text-align: center;
	vertical-align: middle;
}

/* Full-bleed clickable header cell: width/height:100% + padding:0 are
   structural (the button must fill the th exactly, both for the click
   target and for visual alignment with every other th); the reset
   properties (font/background/border/cursor) neutralize UA default
   button chrome that would otherwise break that fill regardless of
   theme — this is normalization, not brand appearance. */
.mwpp-time-header button {
	font-family: inherit;
	font-size: inherit;
	font-weight: inherit;
	color: inherit;
	background: none;
	border: none;
	cursor: pointer;
	width: 100%;
	height: 100%;
	padding: 0;
}

.mwpp-time-cell {
	height: var(--mwpp-row-h);
}

.mwpp-time-start,
.mwpp-time-range {
	/* STRUCTURAL SAFETY, same doctrine as .mwpp-card below: this cell's
	   `height` is a floor, not a ceiling, in table layout, so wrapping
	   text here could grow the row and desync every V(t)-computed float
	   position for every row below it. The time column is narrow (~7% of
	   the table) and a custom row label is admin-entered free text, so
	   this is defensive, not theoretical — must never be removed by a skin. */
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

.mwpp-day-cell {
	/* height: row geometry consumption. position:relative: hosts the
	   optional .mwpp-slot-time-overlay (absolute) for this cell. padding:0
	   + vertical-align:top: the cell's box must be an exact, predictable
	   container for .mwpp-cell-cards' flex fill — part of the same
	   row-height-integrity guarantee as the max-height rule below. */
	height: var(--mwpp-row-h);
	vertical-align: top;
	padding: 0;
	position: relative;
}

.mwpp-cell-cards {
	/* The slot flex layout itself (D1 names this explicitly as core-owned
	   mechanics): cards flex evenly across the cell, stretched to fill
	   its height. The 2px inset is a fixed structural gap between cards
	   and the cell border, consistent for every theme's slot math. */
	display: flex;
	align-items: stretch;
	gap: var(--mwpp-dual-gap);
	height: 100%;
	padding: 2px;
}

.mwpp-spacer-row {
	height: var(--mwpp-spacer-h);
	padding: 0;
}

/* ===================================================== 4. card mechanics + safety */

.mwpp-card {
	position: relative;
	flex: 1 1 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	overflow: hidden;
	/* Flex auto-min-size floor (CSS Flexbox §4.5): a flex item's automatic
	   minimum size is content-based UNLESS overflow is non-visible OR
	   min-width:0 is set. Core's overflow:hidden above happens to zero that
	   floor here — but a glow skin (nightfall/daybreak) legitimately overrides
	   the card to overflow:visible so its ::before/::after glow can bleed
	   outside the box, which silently REINSTATES the min-content floor: the
	   two cards in a table-layout:fixed dual-mat cell then refuse to shrink
	   and paint over their neighbor. Setting min-width:0 explicitly guarantees
	   shrinkability independent of any skin's overflow value, so glow themes
	   shrink exactly like clipped themes and .mwpp-name-abbr's own line-clamp
	   can take effect. Structural (D1), not appearance — must never be removed
	   by a skin. */
	min-width: 0;
}

/* STRUCTURAL SAFETY RULE, not a design token — and deliberately scoped to
   CELL cards only, never a bare .mwpp-card. A table row's CSS `height` is
   only a FLOOR, not a ceiling — if a cell card's own content needed more
   room than the row (e.g. wrapped text in a narrow day column) the <tr>
   would grow to match, desyncing every row below it from
   MWPP_Layout_Engine's V(t) pixel offsets (rows are assumed to be exactly
   ROW_H_PX every time V(t) is computed server-side), corrupting the float
   layer's positions for the rest of the table. Capped at the ROW height
   (not the card's own design height) so NO skin can ever re-trigger the
   Phase 2 row-height-corruption bug — a skin may make its CELL cards
   SHORTER than the row (themes/classic.css sets the actual
   height: var(--mwpp-card-h), 85px, comfortably under this 100px ceiling);
   nothing a skin does may make the row grow. Must remain `max-height`,
   never `min-height` (min-height is exactly the floor that caused the
   original bug).
   MUST NOT apply to float cards: an R2/R3 float-group card is SUPPOSED to
   exceed one row's height (that is the entire point of the float layer —
   a 90-minute card is ~113-143px tall by design, spec §6.4) and floats are
   absolutely positioned outside the table's box model, so nothing about
   them can corrupt row heights the way a cell card could. A bare
   `.mwpp-card { max-height: ... }` rule would silently clip every R2/R3
   card back down to one row's height regardless of its legitimate inline
   height — this reproduced concretely in dev/verify-theme-parity.mjs
   before being scoped correctly here. */
.mwpp-day-cell .mwpp-card {
	max-height: var(--mwpp-row-h);
}

/* D3 GEOMETRY DELIVERY (THEME_SPEC §1 D3, Phase T2): float card height
   arrives as the inline CUSTOM PROPERTY --mwpp-fcard-h (see
   MWPP_Renderer::card_html()), not a literal inline `height`. It is set
   ONLY on R2/R3 (duration-derived) float cards.
   POST-T3 REVERSION (CLAUDE.md "ISSUE 1"): Phase T2 originally also set
   --mwpp-fcard-h on an R1 card riding a slot alongside a taller R2/R3
   groupmate, stretching it to the slot's own height so it wouldn't look
   squashed next to its groupmate. Mike reversed that after seeing it live
   — an R1 rider now carries no --mwpp-fcard-h at all and falls through to
   the var(--mwpp-card-h) fallback below, same fixed 85px as any other R1
   card. The var(--mwpp-card-h) fallback is therefore load-bearing again
   (not merely defensive) for every R1 float rider, in addition to still
   covering the case where a float card somehow carries no --mwpp-fcard-h.
   SPECIFICITY: .mwpp-page is prefixed deliberately (0,3,0, not 0,2,0) —
   unchanged from the pre-D3 reasoning this replaces. Every skin scopes
   its own rules under .mwpp-theme-{slug} (D1 contract), so a skin's
   `.mwpp-theme-{slug} .mwpp-card { height: ... }` is ALSO a 2-class
   selector — equal to a bare `.mwpp-float-slot .mwpp-card`, which means
   the skin (loaded after core) would win the tie and silently clip every
   float card back to its cell-card design height. Reproduced concretely
   by dev/verify-theme-parity.mjs against themes/classic.css before this
   fix. The 3-class form reliably beats any 2-class skin rule regardless
   of that skin's own scoping, while still losing (correctly, no
   !important needed) to a FUTURE skin that deliberately opts out of
   duration-derived float heights (THEME_SPEC D3 — Mono, T4) by targeting
   this exact `.mwpp-theme-{slug} .mwpp-float-slot .mwpp-card` combination
   — also 0,3,0, so it ties and wins on load order (skins load after
   core). This is the D3 "clean override point" the retheme contract
   promises: a skin restyles the RULE, never fights an inline style. */
.mwpp-page .mwpp-float-slot .mwpp-card {
	height: var(--mwpp-fcard-h, var(--mwpp-card-h));
}

.mwpp-name-mono {
	/* THEME_SPEC D2: rendered for every theme, unconditionally — hidden
	   by default here so only a theme that actually uses it (Mono, T4)
	   needs to say anything (a single scoped rule flipping it back to
	   block). Every other theme, including a missing/broken skin,
	   inherits this neutral default and simply never shows it. */
	display: none;
}

.mwpp-name-abbr {
	/* STRUCTURAL SAFETY, same doctrine as .mwpp-card's max-height: cell
	   cards are height-capped, so a long abbr in a narrow day column can
	   never grow the row — clamp with an ellipsis so it degrades
	   gracefully instead of being cut off mid-character by the card's
	   overflow:hidden.

	   WHY 3 AND NOT 2 (v1.4.2, RESPONSIVE_ASSESSMENT.md). The clamp was 2
	   through v1.4.1. Measured across all eight themes at seven widths:
	   day COLUMNS are percentage-width so cards scale continuously with
	   the viewport (Classic card 100.6px at 1277 vs 206.8px at 2560),
	   while this clamp, the row height and every font size are fixed. At
	   1277 the card's own vertical budget for the name is 67-84px — three
	   to five lines — and horizontal ink over-run was 0 of 41 in EVERY
	   theme. The names were not too wide and the cards were not too small;
	   the clamp was cutting off a third line the card had room for
	   (41/41 cards in classic/dojo/nightfall/daybreak/meridian).

	   This value is NOT a design token and no skin should restate it to
	   taste — it is the same row-height-integrity guarantee as
	   .mwpp-day-cell .mwpp-card's max-height, which still caps the row at
	   var(--mwpp-row-h) and is what actually prevents growth. 3 stays
	   comfortably inside that cap at every shipped name size.

	   INERT IN TWO THEMES, BY THEIR OWN DESIGN — do not "fix" this:
	   Mono renders .mwpp-name-mono at rest (a monogram, one line), and
	   Transit sets display:block on its rest label (positioned outside the
	   station box, nowrap). A competing `display` value defeats
	   -webkit-line-clamp silently, with no parse error, so both compute a
	   clamp they never apply. Kraft restates 3 in its own skin already and
	   Daybreak restates 3 mobile-only; neither is affected by this line.

	   ONE ACCEPTED EXCEPTION TO "INERT ABOVE THE CRISIS BAND" (measured,
	   accepted by Mike at the v1.4.2 Gate 1 review — do not re-investigate
	   as a mystery). Seven of eight themes render BYTE-IDENTICALLY at 2560
	   before and after this change. Dojo differs by EIGHT pixels, all in
	   column x=2559 (the single rightmost pixel column), on the top-right
	   and bottom-right curves of its border-radius:12px wrap, max channel
	   delta 1/255. Verified NOT to be capture noise (three consecutive
	   captures byte-identical), NOT to be the two-server test rig (same
	   trees on both ports diff to 0 pixels), and NOT a layout change: a
	   full-float-precision A/B found .mwpp-page/.mwpp-schedule-wrap/
	   .mwpp-schedule/thead/last-th/.mwpp-footer/scrollHeight all identical
	   and 0 of 33 name boxes changed. It is a paint-level antialiasing
	   artifact on a rounded corner, not a design change. */
	display: -webkit-box;
	-webkit-line-clamp: 3;
	-webkit-box-orient: vertical;
	overflow: hidden;

	/* BADGE RESERVE (v1.4.2). MECHANISM ONLY — inert by default, D5
	   "define, don't apply": core states HOW a skin may drop the resting
	   name clear of its mat badge; only a skin that sets the token is
	   affected, and a skin that sets nothing renders exactly as before.

	   WHY IT IS NEEDED. .mwpp-card is `flex-direction:column;
	   justify-content:center` and .mwpp-mat-badge is position:absolute,
	   i.e. OUT OF FLOW — so the name is centred in the card's whole
	   content box and a third line pushes the FIRST line UP, into the band
	   the badge occupies. Raising the clamp to 3 above therefore put glyph
	   cap-tops under the badge on 5-15 cards per theme below ~1440px
	   (measured: dojo -5.8px, daybreak -3.7px, nightfall -2.7px; classic
	   stayed clear at +1.4px). dev/verify-nightfall-hover.mjs item 5 is the
	   only assertion in the tree that catches this — do not weaken it.

	   WHY MARGINS AND NOT position:relative + top. The obvious form was
	   rejected after measuring: it makes the name a POSITIONED element,
	   which paints above in-flow static content, and Dojo's and
	   Nightfall's badges are absolute while their names are not — so it
	   silently FLIPS badge-vs-name paint order in exactly two themes.
	   (Daybreak and Kraft already give `.mwpp-card > div` z-index:1, so
	   they would not have changed, which is what makes the trap easy to
	   miss.) A +P/-P margin pair shifts the border box down by exactly P
	   while the item's OUTER size stays h + P - P = h, so flex centring is
	   unchanged, nothing is positioned, and no stacking context appears.
	   Verified geometrically identical to the position:relative form at
	   1024/1277/2560 on all three themes.

	   WHY IT COSTS NOTHING. This does not reduce the height available to
	   the name, so every third line the clamp above buys survives: content
	   height measured unchanged (dojo 67, nightfall 69, daybreak 67) and
	   the three-line counts are identical before and after.

	   HOVER-SAFE BY CONSTRUCTION: .mwpp-name-abbr is display:none on
	   hover, so this rule cannot reach the hover path at all. Measured:
	   hovered-name offset moved 0.0px on all four scale(1.5) themes.

	   NOT for Kraft — see its own note. Kraft is the one theme whose card
	   cannot hold three lines at any offset. */
	margin-top: var( --mwpp-badge-reserve, 0px );
	margin-bottom: calc( -1 * var( --mwpp-badge-reserve, 0px ) );
}

/* Resting-state visibility mechanic for the hover-revealed pair — WHICH
   elements swap is core; what they look like once shown is a skin's job
   (themes/classic.css restates font/size/weight for both). */
.mwpp-name-full,
.mwpp-time-line {
	display: none;
}

@media (min-width: 993px) {
	/* Hover z-index bookkeeping is BEHAVIOR MECHANICS, not appearance —
	   assets/js/mwpp-schedule.js's HOVER_Z_INDEX constant duplicates this
	   exact value (Phase 3 doctrine: "if either value is retuned, retune
	   both together"). A skin may restyle the hover treatment (transform,
	   shadow, width) but must never touch z-index here. */
	.mwpp-card:hover {
		z-index: 20;
	}

	/* The abbr <-> full/time-line swap-on-hover mechanic itself (which
	   elements the browser reveals) — same doctrine as .mwpp-show-times
	   below. A skin restyles what "revealed" looks like, never whether. */
	.mwpp-card:hover .mwpp-name-abbr {
		display: none;
	}

	.mwpp-card:hover .mwpp-name-full,
	.mwpp-card:hover .mwpp-time-line {
		display: block;
	}
}

/* ===================================================== 5. float layer mechanics */

.mwpp-float-layer {
	position: absolute;
	top: var(--mwpp-head-h);
	left: 0;
	right: 0;
	bottom: 0;
	pointer-events: none;
}

.mwpp-float-slot {
	position: absolute;
	display: flex;
	align-items: stretch;
	gap: var(--mwpp-dual-gap);
	pointer-events: none;
}

/* D3 GEOMETRY DELIVERY (THEME_SPEC §1 D3, Phase T2): slot POSITION is
   schedule data, never a theme choice (D1 contract) — unlike float card
   height above, there is no legitimate skin opt-out here. top/left/width
   arrive as inline custom properties (--mwpp-slot-top/-left/-w, see
   MWPP_Renderer::render_float_layer()); this is the only rule that
   consumes them. There is deliberately no --mwpp-slot-h / height
   property: every card in the slot now carries its own explicit height
   (the rule above), so this flex container's own height is already the
   tallest card's height with nothing left to state separately.
   SPECIFICITY: the extra .mwpp-float-layer ancestor class makes this
   0,3,0 — one level higher than a skin's natural, documented scoping
   convention (`.mwpp-theme-{slug} .mwpp-float-slot`, 0,2,0) — so this
   always wins outright, not merely on a load-order tie, and a skin
   cannot move a float slot without !important. */
.mwpp-page .mwpp-float-layer .mwpp-float-slot {
	top: var(--mwpp-slot-top);
	left: var(--mwpp-slot-left);
	width: var(--mwpp-slot-w);
}

.mwpp-float-slot .mwpp-card {
	pointer-events: auto;
}

/* ===================================================== 6. desktop/mobile switching + mobile structure */

.mwpp-mobile-header,
.mwpp-mobile-notes,
.mwpp-legend-hint,
.mwpp-mobile-legend,
.mwpp-mobile-schedule {
	display: none;
}

@media (max-width: 992px) {
	.mwpp-header,
	.mwpp-schedule-wrap {
		display: none;
	}

	.mwpp-mobile-header {
		display: flex;
		flex-direction: column;
		align-items: center;
	}

	.mwpp-mobile-notes {
		display: block;
	}

	.mwpp-legend-hint {
		display: block;
	}

	.mwpp-mobile-legend {
		/* display:grid is the switching mechanic (base state above is
		   display:none); the column pattern itself is a "legend pattern"
		   choice, owned by the skin per D1 (themes/classic.css supplies
		   grid-template-columns). Without a skin this still renders --
		   one implicit column per item, functional but plain. */
		display: grid;
	}

	.mwpp-mobile-schedule {
		display: block;
	}

	/* Structural: mat-count-driven grid, tied directly to schedule data
	   via --mwpp-mat-count (emitted per-instance) — part of the "mobile
	   list structure" D1 names explicitly as core-owned. */
	.mwpp-mobile-mat-headers,
	.mwpp-mobile-day-row {
		display: grid;
		grid-template-columns: repeat(var(--mwpp-mat-count), 1fr);
		gap: var(--mwpp-dual-gap);
	}

	.mwpp-mat-section {
		display: flex;
		flex-direction: column;
		gap: var(--mwpp-dual-gap);
	}

	/* Mobile cards opt out of the desktop cell-card sizing model
	   entirely (structural: no row-height-integrity constraint applies
	   here, since mobile is a free-flowing stacked list, not a table) —
	   same "mobile is row-independent" doctrine as spec §6.5. */
	.mwpp-mobile-schedule .mwpp-card {
		flex: none;
		width: 100%;
		min-height: 0;
		height: auto;
	}

	/* Mobile always shows full name + time, never abbr/badge (spec §6.5)
	   — the same swap-state doctrine as the desktop hover mechanic above,
	   just permanently applied rather than triggered by :hover. */
	.mwpp-mobile-schedule .mwpp-mat-badge,
	.mwpp-mobile-schedule .mwpp-name-abbr {
		display: none;
	}

	.mwpp-mobile-schedule .mwpp-name-full,
	.mwpp-mobile-schedule .mwpp-time-line {
		display: block;
	}
}

/* ===================================================== 7. filter/time-toggle state wiring */

/* The .mwpp-filtering/.mwpp-match/.mwpp-dim/.mwpp-show-times class names
   and the elements assets/js/mwpp-schedule.js toggles them on are the
   STABLE CONTRACT (THEME_SPEC §7.3) — never renamed, repurposed, or made
   theme-conditional.

   POST-GATE FIX (LocalWP finding, see CLAUDE.md): the FILTER dim/match/
   wiggle mechanic itself — .mwpp-filtering, .mwpp-match, .mwpp-dim — was
   originally moved entirely to themes/classic.css on the theory that "a
   missing skin just shows no visual dimming, which is degraded-but-not-
   broken." That is wrong for THIS mechanic specifically, unlike the purely
   decorative rules elsewhere: without ANY visible feedback, a click
   produces no perceptible change on desktop at all, which reads to a real
   user as "the filter doesn't work" — not as a graceful degradation. THESE
   THREE THEREFORE GET A WORKING BASELINE HERE IN CORE (the exact values
   1.0.0 always used — opacity 0.18 / grayscale(0.4) / the wiggle
   animation), so filtering is functionally AND visibly correct under any
   theme, including a missing/broken one. A skin MAY still override these
   values in its own scope (higher specificity via .mwpp-theme-{slug}) if
   it wants a different dim/highlight treatment; Classic does not, since
   these values already match its own 1.0.0 design exactly.

   The desktop legend button's own ACTIVE-PILL colors (background/text on
   .mwpp-legend-item[aria-pressed="true"]) remain themes/classic.css's
   appearance call — that part is decorative, not the primary "did
   anything happen" signal, and stays out of core intentionally. */

.mwpp-filtering .mwpp-card:not(.mwpp-match) {
	opacity: 0.18;
	filter: grayscale(0.4);
	transition: opacity 0.2s ease, filter 0.2s ease;
}

.mwpp-card.mwpp-match {
	animation: mwpp-wiggle 0.3s ease-in-out 3;
}

@keyframes mwpp-wiggle {
	0%, 100% {
		transform: rotate(0deg);
	}
	25% {
		transform: rotate(-2deg);
	}
	75% {
		transform: rotate(2deg);
	}
}

.mwpp-legend-item.mwpp-dim {
	opacity: 0.3;
}

/* FLIP row collapse/expand (v1.3.7, assets/js/mwpp-schedule.js). The JS adds
   this class to a <tr> or .mwpp-float-slot ONLY for the ~200ms it is being
   FLIP-animated (Web Animations API) and removes it on finish/cancel, so the
   compositor hint is transient — every row is never promoted to its own layer
   at rest. No CSS transition is declared here on purpose: the slide is driven
   entirely by element.animate(), and prefers-reduced-motion is handled in JS
   (it skips the FLIP and uses the instant collapse), so no @media
   (prefers-reduced-motion) guard is needed in CSS. */
.mwpp-flip-anim {
	will-change: transform;
}

.mwpp-slot-time-overlay {
	/* Positioning MECHANICS only (D1 names this explicitly: "how
	   .mwpp-slot-time-overlay positions per its two DOM contexts").
	   For a CELL group it is a sibling of .mwpp-cell-cards inside the
	   already-position:relative .mwpp-day-cell, so it can be absolute;
	   bottom:0 within the cell. For a FLOAT group it is a child of
	   .mwpp-float-slot (itself absolutely positioned), so it positions
	   relative to the slot instead. Both contexts share this one rule. */
	display: none;
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 2;
	pointer-events: none;
}

.mwpp-show-times .mwpp-slot-time-overlay {
	display: block;
}

/* Case 3 (single) and Case 2 (mixed/dualMixed): each card shows its own
   time_line. Case 1 (shared) is excluded below — the overlay above
   covers it instead, so two identical lines don't render side by side. */
.mwpp-show-times .mwpp-card .mwpp-time-line {
	display: block;
}

.mwpp-show-times [data-group-type="shared"] .mwpp-card .mwpp-time-line {
	display: none;
}

/* ===================================================== 8. print scaffolding */

@media print {
	@page {
		size: landscape;
		margin: 0.4in;
	}

	/* There is no standard, cross-browser CSS selector that suppresses
	   pages after the first (CSS Paged Media has no such pseudo-class) —
	   "pages after the first suppressed" is achieved instead by scaling
	   the fixed PRINT_BASE_W_PX-wide content down to fit inside one
	   printable landscape page via the transform below, so nothing
	   overflows onto a second page in the first place. */
	body {
		visibility: hidden;
	}

	.mwpp-page,
	.mwpp-page * {
		visibility: visible;
	}

	.mwpp-page {
		position: fixed;
		left: 0;
		top: 0;
		/* MWPP_Constants::PRINT_BASE_W_PX (1680) — the one geometry
		   constant with no --mwpp-* var, same as BOTTOM_OVERHANG_PX: a
		   PHP-side layout constant that never needed a CSS custom
		   property of its own. Print SCALE (a scaffolding constant, not
		   a color) stays core; a skin may still restate its own print
		   colors within its own @media print block. */
		width: 1680px;
		transform: scale(var(--mwpp-print-scale));
		transform-origin: top left;
		-webkit-print-color-adjust: exact;
		print-color-adjust: exact;
	}

	.mwpp-header .mwpp-legend,
	.mwpp-header .mwpp-notes,
	.mwpp-mobile-header,
	.mwpp-mobile-notes,
	.mwpp-legend-hint,
	.mwpp-mobile-legend,
	.mwpp-mobile-schedule {
		display: none !important;
	}

	.mwpp-header,
	.mwpp-schedule-wrap {
		display: block !important;
	}

	.mwpp-brand {
		display: flex !important;
	}

	/* Print always shows times, as if the toggle were permanently on. */
	.mwpp-card .mwpp-time-line {
		display: block !important;
	}

	.mwpp-slot-time-overlay {
		display: block !important;
	}

	[data-group-type="shared"] .mwpp-card .mwpp-time-line {
		display: none !important;
	}

	/* Cancels ANY theme's hover transform for print, regardless of what
	   value that theme's own :hover rule set — !important beats a skin's
	   non-important value no matter which file loaded when, so this one
	   rule works generically across every theme without needing to know
	   its hover treatment. */
	.mwpp-card:hover {
		transform: none !important;
		width: auto !important;
	}
}

/* ===================================================== 9. proportional layout mode (structural template)

   THEME_SPEC §1 addendum A2 (Meridian, Phase T6). The SECOND structural
   template — a real calendar grid of positioned divs replacing the slotted
   table + float layer. It lives in the CORE because layout MODE is
   structure, not appearance: it is inert unless the container carries
   .mwpp-layout-proportional (added by MWPP_Renderer only for a
   layout_mode:'proportional' theme). A skin (themes/meridian.css) supplies
   ALL appearance — colors, fonts, gridline/band/bar looks — and MUST NOT
   restate any position/size/width rule here (D1 ownership contract).

   ALL geometry arrives as inline CUSTOM PROPERTIES the renderer emits from
   MWPP_Layout_Engine::proportional_plan() (--mwpp-prop-total-h,
   --mwpp-prop-gridline-top, --mwpp-prop-band-top/-h, --mwpp-prop-bar-top/-h)
   — same D3 doctrine as the slotted float layer: geometry is PHP-computed
   data delivered as variables this template consumes, never a theme token.

   ALIGNMENT: the day-name header row (.mwpp-prop-head) is OUTSIDE the
   V-positioned .mwpp-prop-canvas, so it never offsets the coordinate
   origin. Inside the canvas, .mwpp-prop-axis and .mwpp-prop-plot are
   siblings of equal height (total_h); axis labels, hour gridlines, break
   bands, and bars all resolve their top against the same top:0 — so a bar
   at V(start) top-aligns with the hour gridline at V(hour). */

.mwpp-prop-grid {
	/* Fixed hour-axis column width, shared by the header corner and the
	   axis so their columns line up. Structural, not a design token — a
	   skin must not change it (it would desync the header from the canvas). */
	--mwpp-prop-axis-w: 64px;
	display: block;
}

/* Header row: axis corner + 7 day headers — same column split as the
   canvas below, so day headers sit above their columns. */
.mwpp-prop-head {
	display: flex;
}

.mwpp-prop-head-corner {
	flex: 0 0 var(--mwpp-prop-axis-w);
}

.mwpp-prop-head-day {
	flex: 1 1 0;
	min-width: 0;
}

.mwpp-prop-lane-captions {
	display: flex;
}

.mwpp-prop-lane-caption {
	flex: 1 1 0;
	min-width: 0;
}

/* The V-positioned region: fixed height = the plan's total_h. */
.mwpp-prop-canvas {
	display: flex;
	height: var(--mwpp-prop-total-h);
}

.mwpp-prop-axis {
	position: relative;
	flex: 0 0 var(--mwpp-prop-axis-w);
}

.mwpp-prop-axis-label {
	/* Top-aligned to its hour gridline — NO vertical transform (gate #2
	   asserts axis-label top ≈ gridline top). */
	position: absolute;
	left: 0;
	right: 0;
	top: var(--mwpp-prop-gridline-top);
}

.mwpp-prop-plot {
	position: relative;
	flex: 1 1 0;
	min-width: 0;
	display: flex;
}

/* Full-width overlays spanning all 7 day columns (one gridline per hour,
   one break band per compressed gap). Their VISUAL (line color, band fill)
   is the skin's job; only their position/size is structural. */
.mwpp-prop-gridline {
	position: absolute;
	left: 0;
	right: 0;
	top: var(--mwpp-prop-gridline-top);
}

.mwpp-prop-break-band {
	position: absolute;
	left: 0;
	right: 0;
	top: var(--mwpp-prop-band-top);
	height: var(--mwpp-prop-band-h);
	display: flex;
	align-items: center;
	justify-content: center;
}

.mwpp-prop-day-col {
	position: relative;
	flex: 1 1 0;
	min-width: 0;
	display: flex;
}

.mwpp-prop-lane {
	position: relative;
	flex: 1 1 0;
	min-width: 0;
}

.mwpp-prop-bar {
	position: absolute;
	left: 0;
	right: 0;
	top: var(--mwpp-prop-bar-top);
	height: var(--mwpp-prop-bar-h);
}

/* The .mwpp-card fills its bar's geometry box. 3-class (.mwpp-page prefix)
   so a skin's 2-class `.mwpp-theme-{slug} .mwpp-card` rule can never break
   this structural fill — same specificity doctrine as the float-card height
   rule in §4. The card keeps its data-category fill (per-instance style
   block) and every JS behavior class, exactly as in slotted mode. */
.mwpp-page .mwpp-prop-bar .mwpp-card {
	width: 100%;
	height: 100%;
}

@media (min-width: 993px) {
	/* Hover elevation MECHANIC (structural, mirrors the slotted
	   .mwpp-card:hover z-index in §4 at the same value): the whole bar
	   rises so its hover reveal / popover chip clears neighbouring bars.
	   Lanes and day columns are position:relative with z-index:auto, so
	   they create no stacking context and every bar shares the plot's —
	   this reliably lifts the hovered bar above all the others. */
	.mwpp-page .mwpp-prop-bar:hover {
		z-index: 20;
	}
}

/* Desktop-only, exactly like the slotted grid: below the breakpoint the
   proportional grid is hidden and the shared mobile list (§6) takes over.
   No new mobile markup — Meridian reuses the row-independent mobile list. */
@media (max-width: 992px) {
	.mwpp-prop-grid {
		display: none;
	}
}

@media print {
	/* The proportional template is taller than slotted content, so it prints
	   at its own scale (MWPP_Constants::PROP_PRINT_SCALE via
	   --mwpp-prop-print-scale) at its own base width
	   (MWPP_Constants::PROP_PRINT_BASE_W_PX = 1680, a literal here for the
	   same reason PRINT_BASE_W_PX is in §8 — a PHP layout constant with no
	   CSS var). Keyed on the mode-marker class so the core stays theme-slug-
	   blind (THEME_SPEC A1: print scale is per LAYOUT MODE, not per skin);
	   it beats the generic .mwpp-page print rule in §8 on specificity. */
	.mwpp-prop-grid {
		display: block !important;
	}

	.mwpp-page.mwpp-layout-proportional {
		width: 1680px;
		transform: scale(var(--mwpp-prop-print-scale));
	}
}

/* ===================================================== 10. route layout mode (structural template)

   THEME_SPEC §1 / Phase T11 (Transit). The THIRD structural template — a metro
   map of positioned divs on a shared HORIZONTAL time axis, replacing both the
   slotted table + float layer (§3/§5) and the proportional grid (§9). Like §9 it
   lives in the CORE because layout MODE is structure, not appearance: it is
   inert unless the container carries .mwpp-layout-route (added by MWPP_Renderer
   only for a layout_mode:'route' theme). A skin (themes/transit.css) supplies
   ALL appearance — colors, fonts, line/dot/roundel/chip/label looks — and MUST
   NOT restate any position/size rule here (D1 ownership contract).

   ALL geometry arrives as inline CUSTOM PROPERTIES the renderer emits from
   MWPP_Layout_Engine::transit_plan() (--mwpp-transit-total-w, -tick-x,
   -break-x/-w, -station-x, -segment-x/-w) — same D3 doctrine as the float layer
   and the proportional grid: geometry is PHP-computed data delivered as
   variables this template consumes, never a theme token.

   COORDINATE MODEL: every route row is [ roundel gutter | track ]. The axis
   track, the grid overlay, and all 7 day tracks share the SAME width
   (--mwpp-transit-total-w) and the SAME left origin (after the fixed
   --mwpp-route-gutter), so a station at H(start) sits under the hour tick at
   H(hour) and over the hairline at H(hour). The one break-band element and the
   hour hairlines live in a single full-height overlay (.mwpp-route-grid) offset
   past the gutter, so they drop through all 7 day rows (gate #3 counts exactly
   one .mwpp-route-break).

   THE CARD IS THE LIT SEGMENT: each .mwpp-route-station is positioned at
   H(start) and sized to the segment width (H(end)−H(start)); the .mwpp-card
   inside fills it, so the card's own per-instance data-category background
   paints the lit track segment directly — visible even with no skin, exactly as
   the proportional bars are. The skin adds the ring-dot (a card pseudo pulling
   the category hue via background-color:inherit, the Kraft/Nightfall technique)
   and positions the abbr/time labels above or below the line per
   data-label-above. */

.mwpp-route-wrap {
	/* Fixed roundel-gutter width, shared by the axis gutter and every day
	   roundel so the tracks line up under the axis; and the lane/line row
	   metrics. Structural, not design tokens — a skin must not change the gutter
	   (it would desync the grid overlay and axis from the day tracks). */
	--mwpp-route-gutter: 72px;
	--mwpp-route-lane-h: 64px;
	--mwpp-route-line-h: 8px;
	position: relative;
	overflow-x: auto;
}

/* Axis: a gutter spacer + a track of hour-tick labels, same column split as a
   day row so the labels sit above their tracks. */
.mwpp-route-axis {
	display: flex;
	position: relative;
}

.mwpp-route-axis-gutter {
	flex: 0 0 var(--mwpp-route-gutter);
}

.mwpp-route-axis-track {
	position: relative;
	flex: 0 0 var(--mwpp-transit-total-w);
}

.mwpp-route-tick {
	position: absolute;
	left: var(--mwpp-transit-tick-x);
}

.mwpp-route-body {
	position: relative;
}

/* One full-height overlay for the hour hairlines + the compressed-gap break
   band(s), offset past the roundel gutter so it aligns with the day tracks and
   drops through all 7 rows. */
.mwpp-route-grid {
	position: absolute;
	left: var(--mwpp-route-gutter);
	top: 0;
	bottom: 0;
	width: var(--mwpp-transit-total-w);
	pointer-events: none;
}

.mwpp-route-hairline {
	position: absolute;
	top: 0;
	bottom: 0;
	left: var(--mwpp-transit-tick-x);
}

.mwpp-route-break {
	position: absolute;
	top: 0;
	bottom: 0;
	left: var(--mwpp-transit-break-x);
	width: var(--mwpp-transit-break-w);
}

/* Day row: roundel badge + track of the same width/origin as the axis track. */
.mwpp-route-day {
	display: flex;
	align-items: stretch;
	position: relative;
}

.mwpp-route-roundel {
	flex: 0 0 var(--mwpp-route-gutter);
	display: flex;
	align-items: center;
	justify-content: center;
}

.mwpp-route-track {
	position: relative;
	flex: 0 0 var(--mwpp-transit-total-w);
	display: flex;
	flex-direction: column;
}

/* One lane per mat (a double track when mats > 1). */
.mwpp-route-mat {
	position: relative;
	height: var(--mwpp-route-lane-h);
}

/* Lane caption (Transit, dual-mat only — the renderer emits it only then):
   sits in the roundel gutter, right-aligned against the lane's left edge and
   vertically centred on the lane (right:100% pushes its right edge to the
   lane's left edge; it extends left into the gutter). Appearance — font, size,
   colour — is the skin's (D1). */
.mwpp-route-mat-caption {
	position: absolute;
	right: 100%;
	top: 50%;
	transform: translateY(-50%);
	margin-right: 8px;
	text-align: right;
	white-space: nowrap;
	pointer-events: none;
}

/* The base route line spanning the full track — a day/mat with no classes shows
   only this thin stub line. Vertically centered in the lane. */
.mwpp-route-line {
	position: absolute;
	left: 0;
	right: 0;
	top: 50%;
	height: var(--mwpp-route-line-h);
	transform: translateY(-50%);
}

/* Station anchor: positioned at H(start), sized to the lit-segment width. */
.mwpp-route-station {
	position: absolute;
	left: var(--mwpp-transit-station-x);
	top: 0;
	bottom: 0;
	width: var(--mwpp-transit-segment-w);
}

/* The card fills the station horizontally = the lit segment [H(start),H(end)],
   centered on the line. 3-class (.mwpp-page prefix) so a skin's 2-class
   `.mwpp-theme-{slug} .mwpp-card` rule can't break this structural fill — same
   specificity doctrine as §4/§9. The card keeps its data-category fill (the
   per-instance style block) and every JS behavior class, exactly as in the other
   two modes; overflow is visible so the skin can hang the ring-dot and the
   above/below labels outside the thin segment box. */
.mwpp-page .mwpp-route-station .mwpp-card {
	position: absolute;
	left: 0;
	right: 0;
	top: 50%;
	height: var(--mwpp-route-line-h);
	transform: translateY(-50%);
	overflow: visible;
}

/* Interchange connector: at a shared H(start), spanning the day's mat lanes
   (its visual — the elongated ring — is the skin's job). */
.mwpp-route-interchange {
	position: absolute;
	left: var(--mwpp-transit-station-x);
	top: 0;
	bottom: 0;
}

@media (min-width: 993px) {
	/* Hover elevation MECHANIC (structural, mirrors §4/§9 at the same value): the
	   whole station rises so its hover chip clears neighbouring stations. Mats,
	   tracks, and day rows are position:relative with z-index:auto, so they
	   create no stacking context and every station shares the body's — this
	   reliably lifts the hovered station above all the others. */
	.mwpp-page .mwpp-route-station:hover {
		z-index: 20;
	}
}

/* Below the breakpoint the desktop route diagram is hidden. Unlike the other two
   modes (which fall back to the shared §6 list), a route theme replaces that list
   with its own VERTICAL metro map (.mwpp-route-mobile-wrap) — the desktop diagram
   rotated 90°: a shared left time axis + seven day COLUMNS, stations placed by
   time DOWN each column. Structural template only (positions/sizes from the inline
   --mwpp-transit-mobile-* custom properties the renderer scales from the plan);
   the skin (transit.css) supplies all appearance (D1). */
.mwpp-route-mobile-wrap {
	display: none; /* shown only <=992px (below); hidden on desktop */
}

@media (max-width: 992px) {
	.mwpp-route-wrap {
		display: none;
	}

	.mwpp-route-mobile-wrap {
		display: block;
	}

	/* Fixed mobile metrics (structural, shared by axis + columns so ticks line up
	   with stations). --mwpp-route-mobile-line-x/-w set the vertical line + segment
	   bar geometry; a skin must not change these (it would desync the grid overlay
	   and axis from the columns). */
	.mwpp-route-mobile-inner {
		--mwpp-route-mobile-axis-w: 46px;
		--mwpp-route-mobile-head-h: 30px;
		--mwpp-route-mobile-lane-w: 92px;
		--mwpp-route-mobile-line-x: 18px;
		--mwpp-route-mobile-line-w: 6px;
		display: flex;
		align-items: flex-start;
	}

	/* Left time axis (stays pinned while the columns scroll horizontally, since it
	   is a sibling of the overflow-x scrollport, not inside it). */
	.mwpp-route-mobile-axis {
		flex: 0 0 var(--mwpp-route-mobile-axis-w);
		position: relative;
	}
	.mwpp-route-mobile-axishead {
		height: var(--mwpp-route-mobile-head-h);
	}
	.mwpp-route-mobile-axistrack {
		position: relative;
		height: var(--mwpp-transit-mobile-total-h);
	}
	.mwpp-route-mobile-tick {
		position: absolute;
		top: var(--mwpp-transit-mobile-tick-y);
		right: 0;
	}
	/* Half-hour (:30) marker — axis-only, positioned like the hour tick; its size
	   and colour (a short muted dash) are the skin's job. No label, no hairline. */
	.mwpp-route-mobile-halftick {
		position: absolute;
		top: var(--mwpp-transit-mobile-tick-y);
		right: 0;
	}

	/* The seven day columns scroll horizontally here; the page scrolls vertically
	   for the tall time axis. As a day CAROUSEL (Phase 2), this is the scroll-snap
	   container: one day fills the port, neighbours peek by --mwpp-route-mobile-peek.
	   container-type:inline-size makes it the containment context so the day width's
	   100cqw resolves against THIS scrollport (the flex-sized area right of the
	   pinned axis), never the viewport — that is what keeps the horizontal scroll
	   inside the body and stops the PAGE gaining an x-scrollbar. */
	.mwpp-route-mobile-body {
		flex: 1 1 auto;
		min-width: 0;
		overflow-x: auto;
		scroll-snap-type: x mandatory;
		container-type: inline-size;
		overscroll-behavior-x: contain;
		scroll-padding-inline: var(--mwpp-route-mobile-peek);
	}
	.mwpp-route-mobile-track {
		position: relative;
		display: flex;
		align-items: flex-start;
		width: max-content;
		/* Leading/trailing room so the first (Sun) and last (Sat) day can snap to
		   centre identically to the interior days. */
		padding-inline: var(--mwpp-route-mobile-peek);
	}

	/* One overlay across all columns: hour hairlines + the compressed break band,
	   offset past the roundel header so it aligns with the column tracks. */
	.mwpp-route-mobile-grid {
		position: absolute;
		left: 0;
		right: 0;
		top: var(--mwpp-route-mobile-head-h);
		height: var(--mwpp-transit-mobile-total-h);
		pointer-events: none;
	}
	.mwpp-route-mobile-hairline {
		position: absolute;
		left: 0;
		right: 0;
		top: var(--mwpp-transit-mobile-tick-y);
	}
	.mwpp-route-mobile-break {
		position: absolute;
		left: 0;
		right: 0;
		top: var(--mwpp-transit-mobile-break-y);
		height: var(--mwpp-transit-mobile-break-h);
	}

	/* Day column: roundel header + a track of one lane per mat. As a carousel snap
	   item (Phase 2) each day is a uniform width = the scrollport (100cqw of the
	   body) minus a peek on each side, so exactly one day fills the port and the
	   two neighbours peek. scroll-snap-stop:always = one day per swipe (no momentum
	   skipping). The explicit width overrides the flex:0 0 auto content basis; the
	   inner lane width (--mwpp-route-mobile-lane-w) is unchanged — lanes just centre
	   within the wider column. */
	.mwpp-route-mobile-day {
		position: relative;
		flex: 0 0 auto;
		width: calc(100cqw - 2 * var(--mwpp-route-mobile-peek));
		scroll-snap-align: center;
		scroll-snap-stop: always;
	}
	.mwpp-route-mobile-roundel {
		height: var(--mwpp-route-mobile-head-h);
		display: flex;
		align-items: center;
		justify-content: center;
	}
	.mwpp-route-mobile-col {
		position: relative;
		height: var(--mwpp-transit-mobile-total-h);
		display: flex;
		align-items: flex-start;
		/* Shrink the col to exactly its mat lane(s) (fit-content = 1 or 2 * lane-w)
		   and centre it in the day column. Centring the WHOLE col — not the mats via
		   justify-content — keeps the interchange connector (an absolute CHILD of the
		   col, anchored line-to-line) moving with the lanes, so it stays aligned; a
		   bare justify-content:center would shift the mats but leave the connector at
		   the col's left edge. */
		width: fit-content;
		margin-inline: auto;
	}

	/* Interchange connector: the mobile analog of the desktop .mwpp-route-interchange
	   — a horizontal bar at a shared-start y connecting the day's two mat lanes.
	   Structural position anchor only (top = the shared-start y; left/right default
	   to the full column). Its horizontal SPAN (constrained line-to-line by the
	   skin), height, colour, and layering (z-index:-1) are the skin's job — the same
	   core-anchors / skin-shapes split the desktop connector uses. */
	.mwpp-route-mobile-interchange {
		position: absolute;
		left: 0;
		right: 0;
		top: var(--mwpp-transit-mobile-interchange-y);
	}
	.mwpp-route-mobile-mat {
		position: relative;
		flex: 0 0 var(--mwpp-route-mobile-lane-w);
		height: var(--mwpp-transit-mobile-total-h);
	}
	.mwpp-route-mobile-line {
		position: absolute;
		top: 0;
		bottom: 0;
		left: var(--mwpp-route-mobile-line-x);
		width: var(--mwpp-route-mobile-line-w);
		transform: translateX(-50%);
	}

	/* Station anchor: placed at its scaled time position, sized to the scaled
	   segment height. 3-class on the card fill (.mwpp-page) so a skin's 2-class
	   rule cannot break the structural bar; overflow visible so the skin can hang
	   the ring-dot + labels outside the thin segment. */
	.mwpp-route-mobile-station {
		position: absolute;
		top: var(--mwpp-transit-mobile-station-y);
		height: var(--mwpp-transit-mobile-segment-h);
		left: 0;
		right: 0;
	}
	.mwpp-page .mwpp-route-mobile-station .mwpp-card {
		position: absolute;
		top: 0;
		bottom: 0;
		left: var(--mwpp-route-mobile-line-x);
		width: var(--mwpp-route-mobile-line-w);
		transform: translateX(-50%);
		overflow: visible;
	}
}

@media (min-width: 993px) {
	.mwpp-route-mobile-wrap {
		display: none;
	}
}

@media print {
	/* The route diagram is WIDE, so it prints at its own scale
	   (MWPP_Constants::TRANSIT_PRINT_SCALE via --mwpp-transit-print-scale) at its
	   own base width (TRANSIT_PRINT_BASE_W_PX = 1680, a literal here for the same
	   reason PRINT_BASE_W_PX is in §8). Keyed on the mode-marker class so the
	   core stays theme-slug-blind (A1); it beats the generic .mwpp-page print
	   rule in §8 on specificity. overflow is made visible so the full-width
	   diagram is not clipped by the scrollport. */
	.mwpp-route-wrap {
		display: block !important;
		overflow-x: visible !important;
	}

	.mwpp-page.mwpp-layout-route {
		width: 1680px;
		transform: scale(var(--mwpp-transit-print-scale));
	}
}
