Files
TREK/client/src/mobile/mobile.css
T
Maurice 782bb365b5 fix(maps): the phone map carries no credit and no night pause labels
The credit was a round (i) under the locate button, and on a full-bleed phone map a
lone control there is the first thing the eye lands on. It is off on the phone now:
one rule in mobile.css against both engines' containers, so Leaflet, MapLibre and
Mapbox cannot drift apart. The desktop map is untouched and still shows the full
credit with its links. Owner's call, made knowingly.

With the credit gone the band the round controls floated on had nothing under it to
clear, so the 38px credit row comes out of --bottom-nav-h and out of the camera
padding the offered roads are framed with. The controls now stand on the map floor.

The night pause pills go with it. On the desktop one is a drag handle with a hint;
a phone has neither hover to explain it nor room to drag, and the markers come from
the whole drive rather than the stage on screen, so day 1 drew day 2's pill as well.
Both via lists go through the same filter, because the plan tab carries them too.
2026-09-17 12:43:45 +02:00

393 lines
19 KiB
CSS

/* ── Mobile rewrite design tokens ─────────────────────────────────────────────
* Scoped to .m-root (the MobileShell wrapper) so the mobile design system
* never leaks into desktop chrome. Values are the mobile design ground truth;
* dark values follow the app-wide `.dark` class strategy.
*/
.m-root {
--m-ink: #101013;
--m-muted: #68686F;
--m-faint: #9A9AA1;
--m-bg: #F3F2EF;
--m-scr: radial-gradient(120% 80% at 50% -10%, #FBFAF7 0%, #F1EFEA 55%, #E9E7E1 100%);
--m-glass: rgba(255,255,255,.6);
--m-gbr: rgba(255,255,255,.75);
--m-card: rgba(255,255,255,.55);
--m-cbr: rgba(255,255,255,.75);
--m-inner: rgba(255,255,255,.68);
--m-inbr: rgba(255,255,255,.9);
--m-act: #101013;
--m-actfg: #fff;
--m-dim: rgba(16,16,19,.4);
--m-sheet: rgba(250,250,248,.94);
--m-sheetop: #FAFAF8;
--m-shbr: rgba(255,255,255,.95);
--m-ic: rgba(16,16,19,.04);
--m-avbr: rgba(255,255,255,.9);
--m-rowbr: rgba(16,16,19,.045);
--m-conn: #9A9AA1;
--m-mapb: #E8E6E1;
--m-mapr: #FBFAF8;
--m-mapw: #DFE2E4;
--m-mkr: #101013;
--m-mkfg: #fff;
--m-trackoff: rgba(16,16,19,.14);
/* Strength of a dayTintProvider plugin's colour on an inactive day chip. A notch
* above the desktop badge: the chip has no neutral base under it, so a softer tint
* reads as a rendering artifact rather than a deliberate colour. */
--day-tint-chip: 20%;
/* Toggle knob is white on both themes (off-track is translucent enough). */
--m-knob: #fff;
/* Status color canon — theme independent. */
--m-st-confirmed: #2FA37A;
--m-st-pending: #E8A13A;
--m-st-info: #4A7DDB;
--m-st-danger: #D6273B;
--m-st-neutral: #9A9AA1;
/* Chrome metrics. --bottom-nav-h stays the single source of truth for
* content clearance (same value the legacy shell uses). The demo's fixed
* 52px/26px offsets stand in for the phone status bar / home indicator, so
* real devices derive them from the safe-area insets. */
--bottom-nav-h: calc(84px + env(safe-area-inset-bottom, 0px));
--m-safe-top: calc(env(safe-area-inset-top, 0px) + 12px);
}
.dark .m-root {
--m-ink: #F5F5F7;
--m-muted: #9C9CA3;
--m-faint: #6E6E76;
--m-bg: #0A0A0C;
--m-scr: radial-gradient(120% 80% at 50% -10%, #17171C 0%, #0C0C0F 55%, #08080A 100%);
--m-glass: rgba(26,26,31,.6);
--m-gbr: rgba(255,255,255,.12);
--m-card: rgba(24,24,29,.55);
--m-cbr: rgba(255,255,255,.1);
--m-inner: rgba(255,255,255,.07);
--m-inbr: rgba(255,255,255,.1);
--m-act: #F5F5F7;
--m-actfg: #101013;
--m-dim: rgba(0,0,0,.55);
--m-sheet: rgba(22,22,27,.94);
--m-sheetop: #191920;
--m-shbr: rgba(255,255,255,.14);
--m-ic: rgba(255,255,255,.09);
--m-avbr: rgba(255,255,255,.14);
--m-rowbr: rgba(255,255,255,.07);
--m-conn: #6E6E76;
--m-mapb: #121215;
--m-mapr: #1E1F24;
--m-mapw: #0D1013;
--m-mkr: #F5F5F7;
--m-mkfg: #101013;
--m-trackoff: rgba(255,255,255,.14);
--day-tint-chip: 28%;
}
/* Geist "subtext" tier weight — single knob. The design's Geist labels/numbers
* read heavy at their declared bold; render the whole tier at Medium (500) for
* the fine, low-key look. Higher specificity than Tailwind's font-* utilities,
* so it caps font-medium / font-semibold / font-bold / font-extrabold on any
* .font-geist span. Retune here if a weight feels too light or heavy. */
.m-root .font-geist { font-weight: 500; }
/* ── Keyframes ──────────────────────────────────────────────────────────── */
@keyframes m-sheet-in {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes m-sheet-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(24px); }
}
@keyframes m-drawer-in {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
@keyframes m-drawer-out {
from { transform: translateX(0); }
to { transform: translateX(-100%); }
}
@keyframes m-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes m-fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes m-pop-in {
from { opacity: 0; transform: translateY(-6px) scale(.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes m-toast-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes m-toast-out {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(6px); }
}
.m-sheet-in { animation: m-sheet-in 280ms var(--ease-drawer) both; }
.m-sheet-out { animation: m-sheet-out 280ms var(--ease-drawer) both; }
.m-drawer-in { animation: m-drawer-in 280ms var(--ease-drawer) both; }
.m-drawer-out { animation: m-drawer-out 280ms var(--ease-drawer) both; }
.m-fade-in { animation: m-fade-in 280ms var(--ease-drawer) both; }
.m-fade-out { animation: m-fade-out 280ms var(--ease-drawer) both; }
.m-pop-in { animation: m-pop-in 220ms var(--ease-out-quint) both; }
.m-toast-in { animation: m-toast-in 260ms var(--ease-out-quint) both; }
.m-toast-out { animation: m-toast-out 220ms ease both; }
/* Reduced motion: gentler, not zero (same rule the desktop keyframes follow). */
@media (prefers-reduced-motion: reduce) {
.m-sheet-in, .m-drawer-in, .m-pop-in, .m-toast-in {
animation: m-fade-in 120ms ease-out both;
}
.m-sheet-out, .m-drawer-out, .m-toast-out {
animation: m-fade-out 120ms ease-out both;
}
}
/* ── TREK mascot (empty-state) — logo-as-body character, per-scene props ───── */
@keyframes trek-idle { 0%, 100% { transform: translateY(0) rotate(-1.1deg); } 50% { transform: translateY(-2px) rotate(1.1deg); } }
@keyframes trek-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.012, 1.024); } }
@keyframes trek-shadow { 0%, 100% { transform: scaleX(1); opacity: .15; } 50% { transform: scaleX(1.05); opacity: .17; } }
/* body hop, occasional: long rest, then anticipation squash → airborne → land → settle */
@keyframes trek-hop {
0%, 74%, 100% { transform: translateY(0) scale(1, 1); }
79% { transform: translateY(1px) scale(1.05, .92); }
87% { transform: translateY(-10px) scale(.95, 1.07); }
94% { transform: translateY(0) scale(1.07, .93); }
98% { transform: translateY(0) scale(.99, 1.01); }
}
/* body nod — a pleased look left-and-right, e.g. cheering the poll results */
@keyframes trek-nod {
0%, 100% { transform: translateY(0) rotate(0); }
25% { transform: translateY(-2px) rotate(-4deg); }
50% { transform: translateY(0) rotate(0); }
75% { transform: translateY(-2px) rotate(4deg); }
}
/* slow blink: eyes squeeze shut for a couple of frames each cycle */
@keyframes trek-blink { 0%, 90%, 100% { transform: scaleY(1); } 94%, 97% { transform: scaleY(.12); } }
/* entrance / poke-to-react pop-in (plays on mount and on each remount/tap) */
@keyframes trek-in {
0% { opacity: 0; transform: translateY(7px) scale(.85); }
60% { opacity: 1; }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
/* pupils glancing around — centred most of the time, occasional look down/side */
@keyframes trek-look {
0%, 34%, 100% { transform: translate(0, 0); }
42%, 52% { transform: translate(0, .9px); }
64%, 72% { transform: translate(.9px, .1px); }
84%, 90% { transform: translate(-.8px, .2px); }
}
/* wheels rolling in place */
@keyframes trek-roll { to { transform: rotate(360deg); } }
/* skateboard ollie — body and board pop off the ground together, now and then */
@keyframes trek-sk-body {
0%, 68%, 100% { transform: translateY(0) scale(1, 1); }
73% { transform: translateY(1.5px) scale(1.04, .95); }
84% { transform: translateY(-9px) scale(.97, 1.05); }
92% { transform: translateY(0) scale(1.05, .95); }
96% { transform: translateY(0) scale(1, 1); }
}
@keyframes trek-sk-board {
0%, 68%, 100% { transform: translateY(0) rotate(0); }
73% { transform: translateY(0) rotate(-6deg); }
84% { transform: translateY(-8px) rotate(7deg); }
90% { transform: translateY(-3px) rotate(-3deg); }
95% { transform: translateY(0) rotate(0); }
}
/* guide pennant fluttering — pivots at the pole */
@keyframes trek-flag {
0%, 100% { transform: skewY(0) scaleX(1); }
30% { transform: skewY(-7deg) scaleX(.96); }
60% { transform: skewY(6deg) scaleX(1.03); }
}
/* poll bars staying up, gently rising and settling like live tallies */
@keyframes trek-bar { 0%, 100% { transform: scaleY(.78); } 50% { transform: scaleY(1); } }
@keyframes trek-typing { 0%, 70%, 100% { transform: scale(.5); opacity: .4; } 35% { transform: scale(1); opacity: 1; } }
/* card / note rocking gently */
@keyframes trek-ticket { 0%, 100% { transform: rotate(-2.5deg); } 50% { transform: rotate(2.5deg); } }
/* a line being written: draws in from the left, holds, then a fresh pass */
@keyframes trek-write {
0% { transform: scaleX(0); }
30%, 82% { transform: scaleX(1); }
92%, 100% { transform: scaleX(0); }
}
/* pencil scribbling as the lines appear */
@keyframes trek-pencil {
0%, 100% { transform: translate(0, 0) rotate(0); }
20% { transform: translate(-1.5px, 1.5px) rotate(-4deg); }
45% { transform: translate(1.5px, -.5px) rotate(3deg); }
70% { transform: translate(-1px, 1px) rotate(-2deg); }
}
/* suitcase rocking on its wheels, restless to get going */
@keyframes trek-suitcase { 0%, 100% { transform: rotate(-2.5deg); } 50% { transform: rotate(2.5deg); } }
/* suitcase handle telescoping up and down */
@keyframes trek-handle { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }
/* collections map pin bobbing */
@keyframes trek-pin { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
/* Chain: the pair sways, and the near link slides out along its own axis and
clicks back in. Moving only the near one keeps the joint readable at 88px. */
@keyframes trek-chain { 0%, 100% { transform: rotate(0deg); } 50% { transform: rotate(-6deg); } }
@keyframes trek-link { 0%, 62%, 100% { transform: translateX(0); } 30% { transform: translateX(2.6px); } }
/* atlas globe meridian drifting to fake a slow spin */
@keyframes trek-globe { 0%, 100% { transform: translateX(-2px); } 50% { transform: translateX(2px); } }
/* costs top coin bouncing on */
@keyframes trek-coin { 0%, 100% { transform: translateY(0); } 30% { transform: translateY(-5px); } 55% { transform: translateY(0); } 70% { transform: translateY(-2px); } 84% { transform: translateY(0); } }
/* journey page turning over the spine */
@keyframes trek-page { 0%, 55%, 100% { transform: scaleX(1); } 78% { transform: scaleX(-.9); } }
/* dashboard paper plane bobbing in flight */
@keyframes trek-plane { 0%, 100% { transform: translate(0, 0) rotate(0); } 25% { transform: translate(-1px, -2px) rotate(-4deg); } 50% { transform: translate(0, 0) rotate(0); } 75% { transform: translate(1px, 2px) rotate(4deg); } }
/* notifications Zzz drifting up and fading */
@keyframes trek-z { 0% { opacity: 0; transform: translate(0, 4px) scale(.7); } 30% { opacity: .9; } 100% { opacity: 0; transform: translate(3px, -8px) scale(1.1); } }
/* search magnifier scanning */
@keyframes trek-magnifier { 0%, 100% { transform: translateX(-3px) rotate(-3deg); } 50% { transform: translateX(3px) rotate(3deg); } }
/* to-do checkmark ticking on, holding, then resetting */
@keyframes trek-check { 0%, 8% { transform: scale(0); } 22% { transform: scale(1.2); } 32%, 88% { transform: scale(1); } 96%, 100% { transform: scale(0); } }
.trek { animation: trek-in 480ms cubic-bezier(.34, 1.4, .64, 1) both; transform-origin: 50% 100%; }
.trek-root { animation: trek-idle 3.4s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-body { animation: trek-breathe 3.8s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-shadow { animation: trek-shadow 3.4s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 50%; opacity: .15; filter: blur(1px); }
/* eyes read as cutouts in the surface behind the mascot, so they stay visible on the monochrome body */
.trek-eye { fill: var(--m-bg); }
.trek-eyes { animation: trek-blink 5.2s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.trek-pupils { animation: trek-look 7s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
/* body bounce — per scene */
.trek-bounce { transform-box: fill-box; transform-origin: 50% 100%; }
.trek-bounce--idle { animation: trek-hop 4.6s ease-in-out infinite; }
.trek-bounce--polls { animation: trek-nod 3s ease-in-out infinite; }
.trek-bounce--transport { animation: trek-sk-body 3.6s ease-in-out infinite; }
/* per-scene props */
.trek-board { animation: trek-sk-board 3.6s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 65%; }
.trek-wheel { animation: trek-roll 1.1s linear infinite; transform-box: fill-box; transform-origin: center; }
.trek-flag { animation: trek-flag 1.8s ease-in-out infinite; transform-box: fill-box; transform-origin: 0% 50%; }
.trek-ticket { animation: trek-ticket 3.6s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-note { animation: trek-ticket 3.9s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-pencil { animation: trek-pencil 1.15s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.trek-write { animation: trek-write 3.8s ease-in-out infinite; transform-box: fill-box; transform-origin: 0% 50%; }
.trek-bar { animation: trek-bar 2.6s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-dot { animation: trek-typing 1.4s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 50%; }
.trek-suitcase { animation: trek-suitcase 1.5s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-handle { animation: trek-handle 1.5s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-pin { animation: trek-pin 2.2s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-chain { animation: trek-chain 3s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.trek-link { animation: trek-link 2.4s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.trek-globe { animation: trek-globe 3s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.trek-coin { animation: trek-coin 2s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 100%; }
.trek-page { animation: trek-page 3.4s ease-in-out infinite; transform-box: fill-box; transform-origin: 0% 50%; }
.trek-plane { animation: trek-plane 3s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.trek-magnifier { animation: trek-magnifier 2.4s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.trek-z { animation: trek-z 2.8s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
.trek-check { animation: trek-check 3s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }
/* staggered starts for written lines / poll bars / Zzz */
.trek-d2, .trek-z2 { animation-delay: .5s; }
.trek-d3, .trek-z3 { animation-delay: 1s; }
/* Mobile range slider — the --m-* twin of the desktop .trek-range. */
.m-range {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 5px;
border-radius: 999px;
background: linear-gradient(to right, var(--m-act) var(--fill, 50%), var(--m-trackoff) var(--fill, 50%));
outline: none;
cursor: pointer;
}
.m-range::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--m-knob);
border: 1px solid var(--m-rowbr);
box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
transition: transform .12s ease;
}
.m-range::-webkit-slider-thumb:hover { transform: scale(1.1); }
.m-range::-webkit-slider-thumb:active { transform: scale(1.18); }
.m-range::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--m-knob);
border: 1px solid var(--m-rowbr);
box-shadow: 0 1px 4px rgba(0, 0, 0, .25);
}
.m-range::-moz-range-track { height: 5px; border-radius: 999px; background: transparent; }
@media (prefers-reduced-motion: reduce) {
.trek, .trek-root, .trek-body, .trek-shadow, .trek-eyes, .trek-pupils,
.trek-bounce--idle, .trek-bounce--polls, .trek-bounce--transport,
.trek-board, .trek-wheel, .trek-flag, .trek-ticket, .trek-note, .trek-pencil, .trek-write, .trek-bar,
.trek-suitcase, .trek-handle, .trek-pin, .trek-globe, .trek-coin, .trek-page, .trek-plane,
.trek-magnifier, .trek-z, .trek-check, .trek-chain, .trek-link { animation: none; }
/* Zzz would sit invisible at opacity 0 without their animation — pin them visible */
.trek-z { opacity: 1; }
}
/* The phone map carries no visible credit.
*
* Both engines park a credit at the bottom of the map, and on a phone the map is the
* whole screen, so whatever is put there sits in the middle of the picture. It has been
* a band across the bottom (#2022) and then a round (i) beside the locate button, and
* neither reads as part of this shell: a lone control under the locate button is the
* first thing the eye lands on over a full-bleed map.
*
* So on the phone it is off. The desktop map is untouched and still shows the full
* credit with its links, which is where this belongs; `.m-root` is the phone shell only.
* Both engines and both corners, because a phone meets either depending on the screen,
* and the GL containers hold nothing else — MapViewGL adds no control of its own, so
* hiding the corner hides the credit and the wordmark and nothing besides.
*
* Owner's call, made knowingly: OSM and the tile vendors ask for a visible credit. */
.m-root .leaflet-control-attribution,
.m-root .maplibregl-ctrl-bottom-left,
.m-root .mapboxgl-ctrl-bottom-left,
.m-root .maplibregl-ctrl-bottom-right,
.m-root .mapboxgl-ctrl-bottom-right {
display: none;
}
/* Text fields carry the row's background, not their own.
*
* iOS paints its own background inside a text control, and under
* `color-scheme: dark` that fill is darker than the card the field sits on —
* so every field showed as a dark rectangle inside a lighter row (#2028). The
* mobile fields are deliberately `bg-transparent` because the row around them
* supplies the surface, which is exactly why the native fill has nothing
* covering it here and shows through; a desktop field hides it behind its own
* background. Dropping the native appearance is what actually removes it.
*
* Only text entry. `file`, `color`, `range` and the date/time pickers need
* their native chrome to stay usable, and `select` keeps its own arrow. */
.m-root input:not([type]),
.m-root input[type="text"],
.m-root input[type="password"],
.m-root input[type="email"],
.m-root input[type="url"],
.m-root input[type="search"],
.m-root textarea {
-webkit-appearance: none;
appearance: none;
}
/* Horizontal scroller without a visible scrollbar, for control rows that must
survive narrow viewports and large system font scales (dashboard filter). */
.m-hscroll {
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.m-hscroll::-webkit-scrollbar {
display: none;
}