/* === Reset & Base === */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } :root { --safe-bottom: env(safe-area-inset-bottom, 0px); --tab-height: 56px; /* Light mode (default). Surface model (iOS grouped-list style): --color-page app/page background (light gray) --color-bg elevated floating surfaces: sheets, popups, map panels --color-bg-secondary card surface sitting on the page (white) --color-inset nested fills inside a card: inputs, pressed states */ --color-text: #000; --color-text-2: #3c3c43; --color-page: #fafafa; --color-bg: #fff; --color-bg-secondary: #ffffff; --color-inset: #f2f2f7; --color-bg-elevated: #ffffff; /* 4.6:1 on white — the old #8e8e93 was a 3.3:1 WCAG AA failure for the app's pervasive muted text (hints, sublines, section headers). */ --color-hint: #6d6d72; --color-separator: rgba(60,60,67,0.12); --color-border: var(--color-separator); --color-accent: #007aff; --color-accent-text: #fff; --color-favorite: #ffb800; --color-good: #34c759; --color-okay: #ff9500; --color-bad: #ff3b30; /* Spacing scale — every padding/margin/gap should snap to this. */ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --sp-7: 32px; --sp-8: 40px; /* Border-radius scale. r-md is the workhorse for cards/inputs; r-pill is for chips and the round status pills. */ --r-xs: 6px; --r-sm: 8px; --r-md: 12px; --r-lg: 16px; --r-xl: 20px; --r-pill: 999px; /* Elevation — light mode keeps shadows soft so the secondary surface stays the primary visual anchor on cards. */ --shadow-1: 0 1px 2px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.05); --shadow-2: 0 2px 8px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.04); --shadow-3: 0 6px 18px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.06); --shadow-4: 0 16px 48px rgba(0,0,0,0.16), 0 4px 12px rgba(0,0,0,0.08); /* === Motion system === Every transition and animation in the app draws its curve and duration from here. One personality, applied consistently, is what separates a premium-feeling UI from one that merely has animations: nothing should move on a curve that isn't in this list. Curves: --ease-out the workhorse. Fast start, long soft landing — the "expo out" feel iOS uses for almost everything. Everything ENTERING the screen uses this. --ease-in the mirror: slow start, accelerating exit. Everything LEAVING the screen uses this, so departures gather speed instead of drifting. --ease-out-fast a shorter tail, for sub-200 ms press/hover feedback where the long landing would read as sluggish. --ease-soft gentler decel for opacity/colour-only fades. --ease-in-out symmetric, for things that move and come back, and for sweeps that should start and finish gently. --ease-spring ~4 % overshoot. Toggles, thumbs, small pops. --ease-spring-lg ~10 % overshoot. Celebratory pops only. --ease-emphasis iOS sheet curve — near-instant start, glassy glide. Reserved for large surfaces (sheets, drawers). Durations scale with how far a thing travels: a 2 px press is --dur-1, a full-screen sheet is --dur-6. Anything longer than --dur-6 stops feeling responsive. */ --ease-out: cubic-bezier(0.22, 1, 0.36, 1); --ease-in: cubic-bezier(0.64, 0, 0.78, 0); --ease-out-fast: cubic-bezier(0.2, 0, 0, 1); --ease-soft: cubic-bezier(0.33, 1, 0.68, 1); --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); --ease-spring: cubic-bezier(0.34, 1.4, 0.64, 1); --ease-spring-lg: cubic-bezier(0.34, 1.56, 0.64, 1); --ease-emphasis: cubic-bezier(0.32, 0.72, 0, 1); --dur-0: 110ms; --dur-1: 160ms; --dur-2: 220ms; --dur-3: 280ms; --dur-4: 360ms; --dur-5: 460ms; --dur-6: 580ms; /* Uniform press feedback. Rows use the subtle value (they're wide, so a hard scale reads as a glitch), compact controls use the standard one. */ --press-scale: 0.96; --press-scale-subtle: 0.985; /* Stagger timing for dynamically rendered lists lives in app.js (staggerIn) — it has to scale with item count, which CSS can't do. */ color-scheme: light dark; } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --color-text: #ffffff; --color-text-2: #ebebf5; --color-page: #000000; --color-bg: #000000; --color-bg-secondary: #1c1c1e; --color-inset: #2c2c2e; --color-bg-elevated: #2c2c2e; --color-hint: #98989e; --color-separator: rgba(255,255,255,0.1); --color-border: var(--color-separator); --color-accent: #0a84ff; --color-accent-text: #fff; --color-favorite: #ffb800; --shadow-1: 0 1px 2px rgba(0,0,0,0.4); --shadow-2: 0 2px 8px rgba(0,0,0,0.35); --shadow-3: 0 6px 18px rgba(0,0,0,0.45); --shadow-4: 0 16px 48px rgba(0,0,0,0.55); } } html[data-theme="dark"] { --color-text: #ffffff; --color-text-2: #ebebf5; --color-page: #000000; --color-bg: #000000; --color-bg-secondary: #1c1c1e; --color-inset: #2c2c2e; --color-bg-elevated: #2c2c2e; --color-hint: #98989e; --color-separator: rgba(255,255,255,0.1); --color-border: var(--color-separator); --color-accent: #0a84ff; --color-accent-text: #fff; --shadow-1: 0 1px 2px rgba(0,0,0,0.4); --shadow-2: 0 2px 8px rgba(0,0,0,0.35); --shadow-3: 0 6px 18px rgba(0,0,0,0.45); --shadow-4: 0 16px 48px rgba(0,0,0,0.55); color-scheme: dark; } html[data-theme="light"] { color-scheme: light; } html, body { height: 100%; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 15px; line-height: 1.4; color: var(--color-text); background: var(--color-page); -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent; -webkit-overflow-scrolling: touch; } /* === Accessibility: Focus-visible === */ :where(button, a, [role="button"], input, select, textarea):focus { outline: none; } :where(button, a, [role="button"]):focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 6px; } :where(input, select, textarea):focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; } /* === Accessibility: Tap targets === */ /* Extend sub-44px controls' hit area to ~44px without changing their visual size (iOS HIG / WCAG 2.5.5). The pseudo-element participates in hit testing but paints nothing. */ .hour-chart-close, #map-location-banner-close, .map-search-clear, .alert-step-btn, .hl-pin-btn { position: relative; } .hour-chart-close::after, #map-location-banner-close::after, .map-search-clear::after, .alert-step-btn::after, .hl-pin-btn::after, .detail-modal-close::after, .sheet-expand-btn::after { content: ''; position: absolute; inset: calc((100% - 44px) / 2); } /* === Accessibility: Reduced motion === */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } /* === App Container === */ #app { position: fixed; top: 0; left: 0; right: 0; bottom: calc(var(--tab-height) + var(--safe-bottom)); overflow: hidden; } /* First paint eases in instead of snapping on. Opacity only — a transform here would turn #app into the containing block for its position:fixed descendants (the sheet, the toast) and displace them mid-animation. */ @keyframes app-boot { from { opacity: 0; } to { opacity: 1; } } #app, .tab-bar { animation: app-boot var(--dur-4) var(--ease-soft) both; } /* === Tab Views === The container itself no longer moves — it only manages opacity and visibility. All the motion lives on the cards inside (see the cascade below), which is what makes a tab switch read as content arriving and leaving rather than a whole page being swapped. Dropping the container transform also removes a side effect: a non-`none` transform makes the element the containing block for any position:fixed descendant, so the old `translateY(0) scale(1)` on the active view was silently redefining "fixed" for everything inside it. */ .tab-view { position: absolute; top: 0; left: 0; right: 0; bottom: 0; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; opacity: 0; visibility: hidden; pointer-events: none; z-index: 0; /* Held visible long enough for the cards to finish leaving (the slowest exits at ~290 ms), so nothing gets cut off mid-flight. */ transition: opacity var(--dur-3) var(--ease-in), visibility 0s linear var(--dur-4); } .tab-view.active { opacity: 1; visibility: visible; pointer-events: auto; /* Above the outgoing view, so cards arriving pass in front of cards leaving instead of behind them. */ z-index: 1; transition: opacity var(--dur-2) var(--ease-soft), visibility 0s linear 0s; } /* --- Tab card choreography ---------------------------------------------- Entering a tab settles its cards in one after another; leaving sends them back down. Enter decelerates (--ease-out), exit accelerates (--ease-in) — the pairing is what makes the movement feel physical rather than like two unrelated fades. Enter is deliberately the longer, gentler half: arrivals are what the user is waiting for, departures should get out of the way. Containers whose contents are animated by JS on render (the stats body, the history hero/summary cards) are excluded so nothing animates twice. */ @keyframes tab-card-enter { from { opacity: 0; transform: translateY(14px) scale(0.985); } to { opacity: 1; transform: none; } } @keyframes tab-card-exit { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-10px) scale(0.985); } } /* Opacity-only entrance for blocks whose *contents* are staggered by JS (the stats body, the history hero/summary). Moving the container as well would double the travel of every card inside it. */ @keyframes tab-block-enter { from { opacity: 0; } to { opacity: 1; } } /* Per-card stagger index. These rules only set a custom property, so they can never collide with the animation rules that consume it — which is what broke an earlier version of this cascade, where an `animation` shorthand on a higher-specificity selector silently reset every delay to zero. */ .tab-view > *:nth-child(1) { --i: 0; } .tab-view > *:nth-child(2) { --i: 1; } .tab-view > *:nth-child(3) { --i: 2; } .tab-view > *:nth-child(4) { --i: 3; } .tab-view > *:nth-child(5) { --i: 4; } .tab-view > *:nth-child(6) { --i: 5; } .tab-view > *:nth-child(n+7) { --i: 6; } /* EXIT — matches whenever the view is *not* active. Flipping animation-name is what drives both directions: removing .active drops the enter rule, the name reverts to tab-card-exit, and a new animation starts. */ .tab-view > .page-header, #view-settings > .section, #view-settings > .about-footer, #view-stats > .section, #view-stats > #stats-content, #view-history > .section { animation-duration: var(--dur-2); animation-timing-function: var(--ease-in); animation-fill-mode: both; animation-name: tab-card-exit; /* Leaving reverses the order — the block furthest down goes first, so the page empties from the bottom up. */ animation-delay: calc((6 - var(--i, 0)) * 12ms); } /* ENTER — every selector here is one class more specific than its exit counterpart above, so .active always wins. */ .tab-view.active > .page-header, #view-settings.active > .section, #view-settings.active > .about-footer, #view-stats.active > .section, #view-history.active > .section:not(#history-stats):not(#history-summary) { animation-duration: var(--dur-5); animation-timing-function: var(--ease-out); animation-fill-mode: both; animation-name: tab-card-enter; animation-delay: calc(var(--i, 0) * 42ms); } #view-stats.active > #stats-content, #view-history.active > #history-stats, #view-history.active > #history-summary { animation-duration: var(--dur-4); animation-timing-function: var(--ease-out); animation-fill-mode: both; animation-name: tab-block-enter; animation-delay: calc(var(--i, 0) * 42ms); } /* The map tab is a single full-bleed canvas — sliding it would fight the map's own pan/zoom, and it has no page-header to cascade. */ #view-map > * { animation: none; } /* === Sections (iOS Settings Style) === */ .section { padding: 0 var(--sp-4); margin-bottom: var(--sp-3); } .section-header { font-size: 13px; font-weight: 600; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.6px; padding: var(--sp-5) var(--sp-1) var(--sp-2); display: flex; align-items: center; gap: var(--sp-2); } .section-sub-header { font-size: 11px; font-weight: 600; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.6px; padding: var(--sp-5) var(--sp-1) var(--sp-2); } .section-hint { margin: 0 var(--sp-1) var(--sp-3); font-size: 12px; line-height: 1.5; color: var(--color-hint); } .sync-badge { display: none; align-items: center; justify-content: center; color: #fff; opacity: 0.45; cursor: help; width: 14px; height: 14px; position: relative; transition: opacity var(--dur-3) var(--ease-soft), color var(--dur-3) var(--ease-soft); } body.logged-in .sync-badge { display: inline-flex; } .sync-badge svg { position: absolute; left: 0; top: 0; transition: opacity var(--dur-3) var(--ease-soft); } .sync-badge .sync-icon-idle { opacity: 1; } .sync-badge .sync-icon-ok { opacity: 0; } /* The rotating arrow caused visible wobble because the icon is asymmetric; sync state is now signalled by an opacity pulse on the cloud itself, so the spin icon is permanently hidden (markup kept for back-compat). */ .sync-badge .sync-icon-spin { display: none; } /* Syncing: cloud stays put and pulses gently. No rotation, no swap. */ .sync-badge.syncing { opacity: 1; } .sync-badge.syncing .sync-icon-idle { animation: sync-pulse 0.7s ease-in-out infinite; } /* Synced: cloud crossfades into the cloud+check variant with a green tint, plus a tiny pop so success feels noticed. */ .sync-badge.synced .sync-icon-idle { opacity: 0; } .sync-badge.synced .sync-icon-ok { opacity: 1; animation: sync-check-pop 0.28s ease-out; } .sync-badge.synced { opacity: 0.95; color: #34c759; } @keyframes sync-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } } @keyframes sync-check-pop { 0% { transform: scale(0.7); } 60% { transform: scale(1.08); } 100% { transform: scale(1); } } /* Sits directly on the page (not inside a card), so it needs the card surface — the inset tone would vanish against the light-mode page. */ .cloud-sync-hint { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-3) var(--sp-4) var(--sp-1); padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm); background: var(--color-bg-secondary); border: 1px solid var(--color-separator); font-size: 12px; color: var(--color-hint); line-height: 1.4; } .cloud-sync-hint svg { flex-shrink: 0; opacity: 0.5; } body.logged-in .cloud-sync-hint { display: none; } /* === Cards (shadcn-style: 1 px border + subtle background) === */ .card { background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-2); } .card-list { background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); overflow: hidden; margin-bottom: var(--sp-2); } .card-list .card-row { padding: var(--sp-3) var(--sp-4); border-bottom: 0.5px solid var(--color-separator); } /* Inline card header (used inside a .card to act as a section title for the card's contents). Title is bigger and more emphatic than .section-header, the description sits muted right beneath. */ .card-head { display: flex; flex-direction: column; gap: var(--sp-1); padding-bottom: var(--sp-3); margin-bottom: var(--sp-3); border-bottom: 1px solid var(--color-separator); } .card-head-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); } .card-title-lg { font-size: 17px; font-weight: 600; color: var(--color-text); letter-spacing: -0.01em; line-height: 1.25; } .card-description { font-size: 13px; color: var(--color-hint); line-height: 1.45; } /* === Page header (sits at the top of each non-map tab) === */ .page-header { /* Top inset keeps the H1 out from under the notch/status bar on iOS (viewport-fit=cover pulls content behind it). */ padding: calc(env(safe-area-inset-top, 0px) + var(--sp-5)) var(--sp-4) var(--sp-3); margin-bottom: var(--sp-2); } .page-title { font-size: 28px; font-weight: 700; letter-spacing: -0.02em; color: var(--color-text); line-height: 1.15; } .page-description { font-size: 14px; color: var(--color-hint); margin-top: var(--sp-1); line-height: 1.45; } /* === Metric (KPI) card — used for the 3-up dashboard tiles in history and stats. Tighter than .card, more visual weight on the value. */ .metric-card { background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; justify-content: center; gap: var(--sp-1); min-height: 84px; min-width: 0; } .metric-label { font-size: 12px; color: var(--color-hint); font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; } .metric-value { font-size: 24px; font-weight: 700; color: var(--color-text); letter-spacing: -0.01em; font-variant-numeric: tabular-nums; line-height: 1.1; } .metric-trend { font-size: 12px; color: var(--color-hint); margin-top: var(--sp-1); display: inline-flex; align-items: center; gap: 4px; } .metric-trend.up { color: var(--color-bad); } .metric-trend.down { color: var(--color-good); } .metric-trend.flat { color: var(--color-hint); } .metric-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-3); margin-bottom: var(--sp-3); } .stats-overview-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } @media (max-width: 600px) { .metric-row { grid-template-columns: 1fr; } } /* === Extreme cards (lowest / highest price in history summary) === */ .extreme-card { background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); padding: var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; } .extreme-label { font-size: 12px; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; } .extreme-value { font-size: 28px; font-weight: 700; color: var(--color-text); letter-spacing: -0.01em; font-variant-numeric: tabular-nums; line-height: 1.1; } .extreme-value.good { color: var(--color-good); } .extreme-value.bad { color: var(--color-bad); } .extreme-card.is-empty .extreme-value { color: var(--color-hint); opacity: 0.6; } .extreme-station { font-size: 13px; color: var(--color-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .empty-state-inline { text-align: center; padding: var(--sp-5) 0 var(--sp-2); opacity: 0.5; font-size: 13px; color: var(--color-hint); } /* === Stats hero (overall avg headline) === */ .stats-hero { background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); padding: var(--sp-5) var(--sp-5); margin-bottom: var(--sp-3); display: flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: var(--sp-1); min-height: 120px; } .stats-hero-label { font-size: 12px; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; } .stats-hero-value { font-size: 40px; font-weight: 800; color: var(--color-text); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1.05; } /* === Best times (cheapest day / hour) === */ .best-time-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); } .best-time-card { background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4); display: flex; flex-direction: column; justify-content: center; gap: var(--sp-1); min-height: 96px; min-width: 0; } .best-time-label { font-size: 12px; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; } .best-time-value { font-size: 20px; font-weight: 700; color: var(--color-text); letter-spacing: -0.01em; } .best-time-price { font-size: 14px; font-weight: 600; margin-top: var(--sp-1); font-variant-numeric: tabular-nums; } .best-time-price.good { color: var(--color-good); } .best-time-price.bad { color: var(--color-bad); } @media (max-width: 360px) { .best-time-grid { grid-template-columns: 1fr; } } #stats-content { width: 100%; } #stats-content > .metric-row { margin-left: var(--sp-4); margin-right: var(--sp-4); } /* === Stats hero card — overall avg with price spread visualisation === */ .stats-hero-section { padding-bottom: 0; } .stats-hero-card { position: relative; background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-lg); padding: var(--sp-5) var(--sp-4) var(--sp-4); margin-bottom: var(--sp-2); overflow: hidden; } .stats-hero-top { position: relative; z-index: 1; display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); } .stats-hero-headline { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; } .stats-hero-label { font-size: 11px; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.7px; font-weight: 600; } .stats-hero-value { font-size: 36px; font-weight: 800; color: var(--color-text); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1.05; } .stats-spread { position: relative; z-index: 1; margin-top: var(--sp-4); } .stats-spread-track { position: relative; height: 5px; border-radius: 3px; background: var(--color-separator); overflow: visible; } .stats-spread-fill { position: absolute; inset: 0; border-radius: 3px; background: linear-gradient(90deg, var(--color-good) 0%, var(--color-okay) 55%, var(--color-bad) 100%); opacity: 0.32; } .stats-spread-marker { position: absolute; top: 50%; width: 13px; height: 13px; margin-left: -6.5px; border-radius: 50%; background: var(--color-accent); border: 2.5px solid var(--color-bg-secondary); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25); transform: translateY(-50%); transition: left 0.7s var(--ease-out); } .stats-spread-labels { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); margin-top: 8px; font-size: 11px; color: var(--color-hint); font-variant-numeric: tabular-nums; } .stats-spread-low { color: var(--color-good); font-weight: 600; } .stats-spread-high { color: var(--color-bad); font-weight: 600; } .stats-spread-caption { text-transform: uppercase; letter-spacing: 0.6px; font-weight: 600; font-size: 10px; color: var(--color-hint); } /* === Stats facts row — compact 3-up below the hero === */ .stats-facts-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } .stats-fact-card { background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); padding: 10px; display: flex; align-items: center; gap: 8px; min-width: 0; } .stats-fact-icon { flex-shrink: 0; width: 28px; height: 28px; border-radius: 9px; display: grid; place-items: center; } .stats-fact-icon-good { background: color-mix(in srgb, var(--color-good) 16%, transparent); color: var(--color-good); } .stats-fact-icon-bad { background: color-mix(in srgb, var(--color-bad) 16%, transparent); color: var(--color-bad); } .stats-fact-icon-accent { background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent); } .stats-fact-body { display: flex; flex-direction: column; gap: 0; min-width: 0; } .stats-fact-label { font-size: 10px; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; line-height: 1.25; } .stats-fact-value { font-size: 16px; font-weight: 700; letter-spacing: -0.01em; font-variant-numeric: tabular-nums; line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Best-time header + savings badge */ .best-time-head { display: flex; align-items: center; gap: 8px; margin-bottom: 2px; } .best-time-icon-wrap { flex-shrink: 0; width: 26px; height: 26px; border-radius: 8px; background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-accent); display: grid; place-items: center; } .best-time-savings { display: inline-flex; align-items: center; gap: 4px; margin-top: 6px; padding: 3px 8px; border-radius: 999px; background: color-mix(in srgb, var(--color-good) 16%, transparent); color: var(--color-good); font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums; align-self: flex-start; width: fit-content; } /* === Weekday tile row — 7 chronological cards with a coloured cheapness bar at the bottom. Background stays neutral so the price reads cleanly; the bar carries the rank signal. The cheapest day gets a subtle tinted surface + coloured border. === */ .stats-tile-grid { display: grid; gap: 6px; } .stats-tile-grid-7 { grid-template-columns: repeat(7, minmax(0, 1fr)); } .stats-tile { position: relative; border-radius: 12px; padding: 11px 4px 9px; background: var(--color-bg-secondary); border: 1px solid var(--color-separator); display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 2px; min-height: 64px; min-width: 0; transition: transform var(--dur-1) var(--ease-out-fast), border-color var(--dur-2) var(--ease-soft), background var(--dur-2) var(--ease-soft); } .stats-tile.is-empty .stats-tile-name, .stats-tile.is-empty .stats-tile-value { opacity: 0.4; } .stats-tile.is-best { border-color: color-mix(in srgb, var(--tile-color) 65%, transparent); background: color-mix(in srgb, var(--tile-color) 9%, var(--color-bg-secondary)); } .stats-tile-name { font-size: 10px; font-weight: 700; color: var(--color-hint); letter-spacing: 0.5px; text-transform: uppercase; line-height: 1; } .stats-tile-value { font-size: 13px; font-weight: 700; color: var(--tile-color, var(--color-text)); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; line-height: 1.1; margin-top: 2px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .stats-tile.is-empty .stats-tile-value { color: var(--color-hint); } .stats-tile-bar { margin-top: auto; width: calc(100% - 12px); height: 3px; background: var(--color-separator); border-radius: 999px; overflow: hidden; align-self: center; } .stats-tile-bar-fill { height: 100%; width: 0; background: var(--tile-bar-color, var(--tile-color, color-mix(in srgb, var(--color-accent) 55%, transparent))); border-radius: 999px; transition: width 0.7s var(--ease-out); } .stats-tile.is-empty .stats-tile-bar-fill { background: var(--color-separator); } .stats-tile-crown { position: absolute; top: 4px; right: 5px; font-size: 10px; color: var(--tile-color); z-index: 2; line-height: 1; } /* === Settings: account hero card === */ .account-hero { position: relative; background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-lg); padding: var(--sp-4) var(--sp-4) var(--sp-4); overflow: hidden; } .account-hero-top { position: relative; z-index: 1; display: flex; align-items: center; gap: var(--sp-4); min-width: 0; } .account-hero-avatar { flex-shrink: 0; width: 52px; height: 52px; border-radius: 50%; background: color-mix(in srgb, var(--color-accent) 12%, var(--color-bg-secondary)); color: color-mix(in srgb, var(--color-accent) 80%, var(--color-text)); display: grid; place-items: center; overflow: hidden; border: 1px solid color-mix(in srgb, var(--color-accent) 22%, transparent); transition: border-color var(--dur-3) var(--ease-soft), background var(--dur-3) var(--ease-soft); } .account-hero.is-signed-in .account-hero-avatar { background: var(--color-bg); border-color: color-mix(in srgb, var(--color-accent) 55%, transparent); color: var(--color-accent); } .account-hero-avatar .account-avatar-img { width: 100%; height: 100%; object-fit: cover; } .account-hero-avatar .account-avatar-fallback { width: 100%; height: 100%; display: grid; place-items: center; font-size: 24px; font-weight: 700; color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 14%, transparent); } .account-hero-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .account-hero-label { font-size: 10px; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.7px; font-weight: 700; } .account-hero-name { font-size: 19px; font-weight: 700; color: var(--color-text); letter-spacing: -0.02em; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .account-hero-subline { font-size: 13px; color: var(--color-hint); line-height: 1.35; overflow: hidden; text-overflow: ellipsis; } .account-hero-actions { position: relative; z-index: 1; display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-4); flex-wrap: wrap; } .account-hero-sync-hint { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--color-hint); line-height: 1.35; } .account-hero-sync-hint svg { flex-shrink: 0; color: var(--color-accent); opacity: 0.85; } .account-hero-btn { flex-shrink: 0; padding: 9px 18px; border-radius: 999px; background: var(--color-accent); color: var(--color-accent-text); border: none; font-size: 14px; font-weight: 600; font-family: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: transform var(--dur-1) var(--ease-out), box-shadow var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-soft); } .account-hero-btn:hover { box-shadow: 0 4px 12px color-mix(in srgb, var(--color-accent) 35%, transparent); } .account-hero-btn:active { transform: scale(0.96); } .account-hero.is-signed-in .account-hero-btn { background: color-mix(in srgb, var(--color-text) 8%, transparent); color: var(--color-text); } .account-hero.is-signed-in .account-hero-btn:hover { background: color-mix(in srgb, var(--color-text) 14%, transparent); box-shadow: none; } /* === Settings rows: icon chip in front of the label === */ .settings-icon-chip { flex-shrink: 0; width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-accent); } .settings-icon-chip.settings-icon-good { background: color-mix(in srgb, var(--color-good) 14%, transparent); color: var(--color-good); } .settings-icon-chip.settings-icon-warn { background: color-mix(in srgb, var(--color-bad) 14%, transparent); color: var(--color-bad); } .settings-card .settings-row-left { gap: var(--sp-3); } .settings-card .settings-row-label { font-size: 15px; font-weight: 500; } /* === Segmented control (theme, history-default range) === */ .seg-control { position: relative; display: flex; gap: 2px; margin: 0 var(--sp-4) var(--sp-3); padding: 2px; border-radius: 9px; background: color-mix(in srgb, var(--color-text) 7%, transparent); } .seg-thumb { position: absolute; top: 2px; bottom: 2px; left: 0; width: 0; background: var(--color-bg); border-radius: 7px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.06); /* The thumb overshoots a hair and settles — the single most recognisable "this is a native control" cue in an iOS segmented picker. Width lags on the plain curve so the pill doesn't wobble as it resizes. */ transition: transform var(--dur-5) var(--ease-spring), width var(--dur-4) var(--ease-out); pointer-events: none; z-index: 1; } html[data-theme="dark"] .seg-thumb, html:not([data-theme]) .seg-thumb { /* dark via prefers-color-scheme */ background: var(--color-bg-secondary); } @media (prefers-color-scheme: dark) { html:not([data-theme="light"]) .seg-thumb { background: var(--color-bg-secondary); } } .seg-option { position: relative; z-index: 2; flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 7px 10px; border: none; background: none; color: var(--color-hint); font-size: 13px; font-weight: 600; font-family: inherit; cursor: pointer; border-radius: 7px; -webkit-tap-highlight-color: transparent; transition: color var(--dur-2) var(--ease-soft); white-space: nowrap; min-width: 0; } .seg-option svg { flex-shrink: 0; opacity: 0.65; transition: opacity var(--dur-2) var(--ease-soft), color var(--dur-2) var(--ease-soft); } .seg-option:hover { color: var(--color-text); } .seg-option:hover svg { opacity: 0.85; } .seg-option.active { color: var(--color-text); font-weight: 700; } .seg-option.active svg { color: var(--color-accent); opacity: 1; } @media (prefers-reduced-motion: reduce) { .seg-thumb { transition: none; } } /* === About hero — bigger app-icon centerpiece === */ .about-hero { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-5); background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); margin-bottom: var(--sp-3); } .about-hero .about-app-icon { flex-shrink: 0; width: 68px; height: 68px; border-radius: 16px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); cursor: pointer; } .about-hero-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; } .about-hero-info .about-app-name { font-size: 22px; font-weight: 800; color: var(--color-text); letter-spacing: -0.02em; } .about-hero-info .about-app-version { font-size: 12px; color: var(--color-hint); font-variant-numeric: tabular-nums; margin-bottom: 4px; } .about-hero-tag { font-size: 13px; color: var(--color-hint); line-height: 1.35; } /* === History tab — hero card with current avg + trend pills === */ .history-hero-card { position: relative; background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-lg); padding: var(--sp-5) var(--sp-4) var(--sp-4); overflow: hidden; } .history-hero-top { position: relative; z-index: 1; display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); } .history-hero-headline { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; } .history-hero-label { font-size: 11px; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.7px; font-weight: 600; } .history-hero-value { font-size: 36px; font-weight: 800; color: var(--color-text); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1.05; } .history-hero-pills { position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; } .history-hero-pill { display: inline-flex; align-items: center; gap: 5px; padding: 5px 11px; border-radius: 999px; background: color-mix(in srgb, var(--pill-color) 13%, transparent); color: var(--pill-color); font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.1px; } .history-hero-pill-arrow { font-weight: 700; } .history-hero-pill-label { color: color-mix(in srgb, var(--pill-color) 55%, var(--color-text)); opacity: 0.75; font-weight: 500; margin-left: 2px; letter-spacing: 0.2px; } /* === History extreme cards — clickable, opens centred station popup === */ .history-extreme-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .history-extreme-card { position: relative; background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: 14px; padding: 12px 14px; display: flex; align-items: center; gap: 12px; text-align: left; cursor: pointer; font-family: inherit; color: var(--color-text); overflow: hidden; -webkit-tap-highlight-color: transparent; transition: transform var(--dur-1) var(--ease-out-fast), box-shadow var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-soft); } .history-extreme-card:hover { border-color: color-mix(in srgb, var(--accent) 35%, var(--color-separator)); box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 12%, transparent); } .history-extreme-card:active { transform: scale(0.98); } .history-extreme-card.is-empty { cursor: default; opacity: 0.55; } .history-extreme-card.is-empty:hover { border-color: var(--color-separator); box-shadow: none; } .history-extreme-card.is-empty:active { transform: none; } .history-extreme-icon { flex-shrink: 0; width: 34px; height: 34px; border-radius: 10px; background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); display: grid; place-items: center; } .history-extreme-body { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 1px; overflow: hidden; } .history-extreme-label { font-size: 10px; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; } .history-extreme-value { font-size: 18px; font-weight: 800; color: var(--accent); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; line-height: 1.15; } .history-extreme-station { font-size: 12px; color: var(--color-hint); line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } /* === History chart custom tooltip === */ .history-tooltip { position: absolute; pointer-events: none; z-index: 99997; background: rgba(20, 20, 20, 0.94); border-radius: 12px; padding: 10px 14px; transform: translate(-50%, calc(-100% - 14px)) scale(0.94); transform-origin: bottom center; opacity: 0; transition: opacity var(--dur-1) var(--ease-soft), transform var(--dur-1) var(--ease-spring-lg); box-shadow: 0 8px 26px rgba(0, 0, 0, 0.28); white-space: nowrap; font-family: -apple-system, BlinkMacSystemFont, Roboto, sans-serif; text-align: center; min-width: 130px; } .history-tooltip.show { opacity: 1; transform: translate(-50%, calc(-100% - 14px)) scale(1); } .history-tooltip-time { font-size: 11px; font-weight: 600; color: rgba(255, 255, 255, 0.62); letter-spacing: 0.3px; margin-bottom: 2px; } .history-tooltip-price { font-size: 18px; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } .history-tooltip-meta { display: flex; justify-content: center; gap: 12px; margin-top: 5px; font-size: 11px; font-weight: 600; color: rgba(255, 255, 255, 0.78); font-variant-numeric: tabular-nums; letter-spacing: 0.1px; } .history-tooltip-hint { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255, 255, 255, 0.14); font-size: 10px; color: rgba(255, 255, 255, 0.55); letter-spacing: 0.2px; } /* Hour chart section header — drilled-down day + close button */ #hour-chart-label { display: flex !important; align-items: center; justify-content: space-between; gap: 8px; } .hour-chart-day { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .hour-chart-close { flex-shrink: 0; border: none; background: color-mix(in srgb, var(--color-text) 8%, transparent); color: var(--color-hint); width: 24px; height: 24px; border-radius: 50%; display: grid; place-items: center; cursor: pointer; padding: 0; -webkit-tap-highlight-color: transparent; transition: background var(--dur-1) var(--ease-soft), color var(--dur-1) var(--ease-soft); } .hour-chart-close:hover { background: color-mix(in srgb, var(--color-text) 14%, transparent); color: var(--color-text); } .hour-chart-close:active { transform: scale(0.92); } /* === Hour line chart — custom HTML tooltip so the price text can pick up the hovered hour's rank colour. === */ .stats-hour-tooltip { position: absolute; pointer-events: none; z-index: 99997; background: rgba(20, 20, 20, 0.94); border-radius: 12px; padding: 9px 14px; transform: translate(-50%, calc(-100% - 14px)) scale(0.94); transform-origin: bottom center; opacity: 0; transition: opacity var(--dur-1) var(--ease-soft), transform var(--dur-1) var(--ease-spring-lg); box-shadow: 0 8px 26px rgba(0, 0, 0, 0.28); white-space: nowrap; font-family: -apple-system, BlinkMacSystemFont, Roboto, sans-serif; text-align: center; } .stats-hour-tooltip.show { opacity: 1; transform: translate(-50%, calc(-100% - 14px)) scale(1); } .stats-hour-tooltip-time { font-size: 11px; font-weight: 600; color: rgba(255, 255, 255, 0.62); letter-spacing: 0.3px; margin-bottom: 2px; } .stats-hour-tooltip-price { font-size: 17px; font-weight: 800; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; } /* === Hour line chart — Chart.js canvas wrapper === */ .stats-hour-chart-wrap { position: relative; height: 170px; padding: var(--sp-3) var(--sp-2) var(--sp-2); background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-lg); animation: stats-chart-enter 0.55s var(--ease-out) both; } @keyframes stats-chart-enter { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { .stats-hour-chart-wrap { animation: none; } } /* === Station ranking — quiet numbered badges, green winner === */ .stats-station-list .station-ranking-item { position: relative; cursor: pointer; } .stats-station-list .ranking-pos { flex-shrink: 0; width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; background: var(--color-inset); color: var(--color-hint); font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; } .stats-station-list .station-ranking-item.is-best .ranking-pos { background: color-mix(in srgb, var(--color-good) 16%, transparent); color: var(--color-good); } .stats-station-list .ranking-price { font-weight: 600; font-variant-numeric: tabular-nums; } .stats-station-list .station-ranking-item.is-best .ranking-price { color: var(--color-good); font-weight: 700; } /* Small "Ø" mark in the ranking price so users understand the number is the 30-day average rather than the current price. Hover-title row carries min + sample count for the full picture. */ .ranking-price-avg-mark { font-size: 11px; font-weight: 600; margin-right: 4px; opacity: 0.65; letter-spacing: 0.5px; } @media (max-width: 360px) { .stats-fact-value { font-size: 14px; } .stats-fact-icon { width: 26px; height: 26px; border-radius: 8px; } .stats-hero-value { font-size: 32px; } } .card-list .card-row:last-child { border-bottom: none; } .card-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); position: relative; cursor: pointer; -webkit-user-select: none; user-select: none; } .card-row-left { display: flex; align-items: center; gap: var(--sp-3); flex: 1; min-width: 0; } .account-avatar-img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } .account-avatar-fallback { width: 40px; height: 40px; border-radius: 50%; background: var(--color-tint, #007aff); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 16px; } .card-icon { font-size: 20px; flex-shrink: 0; width: 28px; text-align: center; } .card-title { font-size: 15px; font-weight: 500; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-subtitle { font-size: 13px; color: var(--color-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .card-value { font-size: 16px; font-weight: 600; color: var(--color-text); flex-shrink: 0; } .card-value.good { color: var(--color-good); } .card-value.okay { color: var(--color-okay); } .card-value.bad { color: var(--color-bad); } .card-action { background: var(--color-accent); color: var(--color-accent-text); border: none; border-radius: var(--r-sm); padding: var(--sp-2) var(--sp-4); font-size: 14px; font-weight: 600; cursor: pointer; flex-shrink: 0; transition: transform var(--dur-1) var(--ease-out-fast), opacity var(--dur-1) var(--ease-soft); } .card-action:hover { opacity: 0.92; } .card-action:active { transform: scale(0.95); } /* Ripple effect */ .ripple { position: relative; overflow: hidden; } .ripple::after { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; background: var(--color-hint); transform: translate(-50%, -50%); opacity: 0; } .ripple:active::after { width: 200%; height: 200%; opacity: 0.15; transition: width var(--dur-3) var(--ease-out), height var(--dur-3) var(--ease-out), opacity var(--dur-3) var(--ease-out); } /* === Chips === */ .chip-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; padding-bottom: var(--sp-1); } .chip { background: var(--color-bg-secondary); color: var(--color-text); border: none; border-radius: var(--r-pill); padding: var(--sp-2) var(--sp-4); font-size: 14px; font-weight: 500; cursor: pointer; transition: background var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out), transform var(--dur-0) var(--ease-out), box-shadow var(--dur-1) var(--ease-out); -webkit-user-select: none; user-select: none; white-space: nowrap; } .chip:active { transform: scale(0.95); } .chip.active { background: var(--color-accent); color: var(--color-accent-text); box-shadow: var(--shadow-1); } .country-chip-row { margin-bottom: var(--sp-3); } .country-chip-row .chip { display: inline-flex; align-items: center; gap: var(--sp-2); } .country-flag { display: inline-block; flex-shrink: 0; border-radius: 1.5px; box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.15); vertical-align: middle; } /* === Map === */ #map-container { position: relative; height: 55vh; min-height: 250px; } #map-container::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: calc(env(safe-area-inset-top, 0px) + 56px); background: #000; z-index: 2; pointer-events: none; } #map { width: 100%; height: 100%; z-index: 1; -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; } .map-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: var(--color-bg); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; z-index: 10; color: var(--color-hint); font-size: 14px; transition: opacity var(--dur-3) var(--ease-out); } .map-overlay.hidden { opacity: 0; pointer-events: none; } /* Retry affordance shown in the overlay when station loading fails */ .map-retry-btn { padding: 8px 18px; border: none; border-radius: var(--r-pill); background: var(--color-accent); color: var(--color-accent-text); font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; } .map-retry-btn:active { transform: scale(0.96); } /* === Map Search Bar === */ #map-search-wrapper { position: absolute; top: calc(env(safe-area-inset-top, 0px) + 12px); left: 12px; right: 12px; z-index: 1000; display: flex; flex-direction: column; align-items: stretch; pointer-events: none; } #map-search-wrapper > * { pointer-events: auto; } /* Top row holds the search box (grows) and the "Hier suchen" pill (right, only visible after the map has moved). When the pill is hidden the box takes the full width on its own. */ #map-search-top-row { display: flex; align-items: center; gap: var(--sp-2); } /* Scan-picker mode hides the search box — keep the confirm/cancel pills centred in the row instead of stuck to the left. */ #map-search-top-row:has(#map-search-box.hidden) { justify-content: center; } #map-search-box { flex: 1; min-width: 0; display: flex; align-items: center; background: var(--color-bg); border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); padding: 0 12px; height: 44px; gap: 8px; } .map-search-icon { color: var(--color-hint); flex-shrink: 0; } #map-search-input { flex: 1; border: none; background: transparent; color: var(--color-text); font-size: 15px; outline: none; min-width: 0; } #map-search-input::placeholder { color: var(--color-hint); } .map-search-clear { border: none; background: none; color: var(--color-hint); font-size: 20px; cursor: pointer; line-height: 1; flex-shrink: 0; width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; transition: background var(--dur-1) var(--ease-out); } .map-search-clear:hover, .map-search-clear:focus-visible { background: var(--color-inset); } .map-search-clear.hidden { display: none; } #map-search-results { margin-top: 4px; background: var(--color-bg); border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); overflow: hidden; max-height: 240px; overflow-y: auto; } #map-search-results.hidden { display: none; } .map-search-result-item { padding: 12px 14px; font-size: 14px; color: var(--color-text); cursor: pointer; border-bottom: 1px solid var(--color-separator); display: flex; align-items: center; gap: 10px; } .map-search-result-item:last-child { border-bottom: none; } /* Keyboard highlight while arrowing through search results */ .map-search-result-item.kb-active { background: color-mix(in srgb, var(--color-accent) 12%, transparent); } .map-search-result-item:active { background: var(--color-inset); } .map-search-station-item { gap: 12px; } .map-search-station-rank { width: 26px; height: 26px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: #fff; flex-shrink: 0; } .map-search-station-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } .map-search-station-name { font-size: 14px; font-weight: 600; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .map-search-station-addr { font-size: 12px; color: var(--color-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .map-search-station-price { font-size: 16px; font-weight: 700; flex-shrink: 0; } /* ─── Route planner chip + panel ────────────────────────────── */ #map-route-chip-row { display: flex; justify-content: flex-start; margin-top: 6px; pointer-events: none; } .map-route-chip { pointer-events: auto; display: inline-flex; align-items: center; gap: 6px; background: var(--color-bg); border: none; border-radius: 999px; padding: 6px 12px; font-size: 13px; font-weight: 500; color: var(--color-text); cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.12); transition: transform var(--dur-0) var(--ease-out), background var(--dur-1) var(--ease-out); } .map-route-chip:hover { background: var(--color-inset); } .map-route-chip:active { transform: scale(0.96); } .map-route-chip[aria-expanded="true"] { background: var(--color-accent); color: #fff; } .map-route-chip[aria-expanded="true"] svg { color: #fff; } .map-route-panel { margin-top: 6px; background: var(--color-bg); border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); padding: 10px; display: flex; flex-direction: column; gap: 6px; } .map-route-panel.hidden { display: none; } .map-route-row { display: flex; align-items: center; gap: 10px; } .map-route-dot { flex-shrink: 0; width: 10px; height: 10px; border-radius: 50%; } .map-route-dot-start { background: var(--color-accent); } .map-route-dot-dest { background: transparent; border: 2px solid var(--color-text); box-sizing: border-box; } .map-route-input-wrapper { flex: 1; position: relative; min-width: 0; } .map-route-input { width: 100%; border: 1px solid var(--color-separator); background: var(--color-bg); color: var(--color-text); border-radius: 8px; padding: 8px 10px; font-size: 14px; outline: none; } .map-route-input:focus { border-color: var(--color-accent); } .map-route-suggest { position: absolute; top: calc(100% + 2px); left: 0; right: 0; z-index: 1001; background: var(--color-bg); border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); overflow: hidden; max-height: 200px; overflow-y: auto; } .map-route-suggest.hidden { display: none; } .map-route-suggest-item { padding: 10px 12px; font-size: 13px; color: var(--color-text); cursor: pointer; border-bottom: 1px solid var(--color-separator); display: flex; align-items: center; gap: 8px; } .map-route-suggest-item:last-child { border-bottom: none; } .map-route-suggest-item:active { background: var(--color-inset); } .map-route-suggest-item.is-current-location { color: var(--color-accent); font-weight: 500; } .map-route-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 2px; } .map-route-close { background: none; border: none; color: var(--color-hint); font-size: 13px; cursor: pointer; padding: 6px 10px; border-radius: 6px; } .map-route-close:hover { background: var(--color-inset); } .map-route-go { background: var(--color-accent); color: #fff; border: none; border-radius: 8px; padding: 7px 14px; font-size: 13px; font-weight: 600; cursor: pointer; } .map-route-go[disabled] { opacity: 0.4; cursor: not-allowed; } .map-route-summary { font-size: 12px; color: var(--color-hint); padding: 4px 2px 0; } .map-route-summary.hidden { display: none; } .route-exit-btn { position: absolute; top: calc(12px + 44px + 6px); right: 12px; z-index: 1001; background: var(--color-bg); border: none; border-radius: 999px; padding: 6px 12px; font-size: 13px; font-weight: 500; color: var(--color-text); cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.15); display: inline-flex; align-items: center; gap: 6px; } .route-exit-btn.hidden { display: none; } /* ── Route scan dots ─────────────────────────────────────────────── */ /* Yellow markers that appear along the route where live scans will run. */ .route-scan-dot { pointer-events: none; } .route-scan-dot-inner { width: 18px; height: 18px; border-radius: 50%; background: #f5c518; border: 2px solid #fff; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); transition: background var(--dur-3) var(--ease-soft), transform var(--dur-3) var(--ease-out), opacity var(--dur-3) var(--ease-soft); } .route-scan-dot.pending .route-scan-dot-inner { animation: route-scan-pulse 1.6s ease-in-out infinite; } .route-scan-dot.scanning .route-scan-dot-inner { animation: route-scan-pulse 0.7s ease-in-out infinite; background: #ffd84d; } .route-scan-dot.done .route-scan-dot-inner { background: #4caf50; animation: none; } .route-scan-dot.empty .route-scan-dot-inner { background: #9e9e9e; animation: none; opacity: 0.7; } .route-scan-dot.error .route-scan-dot-inner { background: #f44336; animation: none; } @keyframes route-scan-pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.35); opacity: 0.75; } } #map-search-box.hidden { display: none; } .map-search-no-results { padding: 14px; font-size: 13px; color: var(--color-hint); text-align: center; } /* === Map Location Banner === */ #map-location-banner { position: absolute; top: 64px; right: 12px; z-index: 1000; background: rgba(255, 204, 0, 0.75); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); color: #5a4500; border-radius: 10px; padding: 8px 12px; display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 500; box-shadow: 0 2px 8px rgba(0,0,0,0.1); max-width: 220px; } #map-location-banner.hidden { display: none; } #map-location-banner-close { border: none; background: none; color: #5a4500; font-size: 18px; line-height: 1; cursor: pointer; flex-shrink: 0; opacity: 0.7; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; transition: opacity var(--dur-1) var(--ease-out), background var(--dur-1) var(--ease-out); } #map-location-banner-close:hover, #map-location-banner-close:focus-visible { opacity: 1; background: rgba(0, 0, 0, 0.08); } #map-location-banner-close:active { opacity: 1; } .spinner { width: 28px; height: 28px; border: 3px solid var(--color-bg-secondary); border-top-color: var(--color-accent); border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* The shared entrance for anything that renders into a list or grid. The slight scale-up is what sells it: pure translation reads as a slide, translation plus scale reads as the element arriving from behind the surface — which is how the rest of the app's depth model behaves. */ @keyframes slide-in-up { from { opacity: 0; transform: translateY(14px) scale(0.97); } to { opacity: 1; transform: translateY(0) scale(1); } } .station-item.anim-in, .ranking-item.anim-in, .stat-card.anim-in, .metric-card.anim-in, .best-time-card.anim-in, .stats-hero-card.anim-in, .stats-fact-card.anim-in, .stats-tile.anim-in, .history-hero-card.anim-in, .history-extreme-card.anim-in { animation: slide-in-up var(--dur-5) var(--ease-out) both; } /* Hour bars grow via their inner fill (height transition driven by JS), no scaleY animation on the column container — that would squash the layout since the cell now anchors content to the bottom. */ /* === Confetti burst — decorative click effect === */ .confetti-burst { position: fixed; inset: 0; pointer-events: none; z-index: 99999; overflow: visible; } .confetti-piece { position: absolute; will-change: transform, opacity; pointer-events: none; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.18)); } .confetti-trigger { cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; } .confetti-trigger.stats-fact-icon, .confetti-trigger.best-time-icon-wrap, .confetti-trigger.about-heart, .confetti-trigger.about-app-icon { transition: transform var(--dur-1) var(--ease-spring); } .confetti-trigger.stats-fact-icon:hover, .confetti-trigger.best-time-icon-wrap:hover, .confetti-trigger.about-heart:hover, .confetti-trigger.about-app-icon:hover { transform: scale(1.08); } .about-heart { display: inline-block; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; } .about-app-icon { cursor: pointer; -webkit-tap-highlight-color: transparent; } /* === Floating value chip — fades in above tiny tap targets to surface their label (hour heatmap cells, weekday tiles, …) === */ .floating-value { position: fixed; pointer-events: none; z-index: 99998; font-size: 12px; font-weight: 700; padding: 5px 10px; border-radius: 999px; background: var(--color-bg-secondary); border: 1px solid currentColor; font-variant-numeric: tabular-nums; opacity: 0; transform: translate(-50%, calc(-100% - 14px)) translateY(8px) scale(0.9); transition: transform var(--dur-3) var(--ease-spring-lg), opacity var(--dur-2) var(--ease-soft); box-shadow: 0 4px 18px rgba(0, 0, 0, 0.18); white-space: nowrap; } .floating-value.show { opacity: 1; transform: translate(-50%, calc(-100% - 14px)) translateY(0) scale(1); } @media (prefers-reduced-motion: reduce) { .floating-value { transition: opacity var(--dur-1) linear; transform: translate(-50%, calc(-100% - 14px)); } .floating-value.show { transform: translate(-50%, calc(-100% - 14px)); } } @keyframes confetti-pop { 0% { transform: scale(1); } 45% { transform: scale(1.22); } 100% { transform: scale(1); } } .confetti-pop { animation: confetti-pop 0.42s var(--ease-spring-lg); } @media (prefers-reduced-motion: reduce) { .confetti-burst { display: none; } .confetti-pop { animation: none; } } /* === Skeleton Loading === */ /* The sweep pauses briefly at each end instead of ping-ponging continuously. A constant-speed shimmer reads as "busy"; a breathing one reads as "waiting", which is what a placeholder actually means. */ @keyframes shimmer { 0% { background-position: -150% 0; } 60% { background-position: 150% 0; } 100% { background-position: 150% 0; } } /* Placeholders fade up rather than appearing fully formed, so a fast response never flashes a hard-edged skeleton on screen. */ @keyframes skeleton-appear { from { opacity: 0; } to { opacity: 1; } } .skeleton-item { display: flex; align-items: center; padding: 14px 16px; gap: 12px; border-bottom: 0.5px solid var(--color-separator); animation: skeleton-appear var(--dur-4) var(--ease-soft) both; } .skeleton-item:nth-child(2) { animation-delay: 40ms; } .skeleton-item:nth-child(3) { animation-delay: 80ms; } .skeleton-item:nth-child(4) { animation-delay: 120ms; } .skeleton-item:nth-child(n+5) { animation-delay: 160ms; } .skeleton-item:last-child { border-bottom: none; } .skeleton-bone { background: linear-gradient(90deg, var(--color-inset) 25%, var(--color-separator) 50%, var(--color-inset) 75%); background-size: 200% 100%; animation: shimmer 1.9s var(--ease-in-out) infinite; border-radius: 6px; } .skeleton-rank { width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0; } .skeleton-text-long { height: 14px; width: 70%; margin-bottom: 6px; } .skeleton-text-short { height: 12px; width: 45%; } .skeleton-price { width: 52px; height: 20px; margin-left: auto; flex-shrink: 0; } /* Fills the history price-chart box while the first fetch is in flight. Sits over the (still-blank) canvas and is removed once the chart draws. */ .chart-skeleton { position: absolute; inset: var(--sp-4) var(--sp-2) var(--sp-2); border-radius: 10px; } /* === Station Sort === */ .station-sort-bar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 12px 16px 4px; gap: 8px; } .station-sort-actions { display: inline-flex; align-items: center; gap: 6px; justify-self: start; } .station-sort-label { font-size: 13px; font-weight: 600; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.3px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; justify-self: center; text-align: center; } .station-sort-label.is-route { display: flex; flex-direction: column; align-items: center; line-height: 1.15; white-space: normal; overflow: visible; text-align: center; } .station-sort-label.is-route .station-count-sub { font-size: 10px; font-weight: 500; letter-spacing: 0.2px; opacity: 0.85; margin-top: 2px; } .station-sort-btn { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border: none; border-radius: 8px; background: var(--color-bg-secondary); color: var(--color-text); font-size: 13px; font-weight: 600; cursor: pointer; -webkit-user-select: none; user-select: none; white-space: nowrap; transition: opacity var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out-fast); flex-shrink: 0; justify-self: end; } .station-sort-btn:active { transform: scale(0.95); } .station-sort-btn svg { flex-shrink: 0; color: var(--color-accent); } /* === Station List (below map) === */ .station-list { padding: var(--sp-2) 0; margin: 0 var(--sp-4); background: var(--color-bg-secondary); border-radius: var(--r-md); overflow: hidden; } .station-item { display: flex; align-items: center; padding: var(--sp-3) var(--sp-4); gap: var(--sp-3); -webkit-tap-highlight-color: transparent; transition: transform var(--dur-1) var(--ease-out-fast); position: relative; border-bottom: 0.5px solid var(--color-separator); } .station-item:last-child { border-bottom: none; } .station-item:active { transform: scale(0.97); } .station-rank { width: 28px; height: 28px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; color: #fff; flex-shrink: 0; } .station-info { flex: 1; min-width: 0; } .station-name { font-size: 15px; font-weight: 500; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .station-addr { font-size: 13px; color: var(--color-hint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .station-price { font-size: 20px; font-weight: 700; flex-shrink: 0; } .station-price sup { font-size: 13px; vertical-align: super; } .fav-btn { background: none; border: none; cursor: pointer; padding: 6px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; -webkit-tap-highlight-color: transparent; } .fav-btn { position: relative; overflow: visible; } .fav-btn svg { transition: transform var(--dur-3) var(--ease-out-fast); position: relative; z-index: 1; } .fav-btn:active svg { transform: scale(0.8); } .fav-btn svg path { fill: none; stroke: var(--color-hint); stroke-width: 1.5; transition: fill var(--dur-3) var(--ease-out), stroke var(--dur-3) var(--ease-out); } .fav-btn.active svg path { fill: var(--color-favorite); stroke: var(--color-favorite); } /* Sparkle particles container */ .fav-sparkles { position: absolute; inset: -8px; pointer-events: none; z-index: 0; } .fav-sparkle { position: absolute; width: 4px; height: 4px; border-radius: 50%; top: 50%; left: 50%; opacity: 0; } .fav-btn.anim-pop svg { animation: fav-sparkle-star 0.5s var(--ease-spring-lg); } .fav-btn.anim-pop .fav-sparkle:nth-child(1) { animation: fav-spark-out 0.55s 0.05s ease-out forwards; --spark-x: -14px; --spark-y: -12px; background: var(--color-favorite); } .fav-btn.anim-pop .fav-sparkle:nth-child(2) { animation: fav-spark-out 0.5s 0.08s ease-out forwards; --spark-x: 12px; --spark-y: -14px; background: #ffcc4d; } .fav-btn.anim-pop .fav-sparkle:nth-child(3) { animation: fav-spark-out 0.6s 0.03s ease-out forwards; --spark-x: 15px; --spark-y: 4px; background: var(--color-favorite); } .fav-btn.anim-pop .fav-sparkle:nth-child(4) { animation: fav-spark-out 0.5s 0.1s ease-out forwards; --spark-x: -12px; --spark-y: 10px; background: #ffcc4d; } .fav-btn.anim-pop .fav-sparkle:nth-child(5) { animation: fav-spark-out 0.55s 0.06s ease-out forwards; --spark-x: 4px; --spark-y: 15px; background: #ffd983; } .fav-btn.anim-pop .fav-sparkle:nth-child(6) { animation: fav-spark-out 0.5s 0.12s ease-out forwards; --spark-x: -16px; --spark-y: -2px; background: #ffd983; } .fav-btn.anim-pop .fav-sparkle-ring { animation: fav-ring-burst 0.5s 0.02s ease-out forwards; } .fav-sparkle-ring { position: absolute; top: 50%; left: 50%; width: 0; height: 0; border-radius: 50%; border: 2px solid var(--color-favorite); transform: translate(-50%, -50%); opacity: 0; pointer-events: none; } @keyframes fav-sparkle-star { 0% { transform: scale(0.3); } 30% { transform: scale(1.2); } 60% { transform: scale(0.9); } 100% { transform: scale(1); } } @keyframes fav-spark-out { 0% { opacity: 1; transform: translate(0, 0) scale(1); } 50% { opacity: 1; transform: translate(var(--spark-x), var(--spark-y)) scale(1.2); } 100% { opacity: 0; transform: translate(calc(var(--spark-x) * 1.3), calc(var(--spark-y) * 1.3)) scale(0); } } @keyframes fav-ring-burst { 0% { width: 0; height: 0; opacity: 0.8; border-width: 3px; } 60% { opacity: 0.4; border-width: 1.5px; } 100% { width: 36px; height: 36px; opacity: 0; border-width: 0.5px; } } .fav-btn.anim-unpop svg { animation: fav-unpop 0.3s var(--ease-out-fast); } @keyframes fav-unpop { 0% { transform: scale(1); } 40% { transform: scale(0.7); } 100% { transform: scale(1); } } .station-dist { font-size: 12px; color: var(--color-hint); text-align: right; } /* Toolbar toggles (favourites pin + group-by-price) sit in the sort bar and match the sort button's chip-style theming so the row reads as a cohesive control strip. Inactive sits on the secondary surface, active fills with the accent and flips the icon to accent-text. */ .station-fav-btn { background: var(--color-bg-secondary); border: none; padding: 7px; cursor: pointer; color: var(--color-hint); display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; transition: color var(--dur-1) var(--ease-out), background var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out-fast); -webkit-tap-highlight-color: transparent; } .station-fav-btn:hover { background: var(--color-separator); color: var(--color-text); } .station-fav-btn:active { transform: scale(0.94); } .station-fav-btn.active { background: var(--color-accent); color: var(--color-accent-text); } .station-fav-btn.active:hover { background: var(--color-accent); color: var(--color-accent-text); filter: brightness(1.05); } /* Cloud-sync indicator sits right beside the toolbar toggles. Only visible during the syncing → synced burst, then fades back out — idle has no point taking permanent visual space. */ body.logged-in .station-fav-sync { display: inline-flex; opacity: 0; transition: opacity var(--dur-3) var(--ease-soft); pointer-events: none; } body.logged-in .station-fav-sync.syncing, body.logged-in .station-fav-sync.synced { opacity: 1; pointer-events: auto; } /* === Chart === */ .chart-container { background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-lg); padding: var(--sp-4) var(--sp-2) var(--sp-2); height: 280px; position: relative; } .chart-container canvas { width: 100% !important; height: 100% !important; } /* === Bottom Sheet === */ .bottom-sheet { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000; /* Must outlast the content's slide, or closing clips the animation the moment visibility flips. (Opening is unaffected: visibility resolves to `visible` as soon as the transition starts.) */ transition: visibility var(--dur-6) var(--ease-out); } .bottom-sheet.hidden { visibility: hidden; pointer-events: none; } /* Plain dim, no backdrop blur: blurring the map behind the sheet obscured the thing the user was just looking at, and on a full-screen backdrop it's the most expensive effect on the page. */ .bottom-sheet-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); opacity: 0; transition: opacity var(--dur-4) var(--ease-soft); } .bottom-sheet:not(.hidden) .bottom-sheet-backdrop { opacity: 1; } .bottom-sheet-content { position: absolute; bottom: 0; left: 0; right: 0; background: var(--color-bg); border-radius: 16px 16px 0 0; max-height: 85vh; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; touch-action: pan-y; transform: translateY(100%); /* --ease-emphasis is the iOS sheet curve: it leaves the bottom edge almost instantly, then glides the last few pixels. --dur-6 gives that glide room to be felt — at --dur-4 the tail is over before it registers. */ transition: transform var(--dur-6) var(--ease-emphasis), max-height var(--dur-4) var(--ease-emphasis), border-radius var(--dur-4) var(--ease-out); padding-bottom: var(--safe-bottom); } .bottom-sheet:not(.hidden) .bottom-sheet-content { transform: translateY(0); } .bottom-sheet-content.expanded { max-height: 95vh; border-radius: 8px 8px 0 0; } .bottom-sheet-content.dragging { transition: none !important; overflow-y: hidden; } .bottom-sheet-content.snapping { transition: transform var(--dur-4) var(--ease-emphasis) !important; } .bottom-sheet-handle-area { padding: 10px 0 6px; cursor: grab; touch-action: none; } .bottom-sheet-handle-area:active { cursor: grabbing; } .bottom-sheet-handle { width: 36px; height: 4px; border-radius: 2px; background: var(--color-hint); margin: 0 auto; opacity: 0.5; } /* --- Station detail: block cascade --------------------------------------- The detail surface is a stack of blocks (price, address, status, opening hours, navigation, chart). Revealing them in sequence rather than all at once gives the eye an order to read them in, and covers the moment the skeleton is swapped for real data. The header is deliberately excluded: it's already on screen from the skeleton, so re-animating it would flicker the one thing that should stay put. Everything fills in beneath a fixed title. */ @keyframes sheet-block-enter { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } } #bottom-sheet-body.is-revealing > *:nth-child(1) { --i: 0; } #bottom-sheet-body.is-revealing > *:nth-child(2) { --i: 1; } #bottom-sheet-body.is-revealing > *:nth-child(3) { --i: 2; } #bottom-sheet-body.is-revealing > *:nth-child(4) { --i: 3; } #bottom-sheet-body.is-revealing > *:nth-child(5) { --i: 4; } #bottom-sheet-body.is-revealing > *:nth-child(6) { --i: 5; } #bottom-sheet-body.is-revealing > *:nth-child(7) { --i: 6; } #bottom-sheet-body.is-revealing > *:nth-child(n+8) { --i: 7; } #bottom-sheet-body.is-revealing > *:not(.sheet-station-header) { animation-name: sheet-block-enter; animation-duration: var(--dur-4); animation-timing-function: var(--ease-out); animation-fill-mode: both; animation-delay: calc(var(--i, 0) * 34ms); } /* === Station detail modal — used when the user opens a station from the Stats Tankstellen-Ranking or the Verlauf cheapest/most-expensive cards. Same internal layout as the map bottom-sheet, but presented as a floating card in the middle of the screen instead of a drag-to-dismiss sheet glued to the bottom edge. */ .bottom-sheet.detail-modal { display: flex; align-items: center; justify-content: center; padding: 24px; box-sizing: border-box; /* Snaps shut faster than the drag-sheet slides, so its visibility flip shouldn't wait on the sheet's longer window. */ transition: visibility var(--dur-4) var(--ease-out); } /* Dims harder than the drag-sheet — it's a focus surface, not a peek at the map underneath. Separation comes from the dim alone; no blur. */ .bottom-sheet.detail-modal .bottom-sheet-backdrop { background: rgba(0, 0, 0, 0.55); } .bottom-sheet.detail-modal .bottom-sheet-content { position: relative; bottom: auto; left: auto; right: auto; width: min(440px, 100%); max-height: 86vh; border-radius: 20px; padding-bottom: 8px; transform-origin: center center; transform: scale(0.92); opacity: 0; box-shadow: 0 24px 64px rgba(0, 0, 0, 0.32), 0 8px 24px rgba(0, 0, 0, 0.18); /* This is the CLOSING state, so it carries the exit curve: the modal holds its size for a moment, then drops away with gathering speed. Using the decel curve here made it drift off, which read as lag. */ transition: transform var(--dur-3) var(--ease-in), opacity var(--dur-2) var(--ease-in); } .bottom-sheet.detail-modal:not(.hidden) .bottom-sheet-content { transform: scale(1); opacity: 1; /* Tiny overshoot on the way in: scale up to 1.02 mid-flight, settle back to 1. Soft "pop" without the awkward translate-up. */ animation: detail-modal-pop var(--dur-5) var(--ease-out) both; } @keyframes detail-modal-pop { 0% { transform: scale(0.92); opacity: 0; } 60% { transform: scale(1.02); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } .bottom-sheet.detail-modal .bottom-sheet-handle-area { display: none; } .bottom-sheet.detail-modal .sheet-expand-btn { display: none; } .detail-modal-close { position: absolute; top: 12px; right: 12px; z-index: 2; width: 32px; height: 32px; border-radius: 50%; background: color-mix(in srgb, var(--color-text) 8%, transparent); border: none; color: var(--color-text); cursor: pointer; display: grid; place-items: center; transition: background var(--dur-1) var(--ease-soft), transform var(--dur-1) var(--ease-out); padding: 0; } .detail-modal-close:hover { background: color-mix(in srgb, var(--color-text) 14%, transparent); } .detail-modal-close:active { transform: scale(0.92); } /* Skeleton placeholders shown while we fetch the full detail data. */ .detail-skeleton { display: flex; flex-direction: column; gap: 10px; padding: 4px 20px 20px; } .detail-skeleton-line, .detail-skeleton-block { border-radius: 8px; background: linear-gradient( 90deg, color-mix(in srgb, var(--color-text) 8%, transparent) 0%, color-mix(in srgb, var(--color-text) 14%, transparent) 50%, color-mix(in srgb, var(--color-text) 8%, transparent) 100% ); background-size: 200% 100%; animation: detail-skeleton-shimmer 1.4s ease-in-out infinite; } .detail-skeleton-line { height: 14px; width: 100%; } .detail-skeleton-line.short { width: 60%; } .detail-skeleton-line.detail-skeleton-price { height: 36px; width: 50%; margin-bottom: 6px; } .detail-skeleton-block { height: 140px; margin-top: 8px; } @keyframes detail-skeleton-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } @media (prefers-reduced-motion: reduce) { .bottom-sheet.detail-modal .bottom-sheet-content, .bottom-sheet.detail-modal:not(.hidden) .bottom-sheet-content { transition: opacity var(--dur-1) var(--ease-soft); transform: none; animation: none; } .detail-skeleton-line, .detail-skeleton-block { animation: none; } } /* Sheet station detail */ .sheet-station-header { padding: 12px 20px 8px; display: flex; align-items: flex-start; gap: 8px; } .sheet-fav-btn { margin-top: 2px; } .sheet-station-name { font-size: 18px; font-weight: 700; color: var(--color-text); } .sheet-station-brand { font-size: 14px; color: var(--color-hint); margin-top: 2px; } .sheet-station-price { font-size: 36px; font-weight: 800; padding: 8px 20px; } .sheet-station-price sup { font-size: 18px; vertical-align: super; } .sheet-info-row { display: flex; align-items: center; gap: 10px; padding: 10px 20px; font-size: 14px; color: var(--color-text); border-top: 0.5px solid var(--color-separator); } .sheet-info-icon { font-size: 18px; width: 24px; text-align: center; flex-shrink: 0; } /* Sheet opening hours */ .sheet-hours-section { overflow: hidden; } .sheet-hours-toggle { display: flex; align-items: center; gap: 10px; padding: 10px 20px; cursor: pointer; -webkit-user-select: none; user-select: none; font-size: 14px; color: var(--color-text); border-top: 0.5px solid var(--color-separator); transition: background var(--dur-1) var(--ease-out); } .sheet-hours-toggle:active { background: var(--color-inset); } .sheet-hours-chevron { transition: transform var(--dur-3) var(--ease-soft); } .sheet-hours-section.expanded .sheet-hours-chevron { transform: rotate(180deg); } .sheet-hours-list { max-height: 0; overflow: hidden; transition: max-height var(--dur-3) var(--ease-soft), opacity var(--dur-3) var(--ease-soft); opacity: 0; padding: 0 20px; } .sheet-hours-section.expanded .sheet-hours-list { max-height: 300px; opacity: 1; padding: 0 20px 10px; } .sheet-hours-row { display: flex; justify-content: space-between; /* 24 px icon slot + 10 px gap = 34 px text indent, matching the toggle row. */ padding: 5px 0 5px 34px; font-size: 13px; } .sheet-hours-day { color: var(--color-hint); font-weight: 500; } .sheet-hours-time { color: var(--color-text); font-weight: 600; font-variant-numeric: tabular-nums; } /* Sheet chart */ .sheet-chart-section { padding: 0 20px 8px; } .sheet-chart-header-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 0 8px; } .sheet-chart-header { font-size: 13px; font-weight: 500; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.5px; } .sheet-chart-toggle { display: flex; gap: 4px; background: var(--color-inset); border-radius: 8px; padding: 2px; } .sheet-toggle-btn { border: none; background: transparent; color: var(--color-hint); font-size: 12px; font-weight: 500; padding: 4px 10px; border-radius: 6px; cursor: pointer; transition: all var(--dur-2) var(--ease-out); } .sheet-toggle-btn.active { background: var(--color-accent); color: #fff; } .sheet-toggle-btn.disabled { opacity: 0.35; cursor: not-allowed; } .sheet-chart-container { background: var(--color-inset); border-radius: 12px; padding: 12px 8px 8px; height: 120px; position: relative; transition: height var(--dur-4) var(--ease-emphasis); } .bottom-sheet-content.expanded .sheet-chart-container { height: 28vh; } .sheet-chart-container canvas { width: 100% !important; height: 100% !important; } .sheet-chart-empty { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--color-hint); font-size: 13px; } /* Friendly empty-state for German stations that aren't in the scanner — points the user at the location-request flow. */ .sheet-chart-empty-msg { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 6px; padding: 12px 16px; height: 100%; max-width: 320px; margin: 0 auto; } .sheet-chart-empty-title { font-size: 13px; font-weight: 600; color: var(--color-text); } .sheet-chart-empty-hint { font-size: 12px; color: var(--color-hint); line-height: 1.4; } .sheet-chart-empty-btn { margin-top: 6px; display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 999px; border: none; background: var(--color-accent); color: var(--color-accent-text); font-size: 13px; font-weight: 600; cursor: pointer; -webkit-tap-highlight-color: transparent; } .sheet-chart-empty-btn:active { opacity: 0.85; transform: scale(0.97); } /* Subtle pill for manual-scan stations — ephemeral data (15 min TTL). Freshness colour comes from --scan-color which is set on #bottom-sheet-body each tick (green → yellow → red). The pill itself stays low-key: tinted background, hint text, small pulsing dot, hairline progress strip along the inner-bottom edge. */ #bottom-sheet-body { --scan-color: #22c55e; } .sheet-manual-scan-banner-wrap { padding: 4px 20px 10px; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } /* Small pill button in the sheet header that rescans the 25 km circle * around the current station — same action as "Hier suchen" but anchored * to the tapped pin. Sits next to the freshness banner when there is one, * on its own otherwise. */ .sheet-refresh-btn { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px 6px; border-radius: 999px; border: 1px solid var(--color-border); background: var(--color-inset); color: var(--color-text); font-size: 11.5px; font-weight: 500; cursor: pointer; transition: background var(--dur-1) var(--ease-soft), border-color var(--dur-1) var(--ease-soft), opacity var(--dur-1) var(--ease-soft); } .sheet-refresh-btn:hover:not(:disabled) { background: var(--color-border); } .sheet-refresh-btn:active:not(:disabled) { transform: scale(0.97); } .sheet-refresh-btn:disabled { opacity: 0.45; cursor: not-allowed; } .sheet-refresh-btn svg { flex-shrink: 0; } .sheet-manual-scan-banner { --scan-track: rgba(148, 163, 184, 0.18); display: inline-flex; align-items: center; gap: 7px; padding: 5px 11px 6px; border-radius: 999px; background: color-mix(in srgb, var(--scan-color) 10%, transparent); font-size: 11.5px; color: var(--color-hint); max-width: 100%; position: relative; overflow: hidden; transition: background 1s linear; } .sheet-manual-scan-text { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .sheet-manual-scan-countdown { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--color-text); } /* Small pulsing dot. */ .sheet-manual-scan-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--scan-color); flex-shrink: 0; animation: manualScanPulse 1.8s ease-in-out infinite; transition: background 1s linear; } @keyframes manualScanPulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.55; transform: scale(0.8); } } /* Hairline progress strip hugging the inside-bottom of the pill. */ .sheet-manual-scan-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--scan-track); } .sheet-manual-scan-progress-bar { height: 100%; width: 100%; background: var(--scan-color); transition: width 0.9s linear, background 1s linear; } /* Last minute: only the dot pulses faster. */ .sheet-manual-scan-banner.urgent .sheet-manual-scan-dot { animation-duration: 0.9s; } .sheet-manual-scan-banner.expired { opacity: 0.55; } .sheet-manual-scan-banner.expired .sheet-manual-scan-dot { animation: none; } .sheet-expand-btn { display: none; position: absolute; top: 12px; right: 16px; width: 32px; height: 32px; border: none; background: var(--color-inset); border-radius: 8px; cursor: pointer; align-items: center; justify-content: center; color: var(--color-text); z-index: 2; transition: background var(--dur-1) var(--ease-out); } .sheet-expand-btn:hover { background: var(--color-border); } .sheet-expand-btn svg { width: 18px; height: 18px; } @media (hover: hover) and (pointer: fine) { .sheet-expand-btn { display: flex; } .sheet-station-header { padding-right: 52px; } } .sheet-nav-buttons { display: flex; gap: 10px; padding: 16px 20px; } .sheet-nav-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 13px 16px; border-radius: 14px; border: none; font-family: inherit; font-size: 14px; font-weight: 600; cursor: pointer; text-decoration: none; transition: transform var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out); } .sheet-nav-btn:active { transform: scale(0.95); } .sheet-nav-btn.gmaps, .sheet-nav-btn.amaps { background: var(--color-inset); color: var(--color-text); border: 1px solid var(--color-separator); } .sheet-nav-icon { border-radius: 5px; flex-shrink: 0; } /* === Stats === */ .stat-row { display: flex; gap: var(--sp-2); padding: 0 var(--sp-4); margin-bottom: var(--sp-2); } .stat-card { flex: 1; background: var(--color-bg-secondary); border: 1px solid var(--color-separator); border-radius: var(--r-md); padding: var(--sp-3) var(--sp-3); text-align: center; } .stat-card-value { font-size: 20px; font-weight: 700; color: var(--color-text); letter-spacing: -0.01em; } .stat-card-label { font-size: 12px; color: var(--color-hint); margin-top: var(--sp-1); } .ranking-item { display: flex; align-items: center; padding: var(--sp-3) var(--sp-4); gap: var(--sp-3); border-bottom: 0.5px solid var(--color-separator); transition: transform var(--dur-1) var(--ease-out-fast), background var(--dur-1) var(--ease-soft); } .ranking-item:hover { background: var(--color-separator); } .ranking-item:active { transform: scale(0.98); } .ranking-pos { font-size: 13px; font-weight: 700; color: var(--color-hint); width: 24px; text-align: center; font-variant-numeric: tabular-nums; } .ranking-name { flex: 1; font-size: 14px; color: var(--color-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ranking-price { font-size: 15px; font-weight: 600; flex-shrink: 0; font-variant-numeric: tabular-nums; } /* === Tab Bar === */ .tab-bar { position: fixed; bottom: 0; left: 0; right: 0; height: calc(var(--tab-height) + var(--safe-bottom)); padding-bottom: var(--safe-bottom); background: color-mix(in srgb, var(--color-bg-secondary) 88%, transparent); -webkit-backdrop-filter: blur(18px) saturate(1.6); backdrop-filter: blur(18px) saturate(1.6); border-top: 0.5px solid var(--color-separator); display: flex; align-items: center; justify-content: space-around; z-index: 100; } .tab-item { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; background: none; border: none; cursor: pointer; padding: 6px 14px; border-radius: 10px; -webkit-user-select: none; user-select: none; color: var(--color-hint); transition: color var(--dur-3) var(--ease-soft), transform var(--dur-0) var(--ease-out-fast); } /* The selected pill is its own layer so it can spring in from the centre. Animating `background` alone just cross-fades a rectangle — scaling a pseudo-element gives the selection somewhere to come *from*. */ .tab-item::before { content: ''; position: absolute; inset: 0; border-radius: 10px; background: color-mix(in srgb, var(--color-accent) 12%, transparent); opacity: 0; transform: scale(0.72); pointer-events: none; transition: opacity var(--dur-2) var(--ease-soft), transform var(--dur-4) var(--ease-spring); } .tab-item.active { color: var(--color-accent); } .tab-item.active::before { opacity: 1; transform: scale(1); } .tab-item:active { transform: scale(0.93); } /* Softer than the old 1.18 overshoot — at 24 px the big jump read as a glitch rather than a bounce. */ @keyframes tab-bounce { 0% { transform: scale(1); } 38% { transform: scale(1.12); } 70% { transform: scale(0.97); } 100% { transform: scale(1); } } .tab-item.active .tab-icon { animation: tab-bounce var(--dur-5) var(--ease-spring); } .tab-icon { width: 24px; height: 24px; fill: currentColor; position: relative; } .tab-item span { font-size: 10px; font-weight: 500; position: relative; transition: font-weight var(--dur-2) var(--ease-soft); } .tab-item.active span { font-weight: 600; } /* === Location chip in settings === */ /* Leaflet popup override */ .leaflet-popup-content-wrapper { border-radius: 12px !important; box-shadow: 0 4px 20px rgba(0,0,0,0.15) !important; } .leaflet-popup-content { margin: 10px 14px !important; font-family: inherit !important; font-size: 14px !important; } /* Map bubble markers (tankste! style) */ .map-bubble { position: relative; display: flex; flex-direction: column; align-items: center; padding: 3px 8px 4px; border-radius: 8px; color: #fff; white-space: nowrap; box-shadow: 0 2px 8px rgba(0,0,0,0.4); text-align: center; min-width: 48px; } .map-bubble-brand { font-size: 9px; font-weight: 500; opacity: 0.9; letter-spacing: 0.3px; line-height: 1.1; } .map-bubble-price { font-size: 15px; font-weight: 800; line-height: 1.2; letter-spacing: -0.3px; } .map-bubble-price sup { font-size: 9px; vertical-align: super; } .map-bubble-arrow { position: absolute; bottom: -6px; left: 50%; margin-left: -6px; width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid; } /* Cluster marker overrides */ .marker-cluster-small, .marker-cluster-medium, .marker-cluster-large { background: none !important; } .marker-cluster-small div, .marker-cluster-medium div, .marker-cluster-large div { background: none !important; } /* Empty state */ .empty-state { text-align: center; padding: 40px 20px; color: var(--color-hint); } .empty-state-icon { font-size: 48px; margin-bottom: 12px; } .empty-state-text { font-size: 15px; } /* Map floating action button — sits above the bottom sheet's resting peek on mobile by leaving a generous bottom inset. On desktop the sheet is a side panel, so the FAB returns to a tighter offset (overridden below). */ .map-fab { position: absolute; bottom: 20px; right: 16px; z-index: 1000; width: 46px; height: 46px; border-radius: var(--r-md); border: none; background: var(--color-bg); color: var(--color-accent); box-shadow: var(--shadow-3); cursor: pointer; display: flex; align-items: center; justify-content: center; pointer-events: auto; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: transform var(--dur-1) var(--ease-out-fast), opacity var(--dur-1) var(--ease-soft); } .map-fab:active { opacity: 0.7; transform: scale(0.92); } /* Row that holds the "Search here" pill (and, in scan-picker mode, a round cancel button to its left). Lives inside #map-search-top-row, inline to the right of the search box, so it sits on the same line as the search bar. Hidden (display:none below) until the map has moved, at which point the box shrinks to make room. */ .map-search-here-row { display: flex; align-items: center; justify-content: center; gap: 8px; flex: 0 0 auto; margin-top: 0; pointer-events: none; } .map-search-here-row > * { pointer-events: auto; } .map-search-here-row:empty, .map-search-here-row:has(> .map-search-here.hidden:only-child) { display: none; } .map-search-here { display: inline-flex; align-items: center; gap: 8px; padding: 10px 18px; border-radius: 999px; border: none; background: var(--color-accent); color: var(--color-accent-text); font-size: 14px; font-weight: 600; box-shadow: 0 4px 14px rgba(0,0,0,0.2); cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; animation: searchHerePopIn 0.26s var(--ease-spring-lg); white-space: nowrap; } .map-search-here:active { opacity: 0.85; transform: scale(0.96); } .map-search-here.hidden { display: none; } .map-search-here[disabled] { background: var(--color-hint); color: var(--color-bg); opacity: 0.75; cursor: not-allowed; box-shadow: 0 2px 6px rgba(0,0,0,0.12); } .map-search-here[disabled]:active { transform: none; opacity: 0.75; } @keyframes searchHerePopIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } } /* "Confirm" state of the search-here pill (during scan-picker mode) */ .map-search-here.is-confirm { background: var(--color-accent); color: var(--color-accent-text); box-shadow: 0 6px 18px rgba(0, 122, 255, 0.35); } /* Round cancel pill placed left of the confirm button while picking. JS inserts it as a sibling of the search-here pill inside .map-search-here-row, so flex centering handles the side-by-side layout. */ .map-scan-cancel { width: 38px; height: 38px; border-radius: 50%; border: none; background: var(--color-bg); color: var(--color-text); display: inline-flex; align-items: center; justify-content: center; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18); cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation; animation: searchHerePopIn 0.22s ease-out; flex-shrink: 0; } .map-scan-cancel:active { opacity: 0.85; transform: scale(0.94); } /* Draggable pin shown in the centre of the scan-radius picker circle */ .scan-pin { width: 28px; height: 28px; border-radius: 50%; background: color-mix(in srgb, var(--scan-accent, #007aff) 18%, transparent); border: 2px solid var(--scan-accent, #007aff); display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); cursor: grab; } .scan-pin:active { cursor: grabbing; } .scan-pin-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--scan-accent, #007aff); box-shadow: 0 0 0 3px var(--color-bg); animation: scanPinPulse 1.6s ease-in-out infinite; } @keyframes scanPinPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(0.78); opacity: 0.85; } } /* Yellow pin shown for queued scan requests waiting on the cooldown. Click to remove the pin (and its queue entry). */ .scan-queue-pin { width: 24px; height: 24px; border-radius: 50%; background: rgba(255, 193, 7, 0.22); border: 2px solid #ffc107; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); cursor: pointer; } .scan-queue-pin-dot { width: 8px; height: 8px; border-radius: 50%; background: #ffc107; box-shadow: 0 0 0 2px var(--color-bg); animation: scanQueuePinPulse 1.4s ease-in-out infinite; } @keyframes scanQueuePinPulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(0.65); opacity: 0.65; } } /* The scan UI follows what the user is LOOKING at (map centre), not where their pin sits — so searching Salzburg then panning to Munich should still show the "Hier suchen" pill. Keyed off data-view-country. */ [data-view-country="at"] .map-search-here, [data-view-country="at"] .map-scan-cancel, [data-view-country="at"] .map-search-here-row, [data-view-country="at"] .scan-queue-pin { display: none !important; } /* Lift dark-mode tiles so place names stay readable without washing out the dark background — brightness pulls labels up, lower contrast prevents them from collapsing back into the background. */ .map-tiles-dark { filter: brightness(1.75) contrast(0.95); } /* Match the Leaflet container background to the active tile palette so loading tiles don't flash white before they paint. */ [data-theme="dark"] .leaflet-container { background: #313131; } @media (prefers-color-scheme: dark) { html:not([data-theme="light"]) .leaflet-container { background: #313131; } } /* Disable touch/select quirks on Leaflet chrome */ .leaflet-container, .leaflet-pane, .leaflet-control-container, .leaflet-control-attribution, .leaflet-control-zoom { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; } /* Hidden attribution for cleaner UX */ .leaflet-control-attribution { display: none !important; } /* Settings Row (label + dropdown) */ .settings-row { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4); gap: var(--sp-3); } .settings-row-left { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; } .settings-row-left span { text-transform: none; letter-spacing: 0; } .settings-select { -webkit-appearance: none; appearance: none; background: var(--color-inset); color: var(--color-text); border: 1px solid var(--color-separator); border-radius: 8px; padding: 8px 36px 8px 12px; font-size: 14px; font-weight: 500; font-family: inherit; cursor: pointer; outline: none; transition: border-color var(--dur-1) var(--ease-out); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2398989e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; background-size: 16px; min-width: 0; max-width: 160px; } .settings-select:focus { border-color: var(--color-accent); } .settings-select option { background: var(--color-bg); color: var(--color-text); } /* Alert Channel Picker (reuses theme-seg pattern) */ .alert-ch-seg { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 0; border: none; border-radius: 8px; background: transparent; color: var(--color-hint); font-size: 13px; font-weight: 600; cursor: pointer; transition: background var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out); -webkit-user-select: none; user-select: none; } .alert-ch-seg:hover:not(.active) { color: var(--color-text-2); } .alert-ch-seg.active { background: var(--color-accent); color: var(--color-accent-text); box-shadow: 0 2px 8px color-mix(in srgb, var(--color-accent) 30%, transparent); } .alert-ch-seg:active { transform: scale(0.97); } .alert-ch-seg svg { flex-shrink: 0; } /* Toggle Switch */ .toggle-switch { position: relative; display: inline-block; width: 51px; height: 31px; flex-shrink: 0; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .toggle-track { position: absolute; cursor: pointer; inset: 0; background: color-mix(in srgb, var(--color-text) 16%, transparent); border-radius: 31px; transition: background var(--dur-3) var(--ease-soft); } /* The knob travels on a spring while the track's colour fades on the plain curve — the offset between the two is what gives a toggle its weight. */ .toggle-track:before { content: ""; position: absolute; width: 27px; height: 27px; left: 2px; bottom: 2px; background: white; border-radius: 50%; transform-origin: center; transition: transform var(--dur-4) var(--ease-spring), width var(--dur-2) var(--ease-out); box-shadow: 0 1px 3px rgba(0,0,0,0.25), 0 2px 6px rgba(0,0,0,0.1); } .toggle-switch input:checked + .toggle-track { background: var(--color-good); } .toggle-switch input:checked + .toggle-track:before { transform: translateX(20px); } /* Held down, the knob stretches toward its travel direction and snaps back on release — the same trick the iOS switch uses. */ .toggle-track:active:before { width: 32px; } .toggle-switch input:checked + .toggle-track:active:before { transform: translateX(15px); } /* === Price Alert card — redesigned for the settings tab === */ .alert-card { padding: 0; overflow: hidden; transition: border-color var(--dur-3) var(--ease-soft), background var(--dur-3) var(--ease-soft); } .alert-card.is-armed { border-color: color-mix(in srgb, var(--color-accent) 30%, var(--color-separator)); } .alert-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-4); } .alert-toggle-left { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; } .alert-toggle-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .alert-toggle-title { font-size: 15px; font-weight: 600; color: var(--color-text); } .alert-toggle-sub { font-size: 11px; color: var(--color-hint); line-height: 1.4; } .alert-config-body { border-top: 1px solid var(--color-separator); padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-4); } .alert-config-group { display: flex; flex-direction: column; gap: 6px; } .alert-config-label { font-size: 11px; font-weight: 600; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.5px; } /* Threshold hero — integrated into the card body, no competing inner panel */ .alert-threshold { display: flex; flex-direction: column; align-items: stretch; gap: 14px; padding: 4px 0 2px; } .alert-threshold-head { font-size: 11px; font-weight: 600; color: var(--color-hint); text-transform: uppercase; letter-spacing: 0.6px; text-align: center; } .alert-threshold-row { display: flex; align-items: center; justify-content: center; gap: 22px; } .alert-step-btn { width: 36px; height: 36px; border-radius: 50%; border: 1px solid transparent; background: color-mix(in srgb, var(--color-text) 6%, transparent); color: var(--color-text); display: grid; place-items: center; cursor: pointer; font-family: inherit; -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; transition: background var(--dur-1) var(--ease-out), transform var(--dur-0) var(--ease-out), color var(--dur-1) var(--ease-out); } .alert-step-btn:hover { background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-accent); } .alert-step-btn:active { transform: scale(0.92); } .alert-step-btn svg { width: 18px; height: 18px; } .alert-threshold-display { font-size: 42px; font-weight: 700; color: var(--color-text); letter-spacing: -0.03em; font-variant-numeric: tabular-nums; min-width: 132px; width: 132px; text-align: center; line-height: 1; font-family: inherit; background: transparent; border: 0; outline: 0; padding: 6px 4px; border-radius: 10px; box-sizing: border-box; caret-color: var(--color-accent); -webkit-appearance: none; appearance: none; transition: background var(--dur-1) var(--ease-out); } .alert-threshold-display:focus { background: color-mix(in srgb, var(--color-accent) 12%, transparent); } .alert-threshold-display::-webkit-outer-spin-button, .alert-threshold-display::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .alert-threshold-bar { position: relative; margin-top: 2px; cursor: pointer; touch-action: none; -webkit-user-select: none; user-select: none; } .alert-threshold-bar:focus { outline: none; } .alert-threshold-bar:focus-visible .alert-threshold-bar-marker { box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-accent) 22%, transparent), 0 0 0 7px color-mix(in srgb, var(--color-accent) 36%, transparent), 0 2px 8px rgba(0, 0, 0, 0.22); } .alert-threshold-bar[hidden] { display: none; } .alert-threshold-bar-track { height: 5px; border-radius: 999px; background: color-mix(in srgb, var(--color-text) 10%, transparent); } .alert-threshold-bar-current, .alert-threshold-bar-marker { position: absolute; top: 2.5px; /* center of the 5px-tall track, regardless of label height below */ transform: translate(-50%, -50%); } .alert-threshold-bar-current { width: 7px; height: 7px; border-radius: 50%; background: var(--color-hint); border: 2px solid var(--color-bg-secondary); } .alert-threshold-bar-marker { width: 16px; height: 16px; border-radius: 50%; background: var(--color-accent); border: 2px solid var(--color-bg-secondary); box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-accent) 22%, transparent), 0 2px 8px rgba(0, 0, 0, 0.22); transition: left var(--dur-2) var(--ease-out); cursor: grab; } .alert-threshold-bar.is-dragging { cursor: grabbing; } .alert-threshold-bar.is-dragging .alert-threshold-bar-marker { transition: none; cursor: grabbing; } .alert-threshold-bar-labels { display: flex; justify-content: space-between; margin-top: 12px; font-size: 11px; color: var(--color-text-2); font-variant-numeric: tabular-nums; letter-spacing: 0.2px; opacity: 0.7; } .alert-ref-price { text-align: center; font-size: 12px; color: var(--color-hint); margin-top: 4px; letter-spacing: 0.1px; } /* Channel section */ .alert-channel-section { display: flex; flex-direction: column; gap: 8px; margin-top: var(--sp-2); } .alert-channel-picker { display: flex; border-radius: 10px; background: color-mix(in srgb, var(--color-text) 5%, transparent); padding: 3px; gap: 3px; } .alert-input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--color-separator); background: var(--color-inset); color: var(--color-text); font-size: 14px; font-family: inherit; box-sizing: border-box; transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out); } .alert-input:focus-visible { border-color: var(--color-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 15%, transparent); outline: none; } .alert-input-hint { font-size: 11px; color: var(--color-hint); margin-top: 6px; line-height: 1.45; } /* Status / background-check panel */ .alert-status-panel { display: flex; flex-direction: column; gap: 8px; padding: 14px 16px; border-radius: 10px; background: color-mix(in srgb, var(--color-text) 5%, transparent); transition: background var(--dur-2) var(--ease-out); } .alert-status-panel.is-armed { background: color-mix(in srgb, var(--color-good) 8%, transparent); } .alert-status-panel.is-below { background: color-mix(in srgb, var(--color-accent) 9%, transparent); } .alert-status-row { display: flex; align-items: center; gap: 14px; font-size: 13px; color: var(--color-text); font-weight: 500; } .alert-status-row-secondary { font-size: 11.5px; color: var(--color-hint); font-weight: 500; letter-spacing: 0.1px; } .alert-status-row-secondary[hidden] { display: none; } .alert-status-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--color-hint); box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-hint) 18%, transparent); } .alert-status-row.is-armed .alert-status-dot { background: var(--color-good); box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-good) 22%, transparent); animation: alert-status-pulse 2.6s ease-in-out infinite; } .alert-status-row.is-below .alert-status-dot { background: var(--color-accent); box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-accent) 26%, transparent); animation: none; } .alert-status-icon { flex-shrink: 0; opacity: 0.7; } @keyframes alert-status-pulse { 0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-good) 18%, transparent); } 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-good) 28%, transparent); } } @media (prefers-reduced-motion: reduce) { .alert-status-dot { animation: none; } } /* Action buttons — clear primary/tertiary hierarchy */ .alert-actions { display: flex; flex-direction: column; align-items: stretch; gap: 14px; margin-top: 8px; } .alert-save-btn { width: 100%; padding: 13px; border-radius: 12px; border: none; background: var(--color-accent); color: var(--color-accent-text); font-size: 15px; font-weight: 600; font-family: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: box-shadow var(--dur-1) var(--ease-out), transform var(--dur-0) var(--ease-out), filter var(--dur-1) var(--ease-soft); } .alert-save-btn:hover { box-shadow: 0 6px 18px color-mix(in srgb, var(--color-accent) 35%, transparent); filter: brightness(1.05); } .alert-save-btn:active { transform: scale(0.98); } .alert-test-btn { align-self: center; padding: 8px 12px; border-radius: 8px; border: none; background: transparent; color: var(--color-hint); font-size: 12px; font-weight: 500; font-family: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--dur-1) var(--ease-out), background var(--dur-1) var(--ease-out); } .alert-test-btn:hover { color: var(--color-text); background: color-mix(in srgb, var(--color-text) 5%, transparent); } .alert-active-info { margin-top: 10px; padding: 10px 12px; border-radius: 8px; background: var(--color-inset); font-size: 13px; color: var(--color-hint); text-align: center; } .settings-row-label { font-size: 15px; font-weight: 500; } /* === Location picker select === */ .location-picker { width: 100%; padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm); border: 1px solid var(--color-separator); background: var(--color-bg-secondary); color: var(--color-text); font-size: 14px; font-family: inherit; box-sizing: border-box; transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out); } .location-picker-row { display: flex; flex-direction: column; gap: 4px; } /* === Custom desktop dropdown — desktop replaces the native