Files
TREK/client/src/mobile/mobile.css
T
Maurice 0b7cee0b0d feat(mobile): the trip's tabs, administration, collections and the journal use the large screen
On an iPad or an unfolded foldable the list screens kept one phone-wide
column in the middle of the page. They now lay out for the room they have:

- Transports, bookings, polls and tasks put their cards in a grid of as many
  phone-wide columns as the page holds (CardGrid), three on a 13" iPad on
  its side, two upright, one in a narrow Split View.
- Costs: with room for two panes (split size class) the summary (total,
  owe and owed, settle up, final budget, categories) stands in a pane of its
  own beside the expenses, each scrolling on its own (TabPanes). The search
  field sits in the expenses' column again instead of running past it.
- Files: a grid of tiles with each photo's picture; with room for two panes
  the picked file is previewed beside the grid with what it belongs to and
  its actions, the way Files on an iPad does. The signed preview address
  comes through fileRepo.previewUrl.
- Packing: the categories stand in columns, each card whole in its column;
  the action menu drops from its button at a popover's width.
- To-dos: the four smart filters as tiles and the trip's lists in a pane
  beside the tasks, which take the filter's name as their head.
- Collab: the chat stays in view at the trailing edge beside the notes,
  links or polls, and the header switches only the board. The chat keeps its
  place in the tree, so a fold or a rotation into two panes keeps the draft;
  a chat at its newest message stays there when its box changes size. Notes
  stand as a board in columns.
- Administration and settings share a section list beside the page
  (MSectionNav); the stats grid takes four columns.
- Collections: the lists stand in a column beside the places, which take two
  columns, and the head names the list on show.
- Journal: the entries become a feed beside the map, under their days, with
  photo, place and the start of the story, in step with the map like the
  strip of cards it replaces; it fades out under the header.
- The assistant opens as a tall panel at the trailing edge over a dimmed
  page, as Slide Over would, instead of covering the whole screen.
- The rail fits every name: a word too wide for it shrinks the label before
  it is hyphenated, and the admin entry uses its short name.
- The journeys and notifications headers keep to their column.
- The address screen of the native shell sets the photo as a panel beside
  the choices on a large screen on its side.

On a foldable that reports its fold, a leading pane ends at the fold and a
trailing one starts there. Phones keep their layout.
2026-11-07 23:25:12 +01:00

660 lines
30 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);
}
}
.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;
}