diff --git a/src/App.tsx b/src/App.tsx index 6c8ff1e..b773de9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,7 @@ import type { ComponentProps, CSSProperties } from 'react'; import type { Alert, BoardData, Config, Departure, Scene, Settings } from './types'; import { activeAlerts, isFresh, minutesUntil, shouldHoldScene, snapshotUsable } from './lib/board.mjs'; import { hasSavedSettings, readSettings, saveSettings } from './lib/storage'; -import { useBoard, useClock, useTimetable, useWeather, fallbackConfig } from './hooks'; +import { useBoard, useClock, useMediaQuery, useTimetable, useWeather, fallbackConfig } from './hooks'; import { SettingsPanel } from './SettingsPanel'; import { useDeviceControl } from './useDeviceControl'; import type { ManagedState } from './useDeviceControl'; @@ -37,6 +37,8 @@ const clockFormat = new Intl.DateTimeFormat('ru', { timeZone: 'Europe/Minsk', ho const dateFormat = new Intl.DateTimeFormat('ru', { timeZone: 'Europe/Minsk', weekday: 'long', day: 'numeric', month: 'long' }); const timeFormat = new Intl.DateTimeFormat('ru', { timeZone: 'Europe/Minsk', hour: '2-digit', minute: '2-digit' }); const noDepartures: Departure[] = []; +// A vertical stop display, not a phone: phones keep the narrow layout. +const SIGNAGE_PORTRAIT = '(orientation: portrait) and (min-width: 701px)'; const sceneNames: Record = { departures: 'Ближайшие рейсы', info: 'Информация', ad: 'Реклама' }; function Countdown({ departure, now, large = false }: { departure: Departure; now: number; large?: boolean }) { @@ -72,10 +74,14 @@ export function DeparturesScreen({ data, departures, now, alerts, loading, rowCo const table = useRef(null); const [tableHeight, setTableHeight] = useState(360); const [page, setPage] = useState(0); + const portrait = useMediaQuery(SIGNAGE_PORTRAIT); const capacity = Math.max(1, Math.floor(tableHeight / reading.rowHeight)); + // A vertical display has room for more trips than the landscape row count; + // the operator's count stays the minimum, the viewing distance sets the row size. + const requested = portrait ? Math.max(rowCount, Math.min(8, Math.floor(tableHeight / (reading.rowHeight * 2.4)))) : rowCount; const remaining = departures.filter(departure => departure.id !== first?.id); - const visible: { items: DisplayDeparture[]; start: number; count: number; pages: number; size: number } = departurePage(remaining, rowCount, capacity, page); - const rows = Math.max(1, Math.min(rowCount, visible.size)); + const visible: { items: DisplayDeparture[]; start: number; count: number; pages: number; size: number } = departurePage(remaining, requested, capacity, page); + const rows = Math.max(1, Math.min(requested, visible.size)); useLayoutEffect(() => { if (!table.current) return; const observer = new ResizeObserver(entries => setTableHeight(entries[0].contentRect.height)); @@ -130,16 +136,19 @@ function InfoScreen({ data, alerts, fresh, seconds, paused, page, onPage, onAdva // Cards shown during this visit; the scene remounts on every visit. const [shown, setShown] = useState(1); const activeBlock = blocks[page % blocks.length]?.id; - // Four or five cards form two rows; up to three share one. - const rows = blocks.length > 3 ? 2 : 1; - const columns = Math.ceil(blocks.length / rows); + const portrait = useMediaQuery(SIGNAGE_PORTRAIT); + // Landscape: four or five cards form two rows, up to three share one. Portrait: + // two columns from four cards (five use the journey card's double height), else a stack. + const many = blocks.length > 3; + const columns = portrait ? many ? 2 : 1 : Math.ceil(blocks.length / (many ? 2 : 1)); + const rows = portrait ? many ? Math.ceil((blocks.length + blocks.length % 2) / 2) : blocks.length : many ? 2 : 1; useEffect(() => { if (alerts.length || !settings.infoPlacesEnabled) onPlacePages(1); }, [alerts.length, settings.infoPlacesEnabled, onPlacePages]); useEffect(() => { onCompact(compact); }, [compact, onCompact]); useLayoutEffect(() => { if (!grid.current) return; const observer = new ResizeObserver(entries => { const { width, height } = entries[0].contentRect; - setCompact(width < 1000 || innerHeight > innerWidth || innerHeight < 650 || (rows > 1 ? (height - INFO_GAP) / rows < INFO_CARD_MIN_HEIGHT : height < 330) + setCompact(width < 1000 || innerHeight < 650 || (rows > 1 ? (height - INFO_GAP * (rows - 1)) / rows < INFO_CARD_MIN_HEIGHT : height < 330) || settings.displayProfile === 'street' || settings.viewingDistance === 'far'); }); observer.observe(grid.current); diff --git a/src/PassengerInfo.tsx b/src/PassengerInfo.tsx index 2b6958e..dcace91 100644 --- a/src/PassengerInfo.tsx +++ b/src/PassengerInfo.tsx @@ -41,7 +41,7 @@ export function PlacesCard({ trips, now, paused, active, settings, onPages }: { return <>

До важных мест

{visible.map((trip, index) =>
- {trip.departure ?
+ {trip.departure ?
{trip.minutes !== null && {trip.minutes === 0 ? '<1 мин' : durationParts(trip.minutes).map(part => {part.unit})}}
:

Ближайший прямой рейс не найден

}
diff --git a/src/hooks.ts b/src/hooks.ts index 3ba55c8..cbd88af 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useSyncExternalStore } from 'react'; import type { BoardData, Config, Settings, Timetable, Weather } from './types'; import { createDemo } from './lib/demo'; import { isBoardData } from './lib/board.mjs'; @@ -111,3 +111,11 @@ export function useClock(offset: number) { }, [offset]); return now; } + +export function useMediaQuery(query: string) { + return useSyncExternalStore(change => { + const list = matchMedia(query); + list.addEventListener('change', change); + return () => list.removeEventListener('change', change); + }, () => matchMedia(query).matches); +} diff --git a/src/styles.css b/src/styles.css index 508dc22..c560074 100644 --- a/src/styles.css +++ b/src/styles.css @@ -10,6 +10,8 @@ svg { display: block; flex-shrink: 0; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } .board { --background: #000000; --foreground: #f5f5f5; --muted: #b7b7b7; --secondary: #c3c3c3; --line: #303030; --outline: #595959; --surface: #0a0a0a; --input: #141414; --warning: #ffd479; --accent-ink: #000; --hero-height: clamp(110px, 18vh, 230px); --hero-gap: clamp(18px, 2.6vh, 32px); --control-size: 40px; --control-gap: 6px; color-scheme: dark; color: var(--foreground); background: var(--background); height: 100dvh; width: 100%; display: flex; flex-direction: column; overflow: hidden; padding: clamp(14px, 2vh, 28px) clamp(18px, 3vw, 64px) 0; } +/* Accent means the nearest trip: its badge, countdown, onward route and the cards about it. + Everything else uses the foreground and grey tokens. Controls keep it for state and focus. */ .board[data-theme='light'] { --background: #FFFFFF; --foreground: #171b23; --muted: #535b68; --secondary: #535b68; --line: #d7dbe2; --outline: #939ba8; --surface: #fff; --input: #f2f4f7; --warning: #8a4300; --accent-ink: #fff; color-scheme: light; } .board[data-row-count='5'], .board[data-row-count='6'] { --hero-height: clamp(100px, 15vh, 180px); } .board-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; flex: 0 0 auto; border-bottom: 1px solid var(--line); padding-bottom: clamp(12px, 1.8vh, 24px); view-transition-name: board-header; } @@ -17,9 +19,10 @@ svg { display: block; flex-shrink: 0; } h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1.08; margin: 0; font-weight: 650; overflow-wrap: anywhere; } .stop-meta { display: flex; align-items: center; gap: 10px 24px; flex-wrap: wrap; } .stop-direction { color: var(--muted); display: flex; align-items: center; gap: 10px; font-size: clamp(18px, 1.6vw, 32px); margin-top: 10px; line-height: 1.2; } -.stop-direction svg { color: var(--accent); } +.stop-direction svg { color: var(--muted); } .weather-summary { display: flex; align-items: center; gap: 9px; margin-top: 10px; color: var(--muted); font-size: clamp(16px, 1.3vw, 26px); line-height: 1.2; } -.weather-summary svg, .weather-summary strong { color: var(--accent); } +.weather-summary svg { color: var(--muted); } +.weather-summary strong { color: var(--foreground); } .weather-summary strong { font-size: 1.25em; font-weight: 600; } .weather-summary small { font-size: .7em; } .header-right { display: grid; grid-template-columns: auto auto; column-gap: clamp(20px, 3vw, 64px); align-items: center; flex-shrink: 0; } @@ -45,7 +48,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .route-badge-large { width: calc(var(--hero-height) * 1.05); height: calc(var(--hero-height) - 10px); border: 0; background: var(--accent); color: var(--accent-ink); border-radius: calc(var(--hero-height) * .27); padding: 0 clamp(16px, 1.4vw, 28px); font-size: min(130px, calc(var(--hero-height) * .68)); } .route-badge-filled { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); } .hero-destination { min-width: 0; height: 100%; display: flex; flex-direction: column; justify-content: center; } -.transport-kind { display: flex; align-items: center; gap: 9px; color: var(--accent); font-size: min(30px, calc(var(--hero-height) * .16)); line-height: 1.2; margin-bottom: 7px; } +.transport-kind { display: flex; align-items: center; gap: 9px; color: var(--secondary); font-size: min(30px, calc(var(--hero-height) * .16)); line-height: 1.2; margin-bottom: 7px; } .hero-destination h2 { flex-shrink: 0; font-size: min(4.7vw, calc(var(--hero-height) * .34)); line-height: 1.08; letter-spacing: -.035em; font-weight: 650; margin: 0; overflow-wrap: anywhere; } .hero-details { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: min(27px, calc(var(--hero-height) * .12)); color: var(--secondary); margin-top: 8px; line-height: 1.25; } .hero-details > span { min-width: 0; } @@ -62,7 +65,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .countdown-large .countdown-now { color: var(--accent); font-size: min(72px, calc(var(--hero-height) * .44)); } .next-heading { border-top: 1px solid var(--line); padding: clamp(10px, 1.4vh, 18px) 0; } .next-heading svg { color: var(--muted); } -.page-indicator { color: var(--accent); font-size: 15px; } +.page-indicator { color: var(--secondary); font-size: 15px; } .departure-table { min-height: 0; display: grid; grid-template-rows: repeat(var(--rows), minmax(0, 1fr)); overflow: hidden; } .departure-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: clamp(16px, 2.2vw, 44px); min-height: 0; padding: 5px 0; border-bottom: 1px solid var(--line); } .departure-row:last-child { border-bottom: 0; } @@ -74,7 +77,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .row-time-compact { display: none; } .transport-notice { display: flex; align-items: center; gap: 10px; color: var(--warning); padding-top: 10px; font-size: clamp(15px, 1.4vw, 26px); flex: 0 0 auto; } .empty-departures { display: flex; flex-direction: column; justify-content: center; min-height: 0; } -.empty-departures > svg { color: var(--accent); } +.empty-departures > svg { color: var(--muted); } .empty-departures h2 { font-size: clamp(24px, 3vw, 54px); font-weight: 550; margin: 10px 0; letter-spacing: -.04em; } .empty-departures p, .empty-following { color: var(--secondary); font-size: clamp(14px, 1.5vw, 28px); margin: 0; line-height: 1.4; } .quiet-placeholder { display: none; } @@ -90,7 +93,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .info-intro h2 { margin: 0; font-size: clamp(28px, 3vw, 56px); letter-spacing: -.045em; font-weight: 550; } .information-tabs { display: none; gap: 8px; } .information-tabs button { font-size: 14px; border: 1px solid var(--outline); background: transparent; padding: 8px 12px; border-radius: 10px; color: var(--secondary); } -.information-tabs button[aria-current='page'] { color: var(--accent-ink); border-color: var(--accent); background: var(--accent); } +.information-tabs button[aria-current='page'] { color: var(--background); border-color: var(--foreground); background: var(--foreground); } .info-grid { flex: 1 1 0; min-height: 0; display: grid; grid-template-columns: repeat(var(--info-columns, 3), minmax(0, 1fr)); grid-template-rows: repeat(var(--info-rows, 1), minmax(0, 1fr)); gap: 24px; } /* Five cards: the journey rail takes both rows of the first column instead of leaving a hole. */ .info-screen[data-blocks='5']:not([data-compact='true']) .service-info { grid-row: span 2; } @@ -99,7 +102,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .info-card h3 { font-size: clamp(23px, 2vw, 38px); line-height: 1.15; font-weight: 600; letter-spacing: -.03em; margin: 0 0 18px; } .info-card h3.card-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .card-heading .route-badge { width: auto; min-width: 2.3em; height: 1.5em; padding: 0 .4em; font-size: .85em; border-radius: .34em; } -.card-icon { color: var(--accent); margin-bottom: 16px; } +.card-icon { color: var(--secondary); margin-bottom: 16px; } .card-icon svg { width: 32px; height: 32px; } .info-card p { color: var(--secondary); font-size: clamp(18px, 1.75vw, 32px); line-height: 1.4; margin: 0 0 16px; } .info-caption { display: flex; align-items: center; gap: 9px; font-size: clamp(14px, 1.3vw, 23px); color: var(--secondary); margin-top: auto; line-height: 1.4; } @@ -115,7 +118,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .service-info .journey-empty { margin-block: auto; padding: 20px 0; text-align: center; font-size: 22px; } .journey-metro { --station-width: clamp(180px, 22cqw, 260px); --focused-width: min(clamp(300px, 60cqw, 560px), calc(100cqw - 48px)); flex: 1 1 0; min-height: 0; min-width: 0; display: flex; flex-direction: column; padding-top: clamp(16px, 4cqh, 32px); } .journey-tour-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; color: var(--secondary); font-size: clamp(17px, 3.3cqw, 24px); line-height: 1.3; } -.journey-order { color: var(--accent); font-size: .85em; white-space: nowrap; } +.journey-order { color: var(--secondary); font-size: .85em; white-space: nowrap; } .journey-map-space { flex: 1 1 0; min-height: 0; display: flex; align-items: center; margin: 18px -8px 0; } .journey-rail-window { width: 100%; height: min(100%, 320px); position: relative; overflow: hidden; } .journey-rail-window::after { content: ''; position: absolute; inset: 0 0 0 auto; width: 36px; z-index: 3; background: linear-gradient(90deg, transparent, var(--background)); pointer-events: none; } @@ -148,7 +151,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .journey-continuation-line { position: absolute; bottom: 17.5px; left: 0; right: 24px; border-top: 5px dashed var(--accent); opacity: .5; } .journey-caption { display: flex; flex-wrap: wrap; gap: 4px 14px; justify-content: space-between; color: var(--muted); font-size: 16px; line-height: 1.3; padding-top: 16px; } .journey-caption-compact { display: none; } -.journey-page { color: var(--accent); margin-left: auto; } +.journey-page { color: var(--secondary); margin-left: auto; } .journey-metro[data-calm=true] .journey-station, .journey-metro[data-calm=true] .journey-continuation, .journey-metro[data-calm=true] .journey-line { transition: transform 300ms ease-out; } .journey-metro[data-calm=true] .journey-name-layer { transition: opacity 180ms ease-out, transform 300ms ease-out; } .journey-metro[data-loop=true] .journey-rail-window { animation: journey-overview-reset 600ms ease-out both; } @@ -189,14 +192,14 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .weather-info { container-name: weather-card; } .weather-body { flex: 1 1 0; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr); align-content: center; align-items: center; justify-items: center; gap: 24px; } .weather-now { min-width: 0; display: flex; flex-direction: column; align-items: center; } -.weather-current { display: flex; align-items: center; gap: .18em; color: var(--accent); font-size: clamp(64px, min(22cqw, 32cqh), 144px); } +.weather-current { display: flex; align-items: center; gap: .18em; color: var(--foreground); font-size: clamp(64px, min(22cqw, 32cqh), 144px); } .weather-current strong { font-size: 1em; font-weight: 550; line-height: 1; } .weather-current svg { width: .8em !important; height: .8em !important; } .weather-info .weather-description { margin: 12px 0 0; font-size: clamp(22px, 3cqw, 34px); line-height: 1.2; text-align: center; } .weather-hours { width: 100%; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; border-top: 1px solid var(--line); padding-top: 20px; } .weather-hours > div { min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 10px; } .weather-hours time { color: var(--secondary); font-size: clamp(16px, 3cqw, 22px); } -.weather-hours svg { color: var(--accent); width: clamp(40px, min(12cqw, 16cqh), 64px) !important; height: clamp(40px, min(12cqw, 16cqh), 64px) !important; } +.weather-hours svg { color: var(--secondary); width: clamp(40px, min(12cqw, 16cqh), 64px) !important; height: clamp(40px, min(12cqw, 16cqh), 64px) !important; } .weather-hours strong { font-size: clamp(24px, 6cqw, 40px); font-weight: 550; } .weather-attribution { flex-shrink: 0; padding-top: 16px; display: flex; flex-wrap: wrap; justify-content: center; gap: 5px 12px; font-size: 12px; color: var(--muted); } .weather-attribution a { color: inherit; text-underline-offset: 3px; } @@ -231,7 +234,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .passenger-qr { width: 100%; height: 100%; aspect-ratio: 1; } .qr-description { min-width: 0; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 12px; text-align: center; } .qr-info .qr-description p { font-size: clamp(18px, 3cqw, 40px); line-height: 1.25; margin: 0; } -.passenger-link { display: inline-flex; align-items: center; gap: 8px; color: var(--accent); text-decoration: none; font-size: clamp(18px, 2.5cqw, 30px); font-weight: 550; } +.passenger-link { display: inline-flex; align-items: center; gap: 8px; color: var(--foreground); text-decoration: none; font-size: clamp(18px, 2.5cqw, 30px); font-weight: 550; } .passenger-link span { min-width: 0; overflow-wrap: anywhere; } @container qr-card (min-width: 601px) { .qr-content { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); gap: 32px; } @@ -259,7 +262,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .places-info .place-copy .destination-heading { font-size: calc(clamp(24px, 5cqw, 52px) * var(--reading-scale, 1)); font-weight: 600; line-height: 1.13; letter-spacing: -.04em; margin: 0; overflow-wrap: anywhere; } .place-details { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 10px; color: var(--secondary); font-size: clamp(18px, 3.2cqw, 30px); } .place-details .route-badge { width: auto; min-width: 48px; height: 36px; border-radius: 10px; padding: 0 9px; font-size: 24px; } -.place-details time { display: inline-flex; align-items: baseline; gap: 8px; color: var(--accent); font-weight: 600; } +.place-details time { display: inline-flex; align-items: baseline; gap: 8px; color: var(--foreground); font-weight: 600; } .place-details small, .place-arrival small { font-size: .6em; color: var(--secondary); font-weight: 400; } .place-duration { display: flex; align-items: center; gap: 6px; font-size: .8em; } .place-arrival { display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 8px; color: var(--secondary); font-size: clamp(14px, 2cqw, 20px); } @@ -270,15 +273,15 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .fare-body { flex: 1 1 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1.8fr) minmax(0, .8fr); align-items: stretch; gap: 8px; text-align: center; padding: 6px 0; } .fare-label { font-size: clamp(16px, 2cqw, 24px); color: var(--secondary); } .fare-price, .fare-payment { min-height: 0; display: flex; align-items: center; justify-content: center; } -.fare-price .destination-heading { color: var(--accent); margin: 0; font-size: clamp(40px, 15cqw, 130px); line-height: 1.08; font-weight: 650; letter-spacing: -.06em; overflow-wrap: anywhere; } +.fare-price .destination-heading { color: var(--foreground); margin: 0; font-size: clamp(40px, 15cqw, 130px); line-height: 1.08; font-weight: 650; letter-spacing: -.06em; overflow-wrap: anywhere; } .fare-info .fare-payment .destination-heading { color: var(--secondary); margin: 0; font-size: clamp(22px, 4cqw, 40px); font-weight: 500; line-height: 1.3; overflow-wrap: anywhere; } @container fare-card (max-height: 300px) { .fare-label { display: none; } .fare-body { grid-template-rows: minmax(0, 1.8fr) minmax(0, .8fr); padding: 0; } } .weather-hint { display: flex; align-items: center; justify-content: center; gap: 10px; padding-top: 10px; color: var(--secondary); font-size: clamp(15px, 2.6cqw, 24px); line-height: 1.3; } -.weather-hint svg { flex-shrink: 0; color: var(--accent); } -.weather-hint strong { color: var(--accent); white-space: nowrap; font-size: 1.2em; font-weight: 600; } +.weather-hint svg { flex-shrink: 0; color: var(--secondary); } +.weather-hint strong { color: var(--foreground); white-space: nowrap; font-size: 1.2em; font-weight: 600; } .passenger-editor { min-width: 0; border: 1px solid var(--outline); border-radius: 14px; padding: 16px; margin: 12px 0; } .passenger-editor legend { color: var(--secondary); padding: 0 6px; font-size: 14px; } .passenger-editor .text-button { font-size: 14px; padding: 8px 0; } @@ -291,7 +294,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .place-arrival time { font-size: 30px; } } .info-status { display: flex; align-items: center; gap: 10px; flex-shrink: 0; color: var(--secondary); font-size: clamp(14px, 1.2vw, 22px); padding-top: 14px; line-height: 1.3; } -.info-status svg { color: var(--accent); } +.info-status svg { color: var(--muted); } .info-notices { display: grid; gap: 18px; } .notice { --notice-color: var(--accent); display: grid; grid-template-columns: 48px minmax(0, 1fr); gap: 20px; padding: clamp(20px, 2vw, 32px); border: 1px solid var(--line); border-radius: 24px; background: var(--background); overflow: hidden; } .notice[data-kind='cancellation'], .notice[data-kind='delay'] { --notice-color: var(--warning); } @@ -448,13 +451,14 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .clock { font-size: clamp(32px, 6.2vw, 68px); } .today { font-size: clamp(12px, 2vw, 20px); } .brand-word { display: none; } + .persistent-next > .eyebrow { display: none; } +} +@media (max-width: 700px) { + /* A vertical stop display lays out the info grid itself; phones show one card. */ .info-intro { flex-direction: column; align-items: flex-start; gap: 10px; } .info-grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr); } .info-card:not([data-active='true']) { display: none; } .information-tabs { display: flex; } - .persistent-next > .eyebrow { display: none; } -} -@media (max-width: 700px) { .board { --control-size: 32px; --control-gap: 3px; --hero-height: clamp(100px, 16vh, 150px); --hero-gap: 18px; padding: 12px 16px 0; } .board[data-row-count='5'], .board[data-row-count='6'] { --hero-height: 95px; } .board-header { gap: 12px; padding-bottom: 12px; } @@ -569,7 +573,7 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 .departure-content[data-next='true']:not([data-long-wait='true']) { --hero-height: clamp(134px, calc(18vh * var(--reading-scale) + 24px), 260px); } .board[data-row-count='5'] .departure-content[data-next='true']:not([data-long-wait='true']), .board[data-row-count='6'] .departure-content[data-next='true']:not([data-long-wait='true']) { --hero-height: clamp(118px, calc(15vh * var(--reading-scale) + 24px), 224px); } .hero-next { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-top: 8px; color: var(--secondary); font-size: min(calc(22px * var(--reading-scale)), max(15px, calc(var(--hero-height) * .13))); line-height: 1.2; } -.hero-next time { color: var(--accent); display: inline-flex; align-items: baseline; gap: 6px; font-weight: 650; } +.hero-next time { color: var(--foreground); display: inline-flex; align-items: baseline; gap: 6px; font-weight: 650; } .departure-status { min-width: 0; display: flex; flex-direction: column; align-items: flex-end; justify-content: center; gap: 6px; } .departure-change { color: var(--warning); font-size: clamp(12px, calc(1.1vw * var(--reading-scale)), 20px); line-height: 1.2; } .countdown-meta { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: baseline; gap: 2px .7em; text-align: right; font-size: clamp(14px, calc(var(--hero-height) * .12), calc(24px * var(--reading-scale))); line-height: 1.2; } @@ -640,3 +644,21 @@ h1 { font-size: clamp(30px, 3.8vw, 76px); letter-spacing: -.05em; line-height: 1 /* Quiet schedule-based night screen. The saved daytime theme remains intact. */ .board[data-night=true] .cycle-progress{opacity:0}.night-screen{height:100%;min-height:0;display:flex;flex-direction:column;gap:clamp(14px,2.4vh,30px);padding:clamp(14px,2.2vh,28px) 0;container-type:size}.night-intro{flex-shrink:0}.night-intro .eyebrow{display:flex;align-items:center;gap:10px;color:var(--accent)}.night-intro h2{font-size:clamp(26px,3.3vw,54px);letter-spacing:-.05em;font-weight:650;margin:12px 0 0;line-height:1.15;max-width:1000px}.night-content{flex:1;min-height:0;display:grid;grid-template-columns:minmax(0,1fr);align-items:center}.night-service{min-width:0;display:flex;flex-direction:column;gap:clamp(10px,2cqh,20px)}.night-service-label{font-size:clamp(20px,2vw,32px);color:var(--muted)}.night-service-time{font-size:clamp(64px,min(13vw,28cqh),180px);font-weight:650;line-height:1;color:var(--accent);letter-spacing:-.055em;display:block}.night-route{display:flex;align-items:center;gap:clamp(14px,2vw,26px);min-width:0}.night-route .route-badge{width:clamp(72px,8vw,130px);height:clamp(58px,10cqh,100px);border-radius:22px;flex-shrink:0;font-size:clamp(28px,4vw,60px)}.night-route h3{font-size:clamp(24px,3vw,46px);font-weight:600;line-height:1.2;letter-spacing:-.045em;overflow-wrap:anywhere;margin:0}.night-alerts{flex:0 1 30%;min-height:100px;max-height:200px;overflow:auto;position:relative;scrollbar-width:none}.night-alerts .notice{grid-template-columns:minmax(0,1fr);margin:0;padding:16px 20px;border-radius:18px}.night-alerts .notice h3{font-size:clamp(20px,1.8vw,28px)}.night-alerts .notice p{font-size:clamp(18px,1.5vw,24px);line-height:1.35}.night-alerts .notice-routes{display:flex;gap:8px;margin:8px 0}.night-alerts .notice-routes .route-badge{width:62px;height:38px;border-radius:10px;font-size:22px}.night-alerts .notice-graphic{display:none}.night-alert-page{display:block;text-align:right;color:var(--muted);font-size:14px;padding:6px}.board[data-night=true] .numeric-old,.board[data-night=true] .numeric-next,.board[data-night=true] .numeric-previous{animation:none!important}.board[data-night=true] .numeric-current{animation:none!important}@media(max-width:700px){.night-screen{gap:16px;padding:18px 0}.night-intro h2{font-size:clamp(26px,6.7vw,36px)}.night-content{grid-template-columns:1fr;gap:18px;align-content:center}.night-service{gap:12px}.night-service-time{font-size:clamp(68px,min(22vw,24cqh),115px)}.night-route h3{font-size:clamp(23px,5.7vw,32px)}.night-alerts{max-height:150px;flex-basis:28%}.night-alerts .notice{padding:14px}.night-alerts .notice-meta{flex-wrap:wrap}.night-alerts .notice-period{font-size:13px}.night-alerts .notice p{font-size:18px}}@media(max-height:620px) and (min-width:701px){.night-screen{gap:12px;padding:12px 0}.night-intro h2{font-size:clamp(25px,3vw,36px);margin-top:8px}.night-service-time{font-size:clamp(64px,28cqh,120px)}.night-route h3{font-size:clamp(23px,3vw,36px)}.night-route .route-badge{height:56px;width:80px}.night-alerts{min-height:90px;max-height:110px;flex-basis:28%}.night-service-label{font-size:22px}} + +/* Vertical stop displays (1080×1920 and similar). Width-based sizes made names + small while rows stretched; size type from the long side and the row instead. */ +@media (orientation: portrait) and (min-width: 701px) { + .board h1 { font-size: clamp(30px, calc(3.4vh * var(--reading-scale)), 96px); } + .board .clock { font-size: clamp(38px, calc(3.6vh * var(--reading-scale)), 104px); } + .board .today { font-size: clamp(13px, 1.1vh, 24px); } + .board .hero-destination h2 { font-size: min(calc(6.5vw * var(--reading-scale)), calc(var(--hero-height) * .34)); } + .board .row-destination h3 { font-size: min(calc(64px * var(--reading-scale)), calc(6vw * var(--reading-scale)), calc(var(--row-height) * .4)); } + .board .row-destination p { font-size: min(calc(26px * var(--reading-scale)), calc(var(--row-height) * .17)); } + .board .row-time { font-size: min(calc(48px * var(--reading-scale)), calc(var(--row-height) * .32)); } + .departure-row .countdown-value { font-size: min(calc(76px * var(--reading-scale)), calc(var(--row-height) * .5)); } + .departure-row .route-badge { width: clamp(80px, 11vw, 150px); height: min(96px, calc(var(--row-height) * .62)); font-size: min(56px, calc(var(--row-height) * .36)); } + .board .info-card h3 { font-size: clamp(23px, calc(1.6vh * var(--reading-scale)), 44px); } + .board .weather-summary { font-size: clamp(16px, calc(1.2vh * var(--reading-scale)), 30px); } + .board .eyebrow { font-size: clamp(12px, calc(.95vh * var(--reading-scale)), 24px); } + .board .source-label { font-size: clamp(13px, calc(1vh * var(--reading-scale)), 26px); } +}