/* ============================================================================ apple-motion.css — response, materials, type and reduced motion. Loaded last, so it can settle arguments with the older sheets on source order alone. Three things live here, and nothing else: 1. Press feedback and input latency (§ "Response"). Feedback belongs on pointer-DOWN. Waiting for the click to acknowledge a tap is the single cheapest way to make an interface feel dead, and almost every control in this app was hover-only — which on a phone means no feedback at all until the action has already happened. 2. Material weight and type (§ "Materials", § "Typography"). Translucent chrome should read as a real, thick surface with content moving under it; tracking is size-specific, never one value for every size. 3. The three accessibility signals (§ "Reduced motion"). Reduced motion is not "no feedback" — it is a non-vestibular equivalent. Looping decoration stops; entrances cross-fade in place instead of travelling; anything that communicates state stays. ============================================================================ */ :root { /* One place for the curves, so a timing value is never invented twice. These mirror the spring parameters in src/ui/motion.js: CSS cannot do a real spring, but for a press — which is short and cannot be grabbed mid-flight — a curve is honest enough. */ --sb-press: 110ms cubic-bezier(0.2, 0, 0, 1); --sb-settle: 240ms cubic-bezier(0.2, 0.7, 0.3, 1); } /* ── 1. Response ─────────────────────────────────────────────────────────── */ /* `touch-action: manipulation` drops the ~300ms wait a mobile browser spends deciding whether a tap was the first half of a double-tap-to-zoom. Nothing here is zoomable, so that delay buys nothing and costs the illusion of directness. The tap highlight goes with it — we draw our own. */ button, [role="button"], a, label, summary, input[type="checkbox"], input[type="radio"], .cursor-pointer { touch-action: manipulation; -webkit-tap-highlight-color: transparent; } /* The press itself. Deliberately low specificity (0,1,1): every control that already defines its own :active — .button, .btn-partner-modern, the header icons — keeps it, because a class selector outranks this. */ button:not(:disabled):active, [role="button"]:not([aria-disabled="true"]):active, .cursor-pointer:active { transform: scale(0.97); transition: transform var(--sb-press); } /* Release should settle rather than snap back at the same speed it depressed. */ button, [role="button"] { transition: transform var(--sb-settle); } /* Disabled controls must not pretend to respond. */ button:disabled, [aria-disabled="true"] { cursor: not-allowed; } /* A pointer that cannot hover should not inherit hover state — on touch, :hover sticks after the tap and leaves controls lit up with no way to clear them. */ @media (hover: none) { .card-minimal:hover, .wallet-logo:hover, .btn-primary:hover, .btn-secondary:hover, .btn-verify:hover, .lightning-button:hover, .btn-partner-modern:hover, .transfer-item:hover, .file-drop-zone:hover, .project-card:hover .project-card__bg { transform: none; box-shadow: none; } } /* Keyboard users get the same clarity as pointer users. */ :focus-visible { outline: 2px solid rgba(249, 115, 22, 0.75); outline-offset: 2px; } /* ── 2. Materials ────────────────────────────────────────────────────────── */ /* The header's own background is set inline by Header.jsx (it cross-fades on scroll) so its blur is fixed there. What a class rule can still add is the bright top edge — light catching the near face of the glass, which is what separates a translucent bar from a washed-out rectangle. */ .header-minimal { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); } /* Cards are a smaller surface than chrome, so they read thinner: less blur, a shallower shadow. Weight is how a surface says how big it is. */ .card-minimal { backdrop-filter: blur(14px) saturate(140%); -webkit-backdrop-filter: blur(14px) saturate(140%); } /* ── 3. Typography ───────────────────────────────────────────────────────── */ /* Tracking is size-specific. Letterforms drift apart as they grow, so display text needs negative tracking to hold together, and small text needs a touch of positive tracking to stay legible. A single letter-spacing value is wrong at one end of the scale or the other — usually both. Leading moves the opposite way: tight on headings, open on body copy. */ html { font-optical-sizing: auto; } h1, .text-6xl, .text-5xl { letter-spacing: -0.024em; line-height: 1.05; } h2, .text-4xl { letter-spacing: -0.021em; line-height: 1.1; } h3, .text-3xl { letter-spacing: -0.018em; line-height: 1.15; } .text-2xl { letter-spacing: -0.014em; } .text-xl { letter-spacing: -0.011em; } .text-lg { letter-spacing: -0.006em; } .text-base, p { letter-spacing: 0; } .text-sm { letter-spacing: 0.004em; } .text-xs { letter-spacing: 0.012em; } /* Text over a blurred surface loses contrast against whatever is moving behind it. Flat mid-grey is the first thing to disappear; a little more weight and a little more tracking is what keeps it readable — the same trade vibrancy makes on Apple's platforms. */ .header-minimal .text-secondary, .chat-input-area .text-secondary, .card-minimal .text-muted { color: #c3c3c9; letter-spacing: 0.01em; } /* ── 4. Reduced motion ───────────────────────────────────────────────────── */ @media (prefers-reduced-motion: reduce) { /* Looping, decorative motion has no state to communicate — it is the first thing to go, and nothing is lost by holding it on a final frame. */ .wallet-logos-track, .button, .button::before, .icon-loading, [style*="sbPulse"], [style*="sbFlow"], [style*="sbNode"], [style*="sbScan"], [style*="sbTrav"], [style*="sbRing"], [style*="sbLiveBg"], [style*="sbCallPulse"], [style*="rmPulse"], [style*="vmRing"] { animation: none !important; } /* Entrances still happen — they explain that something new arrived — but they cross-fade in place instead of sliding, which is the part that provokes a vestibular response. */ @keyframes sbFadeInPlace { from { opacity: 0; } to { opacity: 1; } } .message-slide, [style*="sbUp"], [style*="sbSlideUp"], [style*="sbExpand"], [style*="ccUp"], [style*="ptUp"], [style*="wuUp"], [style*="rmExp"], [style*="sbRow"], [style*="sbIn1"], [style*="sbIn2"] { animation: sbFadeInPlace 160ms ease-out both !important; transform: none !important; } /* A pulsing REC dot is status, not decoration: keep it, but as opacity alone — the scale component is what makes it jump. */ [style*="vmRec"] { animation: vmRecSoft 1.4s ease-in-out infinite !important; transform: none !important; } @keyframes vmRecSoft { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } /* Nothing lifts, scales or hops. Named rather than `*:hover`, because a transform is also how things are centred — a blanket rule would drop elements out of position rather than merely still them. */ .card-minimal:hover, .wallet-logo:hover, .btn-primary:hover, .btn-secondary:hover, .btn-verify:hover, .lightning-button:hover, .btn-partner-modern:hover, .transfer-item:hover, .file-drop-zone:hover, .project-card:hover, .project-card:hover .project-card__bg, .header-minimal .cursor-pointer:hover { transform: none !important; } button:not(:disabled):active, [role="button"]:active, .cursor-pointer:active { transform: none !important; opacity: 0.7; } /* The mobile drawer's scrim opacity is driven imperatively, frame by frame, from src/ui/motion.js. Under normal motion a CSS transition on top of that would fight the finger; with motion reduced the spring resolves instantly, so the transition is the whole animation — the drawer cross-fades in place instead of sliding in from the edge. */ .sb-drawer-overlay { transition: opacity 160ms ease !important; } /* Programmatic smooth scrolling is a full-viewport slide. The JS callers check the same media query; this covers the CSS ones. */ html, body, .scroll-smooth, .messages-container, .chat-messages-area, .sb-scroll, .track { scroll-behavior: auto !important; } } /* ── 5. Reduced transparency ─────────────────────────────────────────────── */ /* Frostier and heavier, not merely less blurred: the surface still has to separate itself from what is behind it, and without translucency the only tools left are opacity and a border. */ @media (prefers-reduced-transparency: reduce) { .header-minimal, .card-minimal, .sb-chat-header { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; } .header-minimal { background: #18181a !important; } .card-minimal { background: #202022 !important; } } /* ── 6. Increased contrast ───────────────────────────────────────────────── */ @media (prefers-contrast: more) { .card-minimal, .icon-container, .verification-code, .security-shield { background: #141416 !important; border-color: rgba(255, 255, 255, 0.42) !important; } .text-secondary { color: #e4e4e8 !important; } .text-muted { color: #c8c8cf !important; } :focus-visible { outline-width: 3px; } }