feat: add media query hooks and enhance responsive layout for portrait displays
This commit is contained in:
23
src/App.tsx
23
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<Scene, string> = { 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<HTMLDivElement>(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);
|
||||
|
||||
@@ -41,7 +41,7 @@ export function PlacesCard({ trips, now, paused, active, settings, onPages }: {
|
||||
return <><h3>До важных мест</h3><div className="places-body" ref={body} style={{ '--place-rows': visible.length } as CSSProperties}>
|
||||
{visible.map((trip, index) => <article className="place-trip" key={`place-${index}`}>
|
||||
<div className="place-copy"><FittedDestination text={trip.place.name} />
|
||||
{trip.departure ? <div className="place-details"><RouteBadge number={trip.departure.routeNumber} filled /><time dateTime={trip.departure.scheduledAt}><NumericText value={trip.departure.scheduledTime} /><TripDay at={trip.departure.scheduledAt} now={now} /></time>
|
||||
{trip.departure ? <div className="place-details"><RouteBadge number={trip.departure.routeNumber} /><time dateTime={trip.departure.scheduledAt}><NumericText value={trip.departure.scheduledTime} /><TripDay at={trip.departure.scheduledAt} now={now} /></time>
|
||||
{trip.minutes !== null && <span className="place-duration" aria-label={`В пути ${trip.minutes} мин по расписанию`}><Clock size={18} />{trip.minutes === 0 ? '<1 мин' : durationParts(trip.minutes).map(part => <span key={part.unit}><NumericText value={part.value} /> {part.unit}</span>)}</span>}</div>
|
||||
: <p className="place-unavailable">Ближайший прямой рейс не найден</p>}
|
||||
</div>
|
||||
|
||||
10
src/hooks.ts
10
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);
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user