Files
TREK/client/src/mobile/mobile.css
T
Maurice b7a91fc84e fix(mobile): one form sheet for every dialog, opaque and placed beside the rail
Every sheet now stands on the opaque sheet surface: the frosting the
translucent one relied on is dropped while the scrim fades, so the page
read through the fields as sharp ghost text.

On a wide window a sheet is centred in the content beside the navigation
rail instead of on the whole window, so it never lies on the rail, and it
grows to the window's height instead of stopping at 880px. On a phone the
card stands between the status bar and the dock rather than across the
dock. A sheet marked as a form fills the height of a window as short as a
phone on its side, with Cancel and Save beside the title (new `short:`
variant, m-form-sheet grid in mobile.css).

The form chrome gains a scrolling body that fades at the edges with more
behind them (useEdgeFade on the vertical axis), a date field whose
keyboard entry moves into the picker, a field wrapper for amounts with a
currency, and two-line titles. The focused sheet no longer draws the
app-wide focus frame or loses its corners, and the desktop's dark field
paint stays out of the touch shell, which painted a dark box inside every
rounded field.
2026-11-07 23:25:14 +01:00

752 lines
33 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);
/* The navigation rail's share of the leading edge (wide windows only), what
* every full-width fixed bar and the page column start after. */
--m-rail-w: 0px;
}
/* Wide size class (hooks/sizeClass.ts WIDE_QUERY): the rail on the leading
* edge takes the dock's place, so nothing docks at the bottom any more and
* content only clears the home indicator. */
@media (min-width: 600px) and (min-height: 501px) {
.m-root {
--bottom-nav-h: env(safe-area-inset-bottom, 0px);
--m-rail-w: calc(env(safe-area-inset-left, 0px) + 92px);
}
:root[dir='rtl'] .m-root {
--m-rail-w: calc(env(safe-area-inset-right, 0px) + 92px);
}
}
.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 ──────────────────────────────────────────────────────────── */
/* The floating card pops in place, the way a native alert does. */
@keyframes m-sheet-in {
from { opacity: 0; transform: translateY(10px) scale(.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes m-sheet-out {
from { opacity: 1; transform: translateY(0) scale(1); }
to { opacity: 0; transform: translateY(6px) scale(.97); }
}
/* The docked sheet rises from below the screen edge and sinks back there: it
sits above the nav, so it travels its own height plus that gap. */
@keyframes m-sheet-up {
from { transform: translateY(calc(100% + var(--bottom-nav-h, 84px) + 24px)); }
to { transform: translateY(0); }
}
@keyframes m-sheet-down {
from { transform: translateY(0); }
to { transform: translateY(calc(100% + var(--bottom-nav-h, 84px) + 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); }
}
/* A panel at the trailing edge (the assistant on a wide window) slides in from
that edge, the way Slide Over does. */
@keyframes m-panel-in {
from { opacity: 0; transform: translateX(28px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes m-panel-in-rtl {
from { opacity: 0; transform: translateX(-28px); }
to { opacity: 1; transform: translateX(0); }
}
/* A form sheet on a wide window sits mid-screen, so it rises from the bottom
edge of the window rather than from just below its own resting place. */
@media (min-width: 600px) and (min-height: 501px) {
@keyframes m-sheet-up {
from { transform: translateY(calc(50dvh + 50%)); }
to { transform: translateY(0); }
}
@keyframes m-sheet-down {
from { transform: translateY(0); }
to { transform: translateY(calc(50dvh + 50%)); }
}
}
.m-sheet-in { animation: m-sheet-in 260ms var(--ease-out-quint) both; }
.m-sheet-out { animation: m-sheet-out 220ms var(--ease-drawer) both; }
.m-sheet-up { animation: m-sheet-up 420ms var(--ease-drawer) both; }
.m-sheet-down { animation: m-sheet-down 300ms 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; }
@media (min-width: 600px) and (min-height: 501px) {
.m-panel-in { animation: m-panel-in 320ms var(--ease-out-quint) both; }
[dir='rtl'] .m-panel-in { animation-name: m-panel-in-rtl; }
@media (prefers-reduced-motion: reduce) {
.m-panel-in, [dir='rtl'] .m-panel-in { animation: m-fade-in 120ms ease-out both; }
}
}
/* Reduced motion: gentler, not zero (same rule the desktop keyframes follow). */
@media (prefers-reduced-motion: reduce) {
.m-sheet-in, .m-sheet-up, .m-drawer-in, .m-pop-in, .m-toast-in {
animation: m-fade-in 120ms ease-out both;
}
.m-sheet-out, .m-sheet-down, .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); } }
/* tours: a walking step, the pole swinging forward and planting, the pack riding along */
@keyframes trek-walk { 0%, 50%, 100% { transform: translateY(0); } 25%, 75% { transform: translateY(-1.6px); } }
@keyframes trek-pole { 0%, 100% { transform: rotate(-9deg); } 45% { transform: rotate(7deg); } 55% { transform: rotate(7deg); } }
@keyframes trek-pack { 0%, 50%, 100% { transform: rotate(0); } 25%, 75% { transform: rotate(-3deg); } }
@keyframes trek-trail { to { stroke-dashoffset: 8; } }
.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; }
.trek-bounce--tours { animation: trek-walk 1.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; }
.trek-pole { animation: trek-pole 1.6s ease-in-out infinite; transform-box: fill-box; transform-origin: 50% 0%; }
.trek-pack { animation: trek-pack 1.6s ease-in-out infinite; transform-box: fill-box; transform-origin: 90% 10%; }
.trek-trail { animation: trek-trail .8s linear infinite; }
/* 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-bounce--tours, .trek-pole, .trek-pack, .trek-trail,
.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;
}
/* ── Page transitions (mobile/routeTransition.ts) ───────────────────────────
The way a native navigation stack moves: forward slides the new page in from
the trailing edge over the old one, which drifts a little the other way and
dims; back is the mirror; switching between the dock's sections only
cross-fades. --m-vt-dir flips the travel for right-to-left languages. The
dock keeps its own name, so it stands still while the pages move under it. */
:root {
--m-vt-dir: 1;
/* The company-holiday tint of the Vacay cells (vacayDayModel), for the legend chip. */
--m-vacay-company: #f5d9a6;
}
:root[dir='rtl'] {
--m-vt-dir: -1;
}
.m-dock-vt {
view-transition-name: m-dock;
}
@keyframes m-vt-in-trailing {
from {
transform: translateX(calc(100% * var(--m-vt-dir)));
}
}
@keyframes m-vt-out-leading {
to {
transform: translateX(calc(-30% * var(--m-vt-dir)));
filter: brightness(0.82);
}
}
@keyframes m-vt-in-leading {
from {
transform: translateX(calc(-30% * var(--m-vt-dir)));
filter: brightness(0.82);
}
}
@keyframes m-vt-out-trailing {
to {
transform: translateX(calc(100% * var(--m-vt-dir)));
}
}
:root[data-route-transition='forward']::view-transition-old(root) {
animation: m-vt-out-leading 0.4s cubic-bezier(0.32, 0.72, 0, 1) both;
}
:root[data-route-transition='forward']::view-transition-new(root) {
animation: m-vt-in-trailing 0.4s cubic-bezier(0.32, 0.72, 0, 1) both;
box-shadow: -16px 0 40px -12px rgb(0 0 0 / 0.35);
}
:root[data-route-transition='back']::view-transition-old(root) {
z-index: 1;
animation: m-vt-out-trailing 0.34s cubic-bezier(0.32, 0.72, 0, 1) both;
box-shadow: -16px 0 40px -12px rgb(0 0 0 / 0.35);
}
:root[data-route-transition='back']::view-transition-new(root) {
animation: m-vt-in-leading 0.34s cubic-bezier(0.32, 0.72, 0, 1) both;
}
:root[data-route-transition='tab']::view-transition-old(root),
:root[data-route-transition='tab']::view-transition-new(root) {
animation-duration: 0.18s;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
/* A scroll box under a fixed header: content fades out over the padding at its
edges instead of being cut off in a hard line right under the header. */
.m-scroll-fade {
-webkit-mask-image: linear-gradient(to bottom, transparent 0, black 14px, black calc(100% - 14px), transparent 100%);
mask-image: linear-gradient(to bottom, transparent 0, black 14px, black calc(100% - 14px), transparent 100%);
}
/* The shared pickers (select, date, time) inside the phone UI look like the
sheets' own fields: a 42 px round field on the field surface. They style
themselves inline for the desktop, hence the !important. */
.m-root [data-field-trigger] {
min-height: 42px;
border-radius: 9999px !important;
border: 1px solid var(--m-rowbr) !important;
background: var(--m-ic) !important;
}
.m-root button[data-field-trigger],
.m-root [data-field-trigger='pad'] {
padding-inline: 14px !important;
}
/* ── The page column on a wide window ───────────────────────────────────────
Fixed bars span the page, not the window: they start after the navigation
rail (--m-rail-w, 0 on a phone, so these are the phone's insets there). */
.m-bar-x {
inset-inline-start: calc(var(--m-rail-w, 0px) + 16px);
inset-inline-end: 16px;
}
.m-span-x {
inset-inline-start: var(--m-rail-w, 0px);
inset-inline-end: 0;
}
/* A screen whose background is a map reaches under the rail: the map runs to
the window's edge and the rail floats over it like the top controls do. Its
own bars then keep to the page column with m-bar-x / m-span-x. */
.m-bleed {
margin-inline-start: calc(var(--m-rail-w, 0px) * -1);
}
/* The trip's pane beside the map (split size class, MTripShell): a third of
the window, within what a list and its rows need. On a foldable that reports
its fold (two horizontal viewport segments), the pane takes the first half
to the fold exactly, so no row runs across the crease and the map has the
other half to itself. */
.m-trip {
--trip-pane-open: clamp(360px, 38vw, 460px);
}
@media (horizontal-viewport-segments: 2) {
.m-trip {
--trip-pane-open: calc(env(viewport-segment-width 0 0, 50vw) - var(--m-rail-w, 0px));
}
}
/* The leading pane of a two-pane screen (a tab's summary, a list beside its
detail): about a third of the page, within what its cards need. On a
foldable that reports its fold it ends at the fold, like the trip's pane. */
.m-root {
--m-pane-aside-w: clamp(320px, 34vw, 420px);
--m-pane-trail-w: clamp(340px, 36vw, 440px);
}
@media (horizontal-viewport-segments: 2) {
.m-root {
--m-pane-aside-w: calc(env(viewport-segment-width 0 0, 50vw) - var(--m-rail-w, 0px));
--m-pane-trail-w: env(viewport-segment-width 1 0, 50vw);
}
}
/* The Android app reports a foldable's fold itself (native/windowFold.ts):
MobileShell marks a vertical fold that separates the window with data-fold
and its place in --m-fold-start/--m-fold-end, and the panes meet there. The
leading side follows the reading direction, the fold's place does not. */
.m-root[data-fold='vertical'] {
--m-pane-aside-w: calc(var(--m-fold-start) - var(--m-rail-w, 0px));
--m-pane-trail-w: calc(100vw - var(--m-fold-end));
}
.m-root[data-fold='vertical'] .m-trip {
--trip-pane-open: calc(var(--m-fold-start) - var(--m-rail-w, 0px));
}
:root[dir='rtl'] .m-root[data-fold='vertical'] {
--m-pane-aside-w: calc(100vw - var(--m-fold-end) - var(--m-rail-w, 0px));
--m-pane-trail-w: var(--m-fold-start);
}
:root[dir='rtl'] .m-root[data-fold='vertical'] .m-trip {
--trip-pane-open: calc(100vw - var(--m-fold-end) - var(--m-rail-w, 0px));
}
.m-pane-aside {
width: var(--m-pane-aside-w);
}
/* The trailing pane: what accompanies the page rather than leads it, like the
trip chat beside its notes and polls. */
.m-pane-trail {
width: var(--m-pane-trail-w);
}
/* The phone rules of index.css, for the app. It shows this shell at every
size, an iPad's included (mobile/useIsPhone.ts), where their media query does
not match; the shell's root carries .m-shell, so they follow it there: the
document is the scroller, the shell paints the background, no zoom buttons
on a touch map, no scrollbars. */
html:has(.m-shell) {
overflow: visible;
overscroll-behavior: none;
}
html:has(.m-shell) body {
height: auto;
overflow: visible;
overflow-x: clip;
background-color: transparent;
}
html:has(.m-shell) .leaflet-control-zoom {
display: none !important;
}
html:has(.m-shell) * {
scrollbar-width: none;
}
html:has(.m-shell) ::-webkit-scrollbar {
width: 0;
height: 0;
}
/* While the trip's map runs under the rail (MTripShell, --trip-map-inset), the
controls both engines draw in its corners step clear of the rail. A margin on
the leading side moves only the corner next to the rail, in either reading
direction: the corner on the far side is anchored there and does not move. */
.m-trip .leaflet-control-container > div,
.m-trip .maplibregl-control-container > div,
.m-trip .mapboxgl-control-container > div {
margin-inline-start: var(--trip-map-inset, 0px);
}
/* A horizontal strip with more behind an edge fades out there
(components/useEdgeFade.ts), so a cut chip reads as "the row goes on". */
.m-fade-s,
.m-fade-e,
.m-fade-se {
--m-fade-w: 28px;
-webkit-mask-image: var(--m-fade-mask);
mask-image: var(--m-fade-mask);
}
.m-fade-s {
--m-fade-mask: linear-gradient(var(--m-fade-dir, 90deg), transparent 0, #000 var(--m-fade-w));
}
.m-fade-e {
--m-fade-mask: linear-gradient(var(--m-fade-dir, 90deg), #000 calc(100% - var(--m-fade-w)), transparent 100%);
}
.m-fade-se {
--m-fade-mask: linear-gradient(var(--m-fade-dir, 90deg), transparent 0, #000 var(--m-fade-w), #000 calc(100% - var(--m-fade-w)), transparent 100%);
}
:root[dir='rtl'] .m-fade-s,
:root[dir='rtl'] .m-fade-e,
:root[dir='rtl'] .m-fade-se {
--m-fade-dir: 270deg;
}
/* The same for a vertical scroller, a form sheet's body above all: it fades
out at the top once scrolled and at the foot while more waits below, so a
field cut by the footer reads as "the form goes on". Never mirrored. */
.m-fade-t,
.m-fade-b,
.m-fade-tb {
--m-fade-h: 24px;
-webkit-mask-image: var(--m-fade-vmask);
mask-image: var(--m-fade-vmask);
}
.m-fade-t {
--m-fade-vmask: linear-gradient(to bottom, transparent 0, #000 var(--m-fade-h));
}
.m-fade-b {
--m-fade-vmask: linear-gradient(to bottom, #000 calc(100% - var(--m-fade-h)), transparent 100%);
}
.m-fade-tb {
--m-fade-vmask: linear-gradient(to bottom, transparent 0, #000 var(--m-fade-h), #000 calc(100% - var(--m-fade-h)), transparent 100%);
}
/* A hint longer than its field ends in an ellipsis instead of being cut off
mid-word at the field's edge ("Код аэропорта или город (нап"). */
.m-root input:placeholder-shown {
text-overflow: ellipsis;
}
/* ── Sheets ─────────────────────────────────────────────────────────────────
A sheet takes the focus itself when it opens (modalFocus.ts), so a screen
reader starts inside it. The app-wide focus ring (index.css) is meant for
controls: on the sheet it drew a 2px frame round the whole dialog and
squared its corners off to 4px. */
.m-root [data-m-sheet]:focus-visible {
outline: none;
border-radius: var(--m-sheet-r);
}
/* A form sheet (MSheet `form`) on a window as short as a phone on its side
fills the height, and its actions move up beside the title the way an
iPhone form carries Cancel and Save in its bar, so the fields get every
line in between instead of a slot 140px tall. Cancel then does what the
header's close button did, so that one goes. */
@media (max-height: 500px) {
/* Row 1: the title and the actions. Row 2: whatever a form keeps fixed
under its title (the transport's manual/automated switch). Row 3: the
fields, which take the rest. Doubled class: the panel's own `flex`
utility must not win the display back. */
.m-form-sheet.m-form-sheet {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto auto minmax(0, 1fr);
align-items: center;
}
.m-form-sheet > * {
grid-column: 1 / -1;
}
.m-form-sheet > .m-fs-head {
grid-area: 1 / 1;
}
.m-form-sheet > .m-fs-foot {
grid-area: 1 / 2;
border-top: 0;
padding-block: 16px 8px;
padding-inline: 0 18px;
}
.m-form-sheet > .m-fs-body {
grid-row: 3;
align-self: stretch;
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 16px);
}
.m-form-sheet:has(> .m-fs-foot) > .m-fs-head .m-fs-close {
display: none;
}
}