mirror of
https://github.com/Felitendo/Tanken.git
synced 2026-11-01 09:03:25 +01:00
6135 lines
152 KiB
CSS
6135 lines
152 KiB
CSS
/* === 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 <select>
|
|
with a styled trigger + animated panel. On mobile the native
|
|
select stays (the OS picker is the right affordance there). === */
|
|
.custom-select { position: relative; width: 100%; }
|
|
.custom-select-trigger,
|
|
.custom-select-panel { display: none; }
|
|
@media (min-width: 900px) {
|
|
.custom-select > select { display: none; }
|
|
.custom-select-trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
width: 100%;
|
|
padding: 9px 14px;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--color-separator);
|
|
background: var(--color-bg-secondary);
|
|
color: var(--color-text);
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition: border-color var(--dur-2) var(--ease-out),
|
|
box-shadow var(--dur-2) var(--ease-out);
|
|
}
|
|
.custom-select-trigger:hover {
|
|
border-color: var(--color-hint);
|
|
}
|
|
.custom-select.open .custom-select-trigger {
|
|
border-color: var(--color-accent);
|
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
|
|
}
|
|
.custom-select-value {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.custom-select-caret {
|
|
flex-shrink: 0;
|
|
color: var(--color-hint);
|
|
transition: transform var(--dur-2) var(--ease-out);
|
|
}
|
|
.custom-select.open .custom-select-caret {
|
|
transform: rotate(180deg);
|
|
}
|
|
.custom-select-panel {
|
|
display: block;
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
top: calc(100% + 6px);
|
|
z-index: 100;
|
|
border-radius: 12px;
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-separator);
|
|
box-shadow: var(--dt-shadow-md);
|
|
overflow: hidden;
|
|
max-height: 280px;
|
|
overflow-y: auto;
|
|
opacity: 0;
|
|
transform: translateY(-6px) scale(0.98);
|
|
transform-origin: top center;
|
|
pointer-events: none;
|
|
transition: opacity var(--dur-1) var(--ease-out), transform var(--dur-2) var(--ease-out);
|
|
}
|
|
.custom-select.open .custom-select-panel {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
pointer-events: auto;
|
|
}
|
|
.custom-select-option {
|
|
padding: 9px 14px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
color: var(--color-text);
|
|
line-height: 1.3;
|
|
transition: background var(--dur-1) var(--ease-out);
|
|
user-select: none;
|
|
}
|
|
.custom-select-option:hover {
|
|
background: color-mix(in srgb, var(--color-accent) 9%, transparent);
|
|
}
|
|
.custom-select-option.selected {
|
|
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
|
color: var(--color-accent);
|
|
font-weight: 600;
|
|
}
|
|
/* In the settings row, the picker sits to the right of an icon + label
|
|
and should match the existing settings-select width. */
|
|
.settings-row .custom-select {
|
|
width: auto;
|
|
min-width: 132px;
|
|
}
|
|
}
|
|
.location-auto-hint {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 11px;
|
|
color: var(--color-hint);
|
|
padding-left: 4px;
|
|
letter-spacing: 0.1px;
|
|
}
|
|
.location-auto-hint[hidden] { display: none; }
|
|
.location-auto-hint svg { opacity: 0.75; flex-shrink: 0; }
|
|
.location-picker:hover {
|
|
border-color: var(--color-hint);
|
|
}
|
|
.location-picker:focus-visible {
|
|
border-color: var(--color-accent);
|
|
outline: none;
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 18%, transparent);
|
|
}
|
|
|
|
.request-location-btn {
|
|
width: 100%;
|
|
padding: 14px 16px;
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: var(--color-accent);
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
border-top: 1px solid var(--color-separator);
|
|
font-family: inherit;
|
|
transition: background var(--dur-1) var(--ease-out);
|
|
}
|
|
.request-location-btn:hover {
|
|
background: var(--color-separator);
|
|
}
|
|
|
|
|
|
/* Rises and settles with a touch of overshoot on the way in, then leaves
|
|
quietly — an exiting toast shouldn't draw the eye back to itself, so the
|
|
dismiss is a plain fade-down on the softer curve. */
|
|
.toast {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: calc(var(--tab-height) + var(--safe-bottom) + 18px);
|
|
transform: translateX(-50%) translateY(20px) scale(0.94);
|
|
background: rgba(17,17,17,0.92);
|
|
color: #fff;
|
|
padding: 12px 16px;
|
|
border-radius: 12px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
box-shadow: 0 10px 24px rgba(0,0,0,0.24);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transform-origin: bottom center;
|
|
transition: opacity var(--dur-2) var(--ease-soft),
|
|
transform var(--dur-3) var(--ease-soft);
|
|
z-index: 1200;
|
|
max-width: min(92vw, 420px);
|
|
text-align: center;
|
|
}
|
|
|
|
.toast.visible {
|
|
opacity: 1;
|
|
transform: translateX(-50%) translateY(0) scale(1);
|
|
transition: opacity var(--dur-2) var(--ease-soft),
|
|
transform var(--dur-5) var(--ease-spring);
|
|
}
|
|
|
|
.is-hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* === PWA Install Popup === */
|
|
.pwa-popup {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 10000;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
}
|
|
.pwa-popup.hidden { display: none; }
|
|
|
|
.pwa-popup-backdrop {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0,0,0,0.6);
|
|
}
|
|
|
|
.pwa-popup-sheet {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 420px;
|
|
background: var(--color-bg);
|
|
border-radius: 20px 20px 0 0;
|
|
padding: 28px 24px calc(24px + var(--safe-bottom));
|
|
box-shadow: 0 -4px 32px rgba(0,0,0,0.18);
|
|
animation: pwa-slide-up 0.35s var(--ease-emphasis);
|
|
text-align: center;
|
|
}
|
|
|
|
@keyframes pwa-slide-up {
|
|
from { transform: translateY(100%); }
|
|
to { transform: translateY(0); }
|
|
}
|
|
|
|
.pwa-popup-icon {
|
|
margin-bottom: 16px;
|
|
}
|
|
.pwa-popup-icon img {
|
|
width: 72px;
|
|
height: 72px;
|
|
border-radius: 16px;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
|
|
}
|
|
html[data-theme="dark"] .pwa-popup-icon img,
|
|
:root:not([data-theme="light"]) .pwa-popup-icon img {
|
|
background: #1c1c1e;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root:not([data-theme="dark"]) .pwa-popup-icon img {
|
|
background: transparent;
|
|
}
|
|
}
|
|
|
|
.pwa-popup-title {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
color: var(--color-text);
|
|
margin-bottom: 6px;
|
|
letter-spacing: -0.3px;
|
|
}
|
|
|
|
.pwa-popup-desc {
|
|
font-size: 14px;
|
|
color: var(--color-hint);
|
|
line-height: 1.45;
|
|
margin-bottom: 20px;
|
|
max-width: 320px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}
|
|
|
|
.pwa-popup-tabs {
|
|
display: flex;
|
|
background: var(--color-inset);
|
|
border-radius: 10px;
|
|
padding: 3px;
|
|
margin-bottom: 16px;
|
|
gap: 2px;
|
|
}
|
|
|
|
.pwa-popup-tab {
|
|
flex: 1;
|
|
padding: 7px 8px;
|
|
border: none;
|
|
background: transparent;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--color-hint);
|
|
cursor: pointer;
|
|
transition: all var(--dur-2) var(--ease-out);
|
|
}
|
|
.pwa-popup-tab.active {
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.pwa-popup-panel {
|
|
display: none;
|
|
}
|
|
.pwa-popup-panel.active {
|
|
display: block;
|
|
animation: pwa-fade-in 0.2s ease;
|
|
}
|
|
|
|
@keyframes pwa-fade-in {
|
|
from { opacity: 0; transform: translateY(4px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.pwa-popup-steps {
|
|
list-style: none;
|
|
text-align: left;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.pwa-popup-steps li {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 12px 14px;
|
|
border-radius: 12px;
|
|
background: var(--color-inset);
|
|
margin-bottom: 8px;
|
|
font-size: 14px;
|
|
color: var(--color-text);
|
|
line-height: 1.4;
|
|
}
|
|
.pwa-popup-steps li:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.pwa-step-icon {
|
|
flex-shrink: 0;
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 8px;
|
|
background: var(--color-accent);
|
|
}
|
|
.pwa-step-icon svg {
|
|
fill: #fff;
|
|
}
|
|
|
|
.pwa-popup-btn {
|
|
display: block;
|
|
width: 100%;
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
border: none;
|
|
border-radius: 14px;
|
|
background: var(--color-accent);
|
|
color: #fff;
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: opacity var(--dur-1) var(--ease-out);
|
|
letter-spacing: -0.2px;
|
|
}
|
|
.pwa-popup-btn:active {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* === Pull-to-Refresh === */
|
|
#ptr-container {
|
|
position: fixed;
|
|
top: env(safe-area-inset-top, 0px);
|
|
left: 0;
|
|
right: 0;
|
|
display: flex;
|
|
justify-content: center;
|
|
pointer-events: none;
|
|
z-index: 90;
|
|
padding-top: 12px;
|
|
}
|
|
|
|
.native-spinner {
|
|
width: 20px;
|
|
height: 20px;
|
|
opacity: 0;
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cg fill='%23000'%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.12'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.25' transform='rotate(45 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.37' transform='rotate(90 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.50' transform='rotate(135 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.62' transform='rotate(180 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.75' transform='rotate(225 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.87' transform='rotate(270 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='1' transform='rotate(315 10 10)'/%3E%3C/g%3E%3C/svg%3E");
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cg fill='%23000'%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.12'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.25' transform='rotate(45 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.37' transform='rotate(90 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.50' transform='rotate(135 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.62' transform='rotate(180 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.75' transform='rotate(225 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='.87' transform='rotate(270 10 10)'/%3E%3Crect x='9' y='1' width='2' height='5' rx='1' opacity='1' transform='rotate(315 10 10)'/%3E%3C/g%3E%3C/svg%3E");
|
|
-webkit-mask-size: contain;
|
|
mask-size: contain;
|
|
background: var(--color-text);
|
|
animation: native-spin 0.8s steps(8) infinite;
|
|
transition: opacity var(--dur-3) var(--ease-soft);
|
|
}
|
|
|
|
@keyframes native-spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Shift app content during pull */
|
|
#app.ptr-pulling {
|
|
transition: none;
|
|
}
|
|
|
|
#app.ptr-snapping {
|
|
transition: transform var(--dur-5) var(--ease-spring);
|
|
}
|
|
|
|
#app.ptr-refreshing {
|
|
transition: transform var(--dur-4) var(--ease-soft);
|
|
}
|
|
|
|
/* === About Section === */
|
|
.about-card {
|
|
padding: 0 !important;
|
|
overflow: hidden;
|
|
}
|
|
.about-app-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 16px;
|
|
}
|
|
.about-app-icon {
|
|
border-radius: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
.about-app-name {
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
color: var(--color-text);
|
|
line-height: 1.2;
|
|
}
|
|
.about-app-version {
|
|
font-size: 13px;
|
|
color: var(--color-hint);
|
|
margin-top: 2px;
|
|
}
|
|
.about-links-card {
|
|
margin-top: 0;
|
|
}
|
|
.about-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 13px 16px;
|
|
width: 100%;
|
|
text-decoration: none;
|
|
color: var(--color-text);
|
|
background: none;
|
|
border: none;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
box-sizing: border-box;
|
|
-webkit-tap-highlight-color: transparent;
|
|
transition: background var(--dur-1) var(--ease-out);
|
|
}
|
|
.about-row:active {
|
|
background: var(--color-inset);
|
|
}
|
|
.about-row-icon {
|
|
color: var(--color-hint);
|
|
flex-shrink: 0;
|
|
}
|
|
.about-row-label {
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
flex: 1;
|
|
}
|
|
.about-row-chevron {
|
|
opacity: 0.35;
|
|
flex-shrink: 0;
|
|
transition: transform var(--dur-3) var(--ease-out);
|
|
}
|
|
.about-row-divider {
|
|
height: 1px;
|
|
background: var(--color-border);
|
|
margin-left: 48px;
|
|
}
|
|
.about-contributors-chevron.open {
|
|
transform: rotate(90deg);
|
|
}
|
|
.about-contributors-list {
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
transition: max-height var(--dur-3) var(--ease-out);
|
|
}
|
|
.about-contributors-list.open {
|
|
max-height: 300px;
|
|
}
|
|
.about-contributor {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 16px 10px 48px;
|
|
text-decoration: none;
|
|
color: var(--color-text);
|
|
-webkit-tap-highlight-color: transparent;
|
|
transition: background var(--dur-1) var(--ease-out);
|
|
}
|
|
.about-contributor:active {
|
|
background: var(--color-inset);
|
|
}
|
|
.about-contributor-avatar {
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.about-contributor-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.about-contributor-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-text);
|
|
line-height: 1.3;
|
|
}
|
|
.about-contributor-role {
|
|
font-size: 12px;
|
|
color: var(--color-hint);
|
|
line-height: 1.3;
|
|
}
|
|
.about-footer {
|
|
text-align: center;
|
|
padding: 8px 0 24px;
|
|
font-size: 11px;
|
|
color: var(--color-hint);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* === Interaction feedback ===
|
|
A sweep that gives every remaining interactive surface the same press
|
|
response. Consistency is the whole point: when one button dips on touch
|
|
and the one beside it doesn't, the app feels unfinished no matter how
|
|
polished either control is on its own.
|
|
|
|
Rows and cards use the subtle scale — they're wide, and a hard squash on
|
|
a full-width element reads as a rendering glitch rather than a press. */
|
|
.seg-option,
|
|
.settings-select,
|
|
.location-picker,
|
|
.alert-test-btn,
|
|
.request-location-btn,
|
|
.shortcuts-reset-btn,
|
|
.shortcut-key-btn,
|
|
.about-app-icon,
|
|
.about-heart,
|
|
.toggle-switch,
|
|
.card-row,
|
|
.stat-card,
|
|
.best-time-card,
|
|
.stats-tile,
|
|
.stats-fact-card,
|
|
.hl-row {
|
|
transition: transform var(--dur-0) var(--ease-out-fast),
|
|
background var(--dur-2) var(--ease-soft),
|
|
border-color var(--dur-2) var(--ease-soft),
|
|
box-shadow var(--dur-2) var(--ease-soft),
|
|
color var(--dur-2) var(--ease-soft),
|
|
opacity var(--dur-2) var(--ease-soft);
|
|
}
|
|
|
|
.seg-option:active,
|
|
.alert-test-btn:active,
|
|
.request-location-btn:active,
|
|
.shortcuts-reset-btn:active,
|
|
.shortcut-key-btn:active,
|
|
.about-app-icon:active,
|
|
.about-heart:active {
|
|
transform: scale(var(--press-scale));
|
|
}
|
|
|
|
.card-row:active,
|
|
.stats-tile:not(.is-empty):active,
|
|
.hl-row:active {
|
|
transform: scale(var(--press-scale-subtle));
|
|
}
|
|
|
|
/* Baseline for any control that has no transition of its own. Zero-specificity
|
|
so every rule above — and every component rule earlier in this file — still
|
|
wins; this only fills the gaps. */
|
|
:where(button, a, [role="button"], input, select, textarea) {
|
|
transition-property: transform, background, border-color, box-shadow, color, opacity;
|
|
transition-duration: var(--dur-2);
|
|
transition-timing-function: var(--ease-soft);
|
|
}
|
|
|
|
/* --- Pointer-device polish ---
|
|
Lifts and shadow growth only where there's a real cursor, and only on
|
|
things that actually do something when clicked — a card that rises under
|
|
the pointer but isn't clickable is a promise the UI doesn't keep. On touch
|
|
these would either never fire or, worse, stick after a tap. */
|
|
@media (hover: hover) and (pointer: fine) {
|
|
.card-row:hover,
|
|
.history-extreme-card:not(.is-empty):hover,
|
|
.stats-tile:not(.is-empty):hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-2);
|
|
}
|
|
|
|
.chip:hover:not(.active) {
|
|
background: color-mix(in srgb, var(--color-text) 6%, var(--color-bg-secondary));
|
|
}
|
|
|
|
.chip.active:hover {
|
|
box-shadow: 0 4px 14px color-mix(in srgb, var(--color-accent) 32%, transparent);
|
|
}
|
|
|
|
.tab-item:hover:not(.active) {
|
|
color: var(--color-text-2);
|
|
}
|
|
|
|
.tab-item:hover::before {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
background: color-mix(in srgb, var(--color-text) 6%, transparent);
|
|
}
|
|
|
|
.tab-item.active:hover::before {
|
|
background: color-mix(in srgb, var(--color-accent) 16%, transparent);
|
|
}
|
|
|
|
.about-app-icon:hover {
|
|
transform: translateY(-2px) scale(1.03);
|
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18);
|
|
}
|
|
|
|
.about-heart:hover {
|
|
transform: scale(1.18);
|
|
}
|
|
}
|
|
|
|
/* === Desktop Station Panel (transparent wrapper on mobile) === */
|
|
.desktop-station-panel {
|
|
display: contents;
|
|
}
|
|
|
|
/* === Desktop Layout === */
|
|
@media (min-width: 900px) {
|
|
|
|
/* --- Desktop-specific custom properties --- */
|
|
:root {
|
|
--sidebar-w: 88px;
|
|
--panel-w: 360px;
|
|
--sheet-w: 440px;
|
|
--content-max: 920px;
|
|
--content-max-narrow: 660px;
|
|
/* Motion comes from the global ease/duration tokens; the desktop layer
|
|
only adds its own elevation scale. */
|
|
--dt-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
--dt-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.04);
|
|
--dt-shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.12), 0 4px 12px rgba(0, 0, 0, 0.06);
|
|
--dt-shadow-sheet: 0 16px 64px rgba(0, 0, 0, 0.14), 0 6px 20px rgba(0, 0, 0, 0.08);
|
|
--dt-blur: 20px;
|
|
}
|
|
|
|
html[data-theme="dark"] {
|
|
--dt-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
--dt-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25);
|
|
--dt-shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.35);
|
|
--dt-shadow-sheet: 0 16px 64px rgba(0, 0, 0, 0.45);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--dt-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
--dt-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.25);
|
|
--dt-shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.35);
|
|
--dt-shadow-sheet: 0 16px 64px rgba(0, 0, 0, 0.45);
|
|
}
|
|
}
|
|
|
|
/* --- Sidebar Navigation --- */
|
|
.tab-bar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
right: auto;
|
|
width: var(--sidebar-w);
|
|
height: 100%;
|
|
flex-direction: column;
|
|
justify-content: flex-start;
|
|
padding: 16px 8px;
|
|
border-top: none;
|
|
border-right: 1px solid var(--color-separator);
|
|
gap: 2px;
|
|
z-index: 100;
|
|
background: var(--color-bg-secondary);
|
|
}
|
|
|
|
/* Desktop turns the tab bar into a left sidebar. Only the geometry changes
|
|
here — selection is still drawn by the shared ::before pill, so this must
|
|
not set `background` of its own or the two tints would stack. */
|
|
.tab-item {
|
|
padding: 10px 0;
|
|
width: 100%;
|
|
gap: 3px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.tab-item span {
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
.tab-item:active {
|
|
transform: scale(0.97);
|
|
}
|
|
|
|
/* --- App container shifts right --- */
|
|
#app {
|
|
left: var(--sidebar-w);
|
|
bottom: 0;
|
|
}
|
|
|
|
/* --- Tab view transitions (desktop-refined) --- */
|
|
.tab-view {
|
|
transition: opacity var(--dur-5) var(--ease-out),
|
|
transform var(--dur-5) var(--ease-out);
|
|
transform: scale(0.97) translateY(8px);
|
|
}
|
|
|
|
.tab-view.active {
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
|
|
/* --- Non-map views: centered content with padding --- */
|
|
#view-history,
|
|
#view-stats,
|
|
#view-settings {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 0 var(--sp-6) var(--sp-7);
|
|
}
|
|
|
|
/* On desktop the view already provides side padding, so sections shed
|
|
their inner 16 px gutter and let the centred max-width column do the
|
|
work. Cards/headers align flush to that column. */
|
|
#view-history > .section,
|
|
#view-stats > .section,
|
|
#view-settings > .section {
|
|
padding-left: 0;
|
|
padding-right: 0;
|
|
}
|
|
|
|
/* Stat-row's own 16 px gutter is now redundant for the same reason. */
|
|
#view-history .stat-row,
|
|
#view-stats .stat-row {
|
|
padding-left: 0;
|
|
padding-right: 0;
|
|
}
|
|
|
|
/* Page header sits flush with the content column and gets a more
|
|
generous vertical rhythm on desktop. */
|
|
.page-header {
|
|
padding: var(--sp-7) 0 var(--sp-5);
|
|
}
|
|
.page-title {
|
|
font-size: 32px;
|
|
}
|
|
.page-description {
|
|
font-size: 15px;
|
|
margin-top: var(--sp-2);
|
|
}
|
|
|
|
#view-history.active,
|
|
#view-stats.active,
|
|
#view-settings.active {
|
|
display: flex;
|
|
}
|
|
|
|
#view-history > *,
|
|
#view-stats > *,
|
|
#view-settings > * {
|
|
width: 100%;
|
|
max-width: var(--content-max);
|
|
}
|
|
|
|
/* Bigger chart on desktop — the extra horizontal space deserves a
|
|
taller graph instead of a 280px squish. Step it up at the tablet
|
|
breakpoint so 13" laptops don't get a 400px chart that dominates
|
|
the viewport. */
|
|
#view-history .chart-container,
|
|
#view-stats .chart-container {
|
|
height: 340px;
|
|
}
|
|
@media (min-width: 1200px) {
|
|
#view-history .chart-container,
|
|
#view-stats .chart-container {
|
|
height: 400px;
|
|
}
|
|
}
|
|
|
|
/* Pair the two summary cards side-by-side when there's room. The
|
|
section-header above spans both columns. */
|
|
#history-summary {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
column-gap: var(--sp-3);
|
|
row-gap: var(--sp-2);
|
|
align-items: start;
|
|
}
|
|
/* Any non-extreme-card child (section-header, empty-state message) spans
|
|
the full width of the summary grid. */
|
|
#history-summary > *:not(.extreme-card) {
|
|
grid-column: 1 / -1;
|
|
}
|
|
#history-summary > .extreme-card {
|
|
padding: var(--sp-5) var(--sp-5);
|
|
}
|
|
#history-summary > .extreme-card .extreme-value {
|
|
font-size: 32px;
|
|
}
|
|
|
|
/* Desktop card refinements — border + a hint of shadow. The border does
|
|
the visual heavy lifting (shadcn-style), shadow just adds depth on hover. */
|
|
.card {
|
|
border-radius: var(--r-lg);
|
|
padding: var(--sp-4) var(--sp-5);
|
|
transition: box-shadow var(--dur-3) var(--ease-out),
|
|
border-color var(--dur-3) var(--ease-out);
|
|
}
|
|
|
|
.card:hover {
|
|
box-shadow: var(--shadow-1);
|
|
}
|
|
|
|
.card-list {
|
|
border-radius: var(--r-lg);
|
|
transition: box-shadow var(--dur-3) var(--ease-out);
|
|
}
|
|
|
|
.card-list:hover {
|
|
box-shadow: var(--shadow-1);
|
|
}
|
|
|
|
.metric-card,
|
|
.best-time-card,
|
|
.extreme-card,
|
|
.stats-hero,
|
|
.stats-hero-card,
|
|
.stats-fact-card {
|
|
border-radius: var(--r-lg);
|
|
transition: box-shadow var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
|
|
}
|
|
.metric-card { padding: var(--sp-4) var(--sp-5); }
|
|
.best-time-card { padding: var(--sp-5) var(--sp-5); }
|
|
.extreme-card { padding: var(--sp-5) var(--sp-5); }
|
|
.stats-hero { padding: var(--sp-6) var(--sp-6); }
|
|
.stats-hero-card { padding: var(--sp-6); }
|
|
.stats-fact-card { padding: var(--sp-4); gap: var(--sp-3); }
|
|
.stats-fact-card .stats-fact-icon { width: 38px; height: 38px; border-radius: 11px; }
|
|
.stats-fact-card .stats-fact-icon svg { width: 18px; height: 18px; }
|
|
.stats-fact-value { font-size: 18px; }
|
|
.stats-hero-value { font-size: 44px; }
|
|
.stats-facts-row { gap: var(--sp-3); }
|
|
#stats-content > .metric-row {
|
|
margin-left: 0;
|
|
margin-right: 0;
|
|
}
|
|
#stats-content > .section {
|
|
padding-left: 0;
|
|
padding-right: 0;
|
|
}
|
|
#stats-content > .stats-overview-row {
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
}
|
|
#stats-content .metric-card,
|
|
#stats-content .best-time-card {
|
|
min-height: 88px;
|
|
padding: var(--sp-4) var(--sp-5);
|
|
}
|
|
|
|
.metric-card:hover,
|
|
.best-time-card:hover,
|
|
.extreme-card:hover,
|
|
.stats-hero:hover,
|
|
.stats-fact-card:hover {
|
|
box-shadow: var(--shadow-1);
|
|
}
|
|
.stats-hero-card:hover {
|
|
box-shadow: var(--dt-shadow-md);
|
|
}
|
|
|
|
/* Desktop: roomier tiles & bar chart */
|
|
.stats-tile-grid { gap: 8px; }
|
|
.stats-tile {
|
|
min-height: 80px;
|
|
padding: 14px 4px 12px;
|
|
border-radius: 14px;
|
|
transition: transform var(--dur-1) var(--ease-out), box-shadow var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
|
|
}
|
|
.stats-tile:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: var(--shadow-1);
|
|
}
|
|
.stats-tile-name { font-size: 11px; }
|
|
.stats-tile-value { font-size: 16px; }
|
|
.stats-tile-bar { height: 4px; }
|
|
|
|
.stats-hour-chart-wrap { height: 210px; }
|
|
|
|
/* History tab: roomier hero card, bigger headline value, and lift
|
|
the extreme cards on hover so they read as obviously clickable. */
|
|
.history-hero-card {
|
|
padding: var(--sp-6);
|
|
border-radius: var(--r-lg);
|
|
transition: box-shadow var(--dur-3) var(--ease-out);
|
|
}
|
|
.history-hero-card:hover {
|
|
box-shadow: var(--dt-shadow-md);
|
|
}
|
|
.history-hero-value { font-size: 44px; }
|
|
.history-hero-pills { gap: 10px; margin-top: 18px; }
|
|
|
|
.history-extreme-row { gap: var(--sp-3); }
|
|
.history-extreme-card {
|
|
border-radius: var(--r-lg);
|
|
padding: 14px 16px 14px 20px;
|
|
transition: transform var(--dur-1) var(--ease-out),
|
|
box-shadow var(--dur-2) var(--ease-out),
|
|
border-color var(--dur-2) var(--ease-out);
|
|
}
|
|
.history-extreme-card:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 18px color-mix(in srgb, var(--accent) 16%, transparent);
|
|
}
|
|
.history-extreme-icon {
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 11px;
|
|
}
|
|
.history-extreme-value { font-size: 22px; }
|
|
|
|
.metric-value { font-size: 28px; }
|
|
.stats-hero-value { font-size: 48px; }
|
|
|
|
/* Desktop chip refinements */
|
|
.chip {
|
|
transition: background var(--dur-2) var(--ease-out),
|
|
color var(--dur-2) var(--ease-out),
|
|
transform var(--dur-1) var(--ease-out),
|
|
box-shadow var(--dur-2) var(--ease-out);
|
|
}
|
|
|
|
.chip:hover:not(.active) {
|
|
background: var(--color-separator);
|
|
}
|
|
|
|
.chip.active {
|
|
box-shadow: none;
|
|
}
|
|
|
|
/* Desktop chart container — matches card radius so the chart reads as a
|
|
hero card in the same family. Border carries the surface; shadow only
|
|
appears on hover for affordance. */
|
|
.chart-container {
|
|
border-radius: var(--r-lg);
|
|
padding: var(--sp-5) var(--sp-3) var(--sp-3);
|
|
transition: box-shadow var(--dur-3) var(--ease-out);
|
|
}
|
|
|
|
.chart-container:hover {
|
|
box-shadow: var(--shadow-1);
|
|
}
|
|
|
|
/* Stats card hover */
|
|
.stat-card {
|
|
border-radius: var(--r-lg);
|
|
padding: var(--sp-4) var(--sp-3);
|
|
transition: box-shadow var(--dur-3) var(--ease-out);
|
|
}
|
|
|
|
.stat-card:hover {
|
|
box-shadow: var(--shadow-1);
|
|
}
|
|
|
|
/* Stat row gets a touch more breathing room between cards on desktop. */
|
|
.stat-row {
|
|
gap: var(--sp-3);
|
|
}
|
|
|
|
/* Section header reads a little louder on desktop so the hierarchy
|
|
between header → cards is unmistakable. */
|
|
.section-header {
|
|
font-size: 12px;
|
|
padding: var(--sp-6) var(--sp-1) var(--sp-2);
|
|
letter-spacing: 0.7px;
|
|
}
|
|
.section-header:first-child,
|
|
.section > .section-header:first-child {
|
|
padding-top: var(--sp-3);
|
|
}
|
|
|
|
/* Section header sync-badge sits slightly higher to align with the
|
|
bolder header text. */
|
|
.section-header .sync-badge {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* Ranking item hover */
|
|
.ranking-item {
|
|
transition: background var(--dur-2) var(--ease-out),
|
|
transform var(--dur-1) var(--ease-out);
|
|
border-radius: 0;
|
|
}
|
|
|
|
.ranking-item:hover {
|
|
background: var(--color-inset);
|
|
}
|
|
|
|
.ranking-item:active {
|
|
transform: scale(0.99);
|
|
}
|
|
|
|
/* Settings row hover */
|
|
.settings-row {
|
|
border-radius: 0;
|
|
transition: background var(--dur-2) var(--ease-out);
|
|
}
|
|
|
|
.settings-row:hover {
|
|
background: color-mix(in srgb, var(--color-text) 5%, transparent);
|
|
}
|
|
|
|
/* --- Map view: full width with side panel (panel on right) --- */
|
|
#view-map {
|
|
display: flex;
|
|
flex-direction: row;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#view-map.active {
|
|
display: flex;
|
|
}
|
|
|
|
/* Remove the dark bar above the map on desktop */
|
|
#map-container::before {
|
|
display: none;
|
|
}
|
|
|
|
#map-container {
|
|
flex: 1;
|
|
height: 100%;
|
|
min-height: unset;
|
|
}
|
|
|
|
/* Map search bar refinement */
|
|
#map-search-wrapper {
|
|
top: 16px;
|
|
left: 16px;
|
|
max-width: 400px;
|
|
}
|
|
|
|
/* Desktop: the "Hier suchen" pill sits inline to the right of the search
|
|
box, same as mobile. Widen the wrapper only while the pill is visible so
|
|
the search input keeps its width. */
|
|
.map-search-here-row {
|
|
position: static;
|
|
transform: none;
|
|
width: auto;
|
|
margin-top: 0;
|
|
}
|
|
#map-search-wrapper:has(.map-search-here:not(.hidden)) {
|
|
max-width: 520px;
|
|
}
|
|
|
|
#map-search-box {
|
|
border-radius: 12px;
|
|
box-shadow: var(--dt-shadow-md);
|
|
background: var(--color-bg);
|
|
transition: box-shadow var(--dur-3) var(--ease-out);
|
|
height: 46px;
|
|
}
|
|
|
|
#map-search-box:focus-within {
|
|
box-shadow: var(--dt-shadow-lg), 0 0 0 3px rgba(0, 122, 255, 0.2);
|
|
}
|
|
|
|
#map-search-results {
|
|
border-radius: 12px;
|
|
box-shadow: var(--dt-shadow-lg);
|
|
background: var(--color-bg);
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.map-search-result-item {
|
|
transition: background var(--dur-1) var(--ease-soft);
|
|
}
|
|
|
|
.map-search-result-item:hover {
|
|
background: var(--color-inset);
|
|
}
|
|
|
|
/* Map FAB refinement */
|
|
.map-fab {
|
|
border-radius: 12px;
|
|
box-shadow: var(--dt-shadow-md);
|
|
background: var(--color-bg);
|
|
transition: box-shadow var(--dur-3) var(--ease-out),
|
|
transform var(--dur-2) var(--ease-out);
|
|
width: 46px;
|
|
height: 46px;
|
|
}
|
|
|
|
.map-fab:hover {
|
|
box-shadow: var(--dt-shadow-lg);
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.map-fab:active {
|
|
transform: scale(0.94);
|
|
opacity: 1;
|
|
}
|
|
|
|
/* --- Station panel (right sidebar on map) --- */
|
|
.desktop-station-panel {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: var(--panel-w);
|
|
flex-shrink: 0;
|
|
height: 100%;
|
|
overflow-y: auto;
|
|
border-left: 1px solid var(--color-separator);
|
|
background: var(--color-bg);
|
|
}
|
|
|
|
.desktop-station-panel .station-sort-bar {
|
|
padding: 16px 16px 8px;
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--color-bg);
|
|
z-index: 2;
|
|
backdrop-filter: blur(var(--dt-blur));
|
|
-webkit-backdrop-filter: blur(var(--dt-blur));
|
|
}
|
|
|
|
.desktop-station-panel .station-list {
|
|
margin: 0;
|
|
border-radius: 0;
|
|
flex: 1;
|
|
background: transparent;
|
|
overflow: visible;
|
|
}
|
|
|
|
/* Station list item hover on desktop */
|
|
.station-item {
|
|
transition: background var(--dur-2) var(--ease-out),
|
|
transform var(--dur-1) var(--ease-out);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.station-item:hover {
|
|
background: var(--color-inset);
|
|
}
|
|
|
|
.station-item:active {
|
|
transform: scale(0.99);
|
|
}
|
|
|
|
/* Station item stagger animation */
|
|
.station-item.anim-in {
|
|
animation: dt-station-slide 0.35s var(--ease-out) both;
|
|
}
|
|
|
|
@keyframes dt-station-slide {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(16px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
/* --- Bottom sheet becomes right side panel --- */
|
|
.bottom-sheet {
|
|
left: var(--sidebar-w);
|
|
}
|
|
|
|
.bottom-sheet-backdrop {
|
|
background: rgba(0, 0, 0, 0.25);
|
|
transition: opacity var(--dur-4) var(--ease-soft);
|
|
}
|
|
|
|
.bottom-sheet-content {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: auto;
|
|
width: var(--sheet-w);
|
|
max-width: var(--sheet-w);
|
|
max-height: 100%;
|
|
border-radius: 0;
|
|
border-left: 1px solid var(--color-separator);
|
|
box-shadow: var(--dt-shadow-sheet);
|
|
transform: translateX(100%);
|
|
/* Same emphasis curve as the mobile sheet so the panel reads as the same
|
|
surface arriving from a different edge. */
|
|
transition: transform var(--dur-5) var(--ease-emphasis);
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.bottom-sheet:not(.hidden) .bottom-sheet-content {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.bottom-sheet-content.expanded {
|
|
max-height: 100%;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.bottom-sheet-handle-area {
|
|
display: none;
|
|
}
|
|
|
|
/* Sheet station header spacing for desktop (space for close button only) */
|
|
.sheet-station-header {
|
|
padding: 20px 20px 8px;
|
|
padding-right: 48px;
|
|
}
|
|
|
|
.sheet-station-name {
|
|
font-size: 20px;
|
|
}
|
|
|
|
.sheet-station-price {
|
|
font-size: 40px;
|
|
padding: 12px 20px;
|
|
}
|
|
|
|
/* Sheet chart container */
|
|
.sheet-chart-container {
|
|
height: 160px;
|
|
border-radius: 14px;
|
|
box-shadow: var(--dt-shadow-sm);
|
|
}
|
|
|
|
.bottom-sheet-content.expanded .sheet-chart-container {
|
|
height: 240px;
|
|
}
|
|
|
|
/* Sheet nav buttons */
|
|
.sheet-nav-btn {
|
|
border-radius: 12px;
|
|
transition: background var(--dur-1) var(--ease-soft);
|
|
}
|
|
|
|
.sheet-nav-btn:hover {
|
|
background: var(--color-separator);
|
|
}
|
|
|
|
.sheet-nav-btn:active {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* --- Desktop close button (Apple xmark.circle.fill style) --- */
|
|
.sheet-desktop-close {
|
|
position: absolute;
|
|
top: 14px;
|
|
right: 14px;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: var(--color-inset);
|
|
color: var(--color-hint);
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10;
|
|
transition: background var(--dur-2) var(--ease-soft),
|
|
color var(--dur-2) var(--ease-soft),
|
|
opacity var(--dur-2) var(--ease-soft);
|
|
opacity: 0.7;
|
|
line-height: 1;
|
|
}
|
|
|
|
.sheet-desktop-close:hover {
|
|
opacity: 1;
|
|
background: var(--color-separator);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.sheet-desktop-close:active {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* The desktop close button overlays the top-right corner — give the fav
|
|
star enough right-side breathing room so they don't visually collide. */
|
|
.sheet-fav-btn {
|
|
margin-right: 36px;
|
|
}
|
|
|
|
/* --- Hide expand button on desktop (close button handles dismiss) --- */
|
|
.sheet-expand-btn {
|
|
display: none !important;
|
|
}
|
|
|
|
/* --- Hide pull-to-refresh on desktop --- */
|
|
#ptr-container {
|
|
display: none;
|
|
}
|
|
|
|
/* --- PWA popup centers on desktop --- */
|
|
.pwa-popup {
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.pwa-popup-sheet {
|
|
border-radius: 20px;
|
|
box-shadow: var(--dt-shadow-lg);
|
|
max-width: 440px;
|
|
}
|
|
|
|
/* --- Toast positioning (account for sidebar) --- */
|
|
.toast {
|
|
/* No bottom tab bar on desktop — mobile's 74px offset would float
|
|
the toast in the middle of nowhere. */
|
|
bottom: 24px;
|
|
border-radius: 14px;
|
|
box-shadow: var(--dt-shadow-lg);
|
|
}
|
|
|
|
.toast.visible {
|
|
left: calc(var(--sidebar-w) + 50%);
|
|
transform: translateX(-50%) translateX(calc(var(--sidebar-w) / -2));
|
|
}
|
|
|
|
/* --- About card refinements --- */
|
|
.about-card {
|
|
border-radius: 14px;
|
|
box-shadow: var(--dt-shadow-sm);
|
|
transition: box-shadow var(--dur-3) var(--ease-out);
|
|
}
|
|
|
|
.about-card:hover {
|
|
box-shadow: var(--dt-shadow-md);
|
|
}
|
|
|
|
.about-row {
|
|
transition: background var(--dur-2) var(--ease-out);
|
|
}
|
|
|
|
.about-row:hover {
|
|
background: rgba(127, 127, 127, 0.06);
|
|
}
|
|
|
|
/* Auth hero card */
|
|
.auth-hero-card {
|
|
border-radius: 20px;
|
|
box-shadow: var(--dt-shadow-md);
|
|
transition: box-shadow var(--dur-3) var(--ease-out);
|
|
}
|
|
|
|
.auth-hero-card:hover {
|
|
box-shadow: var(--dt-shadow-lg);
|
|
}
|
|
|
|
/* Section header subtle refinement */
|
|
.section-header {
|
|
padding-top: 20px;
|
|
font-size: 12px;
|
|
letter-spacing: 0.8px;
|
|
}
|
|
|
|
/* Location picker dropdown */
|
|
.location-picker {
|
|
border-radius: 10px;
|
|
transition: border-color var(--dur-2) var(--ease-out),
|
|
box-shadow var(--dur-2) var(--ease-out);
|
|
}
|
|
|
|
.location-picker:focus {
|
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15);
|
|
}
|
|
|
|
/* Scrollbar styling for panels */
|
|
.desktop-station-panel::-webkit-scrollbar,
|
|
.bottom-sheet-content::-webkit-scrollbar,
|
|
#view-history::-webkit-scrollbar,
|
|
#view-stats::-webkit-scrollbar,
|
|
#view-settings::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.desktop-station-panel::-webkit-scrollbar-track,
|
|
.bottom-sheet-content::-webkit-scrollbar-track,
|
|
#view-history::-webkit-scrollbar-track,
|
|
#view-stats::-webkit-scrollbar-track,
|
|
#view-settings::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.desktop-station-panel::-webkit-scrollbar-thumb,
|
|
.bottom-sheet-content::-webkit-scrollbar-thumb,
|
|
#view-history::-webkit-scrollbar-thumb,
|
|
#view-stats::-webkit-scrollbar-thumb,
|
|
#view-settings::-webkit-scrollbar-thumb {
|
|
background: var(--color-separator);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.desktop-station-panel::-webkit-scrollbar-thumb:hover,
|
|
.bottom-sheet-content::-webkit-scrollbar-thumb:hover,
|
|
#view-history::-webkit-scrollbar-thumb:hover,
|
|
#view-stats::-webkit-scrollbar-thumb:hover,
|
|
#view-settings::-webkit-scrollbar-thumb:hover {
|
|
background: var(--color-hint);
|
|
}
|
|
|
|
/* Firefox scrollbar */
|
|
.desktop-station-panel,
|
|
.bottom-sheet-content,
|
|
#view-history,
|
|
#view-stats,
|
|
#view-settings {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--color-separator) transparent;
|
|
}
|
|
|
|
|
|
|
|
/* Toggle switch hover */
|
|
.toggle-switch {
|
|
transition: opacity var(--dur-2) var(--ease-soft);
|
|
}
|
|
|
|
.toggle-switch:hover {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
/* --- Button hover refinements (Apple-style: subtle opacity, no lifts) --- */
|
|
.card-action {
|
|
transition: opacity var(--dur-1) var(--ease-soft);
|
|
}
|
|
|
|
.card-action:hover {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.card-action:active {
|
|
opacity: 0.65;
|
|
}
|
|
|
|
.primary-btn {
|
|
transition: opacity var(--dur-1) var(--ease-soft);
|
|
}
|
|
|
|
.primary-btn:hover {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.secondary-btn {
|
|
transition: background var(--dur-2) var(--ease-soft);
|
|
}
|
|
|
|
.secondary-btn:hover {
|
|
background: var(--color-separator);
|
|
}
|
|
|
|
/* Sort button hover */
|
|
.station-sort-btn {
|
|
transition: background var(--dur-2) var(--ease-soft);
|
|
}
|
|
|
|
.station-sort-btn:hover {
|
|
background: var(--color-separator);
|
|
}
|
|
|
|
.station-sort-btn:active {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
/* Map location banner refinement */
|
|
#map-location-banner {
|
|
border-radius: 12px;
|
|
box-shadow: var(--dt-shadow-md);
|
|
backdrop-filter: blur(var(--dt-blur));
|
|
-webkit-backdrop-filter: blur(var(--dt-blur));
|
|
}
|
|
}
|
|
|
|
/* ── History locations (Standorte mit Verlaufsdaten) ───────────────── */
|
|
.hl-summary-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
width: 100%;
|
|
padding: 14px 16px;
|
|
background: transparent;
|
|
border: none;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent;
|
|
transition: background var(--dur-1) var(--ease-out);
|
|
}
|
|
.hl-summary-top:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
|
|
.hl-total-num {
|
|
font-size: 26px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
color: var(--color-text);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.hl-total-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.4px;
|
|
text-transform: uppercase;
|
|
color: var(--color-hint);
|
|
}
|
|
.hl-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.hl-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 4px 9px;
|
|
border-radius: 999px;
|
|
background: color-mix(in srgb, var(--color-text) 8%, transparent);
|
|
font-size: 12px;
|
|
color: var(--color-text);
|
|
}
|
|
.hl-chip b { font-weight: 700; }
|
|
.hl-chevron { transition: transform var(--dur-2) var(--ease-out); }
|
|
.hl-group-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 9px 16px;
|
|
border-top: 1px solid var(--color-separator);
|
|
background: color-mix(in srgb, var(--color-text) 4%, transparent);
|
|
}
|
|
.hl-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 16px;
|
|
border-top: 1px solid var(--color-separator);
|
|
transition: background var(--dur-1) var(--ease-out);
|
|
}
|
|
.hl-row:hover { background: color-mix(in srgb, var(--color-text) 3%, transparent); }
|
|
.hl-row-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--color-text);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.hl-row-sub { font-size: 11px; color: var(--color-hint); margin-top: 2px; }
|
|
.hl-pin-btn {
|
|
flex-shrink: 0;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
|
|
color: var(--color-accent);
|
|
cursor: pointer;
|
|
-webkit-tap-highlight-color: transparent;
|
|
transition: background var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out);
|
|
}
|
|
.hl-pin-btn:hover { background: color-mix(in srgb, var(--color-accent) 22%, transparent); transform: scale(1.06); }
|
|
.hl-pin-btn:active { transform: scale(0.94); }
|
|
|
|
/* "Standort anfragen" reads as the primary action of the locations section. */
|
|
.request-location-btn { color: var(--color-accent); font-weight: 600; }
|
|
.request-location-btn svg { color: var(--color-accent); }
|
|
|
|
/* ── Settings: grouped card (Allgemein) ────────────────────────────── */
|
|
.card.settings-group {
|
|
padding: var(--sp-1) 0 var(--sp-2);
|
|
}
|
|
.settings-group .settings-row {
|
|
padding: var(--sp-3) var(--sp-4) var(--sp-3);
|
|
}
|
|
.settings-group .settings-row-stacked {
|
|
padding-bottom: var(--sp-1);
|
|
}
|
|
.settings-group .seg-control {
|
|
margin: var(--sp-2) var(--sp-4) var(--sp-2);
|
|
}
|
|
.settings-divider {
|
|
height: 1px;
|
|
background: var(--color-separator);
|
|
margin: var(--sp-2) 0 var(--sp-1) var(--sp-4);
|
|
}
|
|
|
|
/* Settings list cards render their own rows — let rows own the padding */
|
|
#history-locations-card,
|
|
#user-requests-card {
|
|
padding: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Compact "n Standorte" summary row */
|
|
.hl-summary-top { gap: 12px; padding: 12px 16px; }
|
|
.hl-total-num {
|
|
font-size: 17px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.hl-total-label {
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
letter-spacing: 0;
|
|
text-transform: none;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
/* ── Light-mode card lift ──────────────────────────────────────────────
|
|
The page is near-white, so cards separate through their hairline
|
|
border plus this whisper of a shadow (invisible on dark's black page). */
|
|
.card,
|
|
.card-list,
|
|
.metric-card,
|
|
.extreme-card,
|
|
.stat-card,
|
|
.stats-hero,
|
|
.stats-hero-card,
|
|
.stats-fact-card,
|
|
.stats-tile,
|
|
.best-time-card,
|
|
.history-hero-card,
|
|
.history-extreme-card,
|
|
.account-hero,
|
|
.about-hero,
|
|
.chart-container,
|
|
.stats-hour-chart-wrap {
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 2px 6px rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
/* Chips sit directly on the page — on near-white they need their own
|
|
edge to read as controls. */
|
|
.chip {
|
|
border: 1px solid var(--color-separator);
|
|
}
|
|
.chip.active {
|
|
border-color: transparent;
|
|
}
|
|
|
|
/* Small "Min" prefix on the tooltip's big price — the plotted line is
|
|
the daily cheapest, while Ø/Max ride along in the meta row. */
|
|
.history-tooltip-price-mark {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
margin-right: 5px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
letter-spacing: 0.4px;
|
|
vertical-align: 2px;
|
|
}
|