mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 08:11:03 +00:00
8955790f17
- Top bar: the segmented control keeps to the room between the back button and the trailing button, draws its segments tighter and then scrolls with faded edges instead of covering the back button. On a wide window every list tab puts its primary pill in the middle and its round buttons at the trailing end, so the pill no longer jumps between tabs. - Day strip: a chevron over the faded end pages the days along on a wide window, and the fade reaches further; the chips keep clear of it. - Plan pane: the day card ends on the rail's bottom line beside the map (as tall as its day across the page), scrolls inside a frame and fades the rows at its edges. Addresses and the hotel chip end in an ellipsis instead of being cut mid-letter; the weather chip keeps its width. - Map: in the dark theme the map beside the pane takes the dark basemap. - Places: the All / Unplanned / Planned chips are never narrower than their names and scroll with a faded edge; on a short screen Places/Tours shares their row. - Chat: the list fades out under the floating controls instead of cutting a bubble. Notes and to-do chip rows fade at their edges and New Note stays outside the scrolling chips. - Costs: the balance amounts share a line, the settle-up action wraps to a line of its own, the totals stand under their labels. - Bookings and transports: one column until two cards fit, titles take two lines with the type under them, the time cell is never cut and a range names its unit once. - Files: the preview pane shows a type tile beside the name where no PDF can be drawn in place, its actions keep their labels on one line and wrap as a row, link labels are badges, and a tile that opens its file is no toggle. - Polls get a fill that differs from card and track, packing filters are two segmented controls, empty checkboxes get an edge with 3:1 contrast, and the middle dots between values are gone.
809 lines
37 KiB
CSS
809 lines
37 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);
|
|
/* The dock's glass: denser than --m-glass, so a photo scrolling under it
|
|
* neither turns it muddy nor washes out its icons. */
|
|
--m-dock: rgba(255,255,255,.8);
|
|
--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);
|
|
/* The edge of an empty checkbox: at least 3:1 against a row, so an open item
|
|
* shows its box rather than reading as plain text. */
|
|
--m-checkbr: rgba(16,16,19,.55);
|
|
|
|
/* 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, written to the shell's root as
|
|
* data-size): 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.
|
|
* Asked from <html>, so a sheet's own .m-root (portalled, perhaps to body)
|
|
* reads the same values as the shell. */
|
|
html:has(.m-shell[data-size='wide'], .m-shell[data-size='split']) .m-root {
|
|
--bottom-nav-h: env(safe-area-inset-bottom, 0px);
|
|
--m-rail-w: calc(env(safe-area-inset-left, 0px) + 100px);
|
|
}
|
|
html[dir='rtl']:has(.m-shell[data-size='wide'], .m-shell[data-size='split']) .m-root {
|
|
--m-rail-w: calc(env(safe-area-inset-right, 0px) + 100px);
|
|
}
|
|
|
|
.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-dock: rgba(26,26,31,.82);
|
|
--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);
|
|
--m-checkbr: rgba(255,255,255,.4);
|
|
--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. */
|
|
@keyframes m-sheet-up-wide {
|
|
from { transform: translateY(calc(50dvh + 50%)); }
|
|
to { transform: translateY(0); }
|
|
}
|
|
@keyframes m-sheet-down-wide {
|
|
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 (prefers-reduced-motion: no-preference) {
|
|
html:has(.m-shell[data-size='wide'], .m-shell[data-size='split']) .m-sheet-up { animation-name: m-sheet-up-wide; }
|
|
html:has(.m-shell[data-size='wide'], .m-shell[data-size='split']) .m-sheet-down { animation-name: m-sheet-down-wide; }
|
|
}
|
|
html:has(.m-shell[data-size='wide'], .m-shell[data-size='split']) .m-panel-in { animation: m-panel-in 320ms var(--ease-out-quint) both; }
|
|
html[dir='rtl']:has(.m-shell[data-size='wide'], .m-shell[data-size='split']) .m-panel-in { animation-name: m-panel-in-rtl; }
|
|
@media (prefers-reduced-motion: reduce) {
|
|
html:has(.m-shell[data-size='wide'], .m-shell[data-size='split']) .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;
|
|
}
|
|
/* The few routes without a phone screen of their own (a plugin's page, help,
|
|
a trip's files) still render through PageShell. Inside the touch shell its
|
|
desktop navbar, shown from 768px up, would stand over the rail and repeat the
|
|
navigation, and --nav-h would keep 64px of room for it: the shell has its own
|
|
navigation, so neither is there. */
|
|
html:has(.m-shell) {
|
|
--nav-h: 0px;
|
|
}
|
|
html:has(.m-shell) [data-desktop-navbar] {
|
|
display: none;
|
|
}
|
|
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);
|
|
}
|
|
|
|
/* The strip under a floating dock (components/MDockScrim.tsx): solid across
|
|
the gap below the dock, clear from about its middle up. */
|
|
.m-dock-scrim {
|
|
-webkit-mask-image: linear-gradient(to top, #000 calc(env(safe-area-inset-bottom, 0px) + 12px), transparent calc(env(safe-area-inset-bottom, 0px) + 40px));
|
|
mask-image: linear-gradient(to top, #000 calc(env(safe-area-inset-bottom, 0px) + 12px), transparent calc(env(safe-area-inset-bottom, 0px) + 40px));
|
|
}
|
|
|
|
/* 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 {
|
|
/* A strip with a chevron over its faded end fades wider (--m-fade-reach). */
|
|
--m-fade-w: var(--m-fade-reach, 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: var(--m-fade-reach-y, 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;
|
|
}
|
|
}
|
|
|
|
/* A segmented control in the trip's top bar that does not fit between the back
|
|
button and the trailing button draws its segments tighter first
|
|
(MTripShellParts GlassSegment), and scrolls only where that is not enough. */
|
|
.m-seg[data-tight='true'] > * {
|
|
padding-inline: 10px;
|
|
}
|
|
/* A one-line Markdown caption (MarkdownText clamp: an address, a note in a
|
|
row) ends in an ellipsis. The clamp sits on the wrapper, but the text is in
|
|
the paragraph inside it, so the line was cut off mid-letter at the row's
|
|
edge or under its buttons with nothing to say it went on. */
|
|
.m-root .collab-note-md.truncate > * {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The trip's shared links in the touch shell (CollabLinks in MCollabTab). Their
|
|
edit, pin and delete buttons are always out on a touch screen, so a link in
|
|
one of two columns kept a sliver for its title ("JR seat / reservations") and
|
|
cut its host. One column, until the board has room for two links side by
|
|
side with their titles on one line. */
|
|
.m-root .collab-link-grid__cells {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
@container (min-width: 720px) {
|
|
.m-root .collab-link-grid__cells {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|