Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

1644 lines
49 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
body { font-family: 'Charter', 'Georgia', serif; -webkit-font-smoothing: antialiased; }
/* ============================================================ */
/* Hero Demo — first-screen live simulator */
/* Pre-boot: black stage. Post-boot: paper-light reveal. */
/* ============================================================ */
.hero-demo {
position: relative;
min-height: 100svh;
padding: 32px 24px 28px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
/* Underlying paper-light surface, revealed once is-booted fades the overlay */
background: linear-gradient(180deg, #fafaf9 0%, #ffffff 60%);
color: #1c1917;
}
/* Black hero overlay covers the whole stage pre-boot, fades on boot */
.hero-demo::before {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 78% 60% at 50% 48%, #15151a 0%, #08080b 58%, #000 100%);
z-index: 0;
pointer-events: none;
transition: opacity 720ms cubic-bezier(0.4, 0, 0.2, 1);
}
.hero-demo::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 50%, transparent 60%, rgba(0,0,0,0.55) 100%);
z-index: 0;
pointer-events: none;
transition: opacity 720ms cubic-bezier(0.4, 0, 0.2, 1);
}
.hero-demo.is-booted::before,
.hero-demo.is-booted::after {
opacity: 0;
}
.hero-demo > * {
position: relative;
z-index: 1;
}
/* Phone shell — pre-boot we hide every trace of the device (bezel, side
buttons, drop shadow, AND the screen + boot-button background) so the
power glyph floats in a pure black void. After boot the shell fades in
to give the iframe a real-device feel on the light bg. */
.hero-demo #demo-frame {
box-shadow: none;
transition: box-shadow 540ms cubic-bezier(0.4, 0, 0.2, 1) 200ms,
background-color 540ms cubic-bezier(0.4, 0, 0.2, 1) 200ms;
}
.hero-demo:not(.is-booted) #demo-frame,
.hero-demo:not(.is-booted) #demo-boot-btn {
background: transparent !important;
}
.hero-demo.is-booted #demo-frame {
box-shadow:
0 0 0 2px #16181c,
0 0 0 3px rgba(255,255,255,0.04),
0 30px 60px -15px rgba(0,0,0,0.5),
0 12px 24px -10px rgba(0,0,0,0.3);
}
/* Phone visual scale based on viewport HEIGHT — phone is 800px tall and
nearly fills 13"/14" laptop viewports, clipping the bottom + crashing
into the SCROLL hint. Scale down visually so it always fits.
• Layout box stays 800px (iframe needs that as its design viewport)
• transform: scale doesn't break iframe coords — browsers remap mouse
events through the transform automatically
• Anchored at "center top" so the bottom shifts up, NOT the top — the
SCROLL hint at the viewport bottom gains room.
• clamp(0.7, …, 1) — scale never above 1 (no upscaling on tall
monitors) and never below 0.7 (phone stays readable)
Formula: (100vh - 200px) / 800px, where 200px is the buffer for brand
above and SCROLL hint below. */
.hero-demo .demo-phone-wrap {
--phone-scale: clamp(0.7, calc((100vh - 100px) / 800px), 1);
transform-origin: center top;
transform: scale(var(--phone-scale));
/* Negative margin-bottom compensates for the phantom layout space below
the phone: scale visually shrinks the phone but the layout box stays
800px tall, so without this the hero (and any surrounding flex/grid
centering) reserves 215px of empty space below the visually-smaller
phone. Pulling the bottom edge up by (1 - scale) * 800px makes the
layout match what the user sees. */
margin-bottom: calc((var(--phone-scale) - 1) * 800px);
transition:
transform 360ms cubic-bezier(0.4, 0, 0.2, 1),
margin-bottom 360ms cubic-bezier(0.4, 0, 0.2, 1);
}
.hero-demo .demo-phone-wrap > div > span {
opacity: 0;
transition: opacity 540ms cubic-bezier(0.4, 0, 0.2, 1) 200ms;
}
.hero-demo.is-booted .demo-phone-wrap > div > span {
opacity: 1;
}
/* Boot button label — uppercase spaced caption echoing the SCROLL hint
typography. Avoids the "loud sentence" feel of the original 16px text. */
.hero-boot-label {
margin-top: 14px;
font: 500 10.5px/1 'Inter', system-ui, sans-serif;
letter-spacing: 0.34em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.42);
transition: color 240ms ease;
}
.hero-demo #demo-boot-btn:hover .hero-boot-label {
color: rgba(255, 255, 255, 0.78);
}
/* Boot button — breathing aura + pulsing ring, anchored to the inner circle
(NOT the button itself: flex centering offsets the button's 50% so a
button-anchored aura would land above the circle). */
.hero-demo #demo-boot-btn > span:first-child {
position: relative;
isolation: isolate;
}
.hero-demo #demo-boot-btn > span:first-child::before {
content: '';
position: absolute;
inset: -22px;
border-radius: 999px;
background:
radial-gradient(circle, rgba(255,255,255,0.12) 0%, rgba(255,255,255,0.03) 45%, transparent 72%);
filter: blur(2px);
animation: hero-aura 3.4s ease-in-out infinite;
pointer-events: none;
z-index: -1;
}
.hero-demo #demo-boot-btn > span:first-child::after {
content: '';
position: absolute;
inset: -2px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.32);
opacity: 0;
animation: hero-ring 2.8s ease-out infinite;
pointer-events: none;
}
.hero-demo #demo-boot-btn:hover > span:first-child::before {
animation-duration: 2.4s;
}
@keyframes hero-aura {
0%, 100% { transform: scale(0.9); opacity: 0.45; }
50% { transform: scale(1.2); opacity: 0.95; }
}
@keyframes hero-ring {
0% { transform: scale(0.94); opacity: 0; }
20% { opacity: 0.55; }
100% { transform: scale(1.6); opacity: 0; }
}
/* State Builder is no longer a hero card — its pre-boot fade is now
handled by .state-dock (vertical pill on phone right edge) and
.state-drawer (fixed right slide-in). See bottom of file. */
/* Hero overlay top bar — brand on the left, resource pills + power-off on
the right. Both groups stay hidden pre-boot (preserving the pure black
void) and fade in once the simulator is started. */
.hero-demo-brand,
.hero-demo-actions {
position: absolute;
top: 22px;
z-index: 3;
opacity: 0;
transform: translateY(-4px);
pointer-events: none;
transition:
opacity 540ms cubic-bezier(0.4, 0, 0.2, 1) 380ms,
transform 540ms cubic-bezier(0.4, 0, 0.2, 1) 380ms;
}
.hero-demo.is-booted .hero-demo-brand,
.hero-demo.is-booted .hero-demo-actions {
opacity: 1;
transform: translateY(0);
}
.hero-demo.is-booted .hero-demo-actions { pointer-events: auto; }
.hero-demo-brand {
left: 32px;
display: flex;
flex-direction: column;
gap: 4px;
}
.hero-demo-brand-name {
font: 700 16px/1 'Inter', system-ui, sans-serif;
letter-spacing: -0.01em;
color: #1c1917;
}
.hero-demo-brand-meta {
font: 500 9.5px/1 'Inter', system-ui, sans-serif;
letter-spacing: 0.24em;
text-transform: uppercase;
color: #a8a29e;
}
.hero-demo-actions {
right: 24px;
display: flex;
align-items: center;
gap: 8px;
}
/* Top-right utility buttons — arXiv wordmark, GitHub mark, Power off.
All borderless to match the same low-key visual weight; brand identity
carries the meaning (arXiv red wordmark, GitHub octocat). Hover reveals
a subtle background tint, matching .hero-demo-poweroff. */
.hero-demo-action {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 10px;
color: #57534e;
background: transparent;
border: 0;
border-radius: 6px;
text-decoration: none;
cursor: pointer;
transition: color 200ms ease, background 200ms ease;
}
.hero-demo-action:hover {
color: #1c1917;
background: rgba(28, 25, 23, 0.04);
}
.hero-demo-arxiv-mark {
font: 800 14px/1 'Inter', system-ui, sans-serif;
letter-spacing: -0.02em;
color: #B31B1B;
transition: color 200ms ease;
}
.hero-demo-action-arxiv:hover .hero-demo-arxiv-mark {
color: #8a1414;
}
.hero-demo-action-arxiv:hover {
background: rgba(179, 27, 27, 0.06);
}
/* Power off — same row as Paper/Code, but lighter (no pill outline),
to keep it as a tertiary action. state-builder.js toggles .hidden /
.inline-flex on click. */
.hero-demo-poweroff {
display: none;
align-items: center;
gap: 5px;
padding: 6px 10px;
font: 500 12px/1 'Inter', system-ui, sans-serif;
letter-spacing: 0;
color: #78716c;
background: transparent;
border: 0;
border-radius: 6px;
cursor: pointer;
transition: color 200ms ease, background 200ms ease;
}
.hero-demo-poweroff.inline-flex { display: inline-flex; }
.hero-demo-poweroff:hover {
color: #1c1917;
background: rgba(28, 25, 23, 0.04);
}
.hero-demo-poweroff svg { opacity: 0.75; transition: opacity 200ms ease; }
.hero-demo-poweroff:hover svg { opacity: 1; }
/* Hero footer — fixed at viewport bottom. Contains the first-load note
and the SCROLL hint, stacked. Only fades in after power-on. */
.hero-demo-foot {
position: fixed;
/* On short viewports the phone (scaled to fit) ends up close to the
viewport bottom — keeping the hint at a fixed 20px gap from the edge
would crash it into the phone. So the hint slides down (closer to the
edge) as the viewport shrinks, maintaining a constant 8px gap above
the phone. Math: phone visual bottom + 8 + hint_height + bottom = vp,
so bottom = vp/2 - 442 in the scaled regime, clamped to [8, 20]. */
bottom: max(env(safe-area-inset-bottom), clamp(8px, calc(50vh - 442px), 20px));
left: 50%;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
z-index: 60;
opacity: 0;
transform: translate(-50%, 14px);
pointer-events: none;
transition:
opacity 540ms cubic-bezier(0.4, 0, 0.2, 1) 600ms,
transform 540ms cubic-bezier(0.4, 0, 0.2, 1) 600ms;
}
.hero-demo.is-booted .hero-demo-foot {
opacity: 1;
transform: translate(-50%, 0);
}
/* Scroll hint is now laid out inside .hero-demo-foot (the footer is
the fixed-positioned container; hint just stacks below the firstload
note in flex order). */
.hero-demo-scroll-hint {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
font: 500 10px/1 'Inter', system-ui, sans-serif;
letter-spacing: 0.3em;
text-transform: uppercase;
color: rgba(28, 25, 23, 0.4);
pointer-events: none;
margin-top: 2px;
}
.hero-demo-scroll-hint svg {
/* Arrow shrunk so the whole hint takes less vertical room — gives more
breathing space above the hint without moving the phone. */
width: 9px;
height: 11px;
animation: hero-arrow 2.4s ease-in-out infinite;
}
@keyframes hero-arrow {
0%, 100% { transform: translateY(0); opacity: 0.55; }
50% { transform: translateY(5px); opacity: 1; }
}
/* Once the user starts scrolling, hide the entire footer so it doesn't
float over the paper content below. */
body.hero-scrolled .hero-demo-foot {
opacity: 0 !important;
pointer-events: none !important;
transform: translate(-50%, 14px) !important;
transition-delay: 0ms !important;
}
@media (max-width: 1040px) {
.hero-demo {
padding: 28px 16px 24px;
}
}
@media (prefers-reduced-motion: reduce) {
.hero-demo::before,
.hero-demo::after,
.hero-demo #demo-boot-btn > span:first-child::before,
.hero-demo #demo-boot-btn > span:first-child::after,
.hero-demo .state-dock,
.state-dock-tab[data-state-dock-tab="session"]::before,
.state-dock-hint,
.state-drawer,
.gesture-guide,
.gesture-guide-icon .gesture-trail,
.hero-demo-foot,
.hero-demo-scroll-hint,
.hero-demo-scroll-hint svg {
transition: none !important;
animation: none !important;
}
.gesture-guide-icon .gesture-trail {
stroke-dashoffset: 0 !important;
opacity: 1 !important;
}
}
/* Paper header resources row — ghost pill style: subtle stone-100 fill +
barely-there 1px border + full rounded. Gives the buttons enough visual
weight to anchor next to the h1 without competing with it. Hover lifts
1px and snaps to white + darker border for clear feedback. */
.paper-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.paper-action {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 16px;
font: 500 14px/1 'Inter', system-ui, sans-serif;
letter-spacing: 0;
color: #44403c; /* stone-700 */
background: rgba(245, 245, 244, 0.6); /* stone-100 @ 60% */
border: 1px solid rgba(28, 25, 23, 0.08);
border-radius: 999px;
text-decoration: none;
cursor: pointer;
transition:
color 200ms ease,
background 200ms ease,
border-color 200ms ease,
transform 200ms ease;
}
.paper-action:hover {
color: #1c1917;
background: #ffffff;
border-color: rgba(28, 25, 23, 0.22);
transform: translateY(-1px);
}
.paper-action-arxiv-mark {
font: 800 15px/1 'Inter', system-ui, sans-serif;
letter-spacing: -0.02em;
color: #B31B1B;
transition: color 200ms ease;
}
.paper-action-arxiv:hover .paper-action-arxiv-mark {
color: #8a1414;
}
.skip-link {
position: absolute;
left: -9999px;
top: 0;
z-index: 1000;
padding: 0.75rem 1.25rem;
background: #1c1917;
color: #fff;
font-family: 'Inter', system-ui, sans-serif;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0 0 0.5rem 0;
text-decoration: none;
}
.skip-link:focus { left: 0; outline: 2px solid #3b82f6; outline-offset: 2px; }
h1, h2, h3, .ui { font-family: 'Inter', system-ui, sans-serif; }
code, pre, .mono { font-family: 'JetBrains Mono', 'SF Mono', monospace; }
.container-narrow { max-width: 880px; }
.container-demo { max-width: 1120px; }
.gradient-bg {
background: linear-gradient(180deg, #fafaf9 0%, #ffffff 60%);
}
.stat-num { font-variant-numeric: tabular-nums; }
details > summary { cursor: pointer; list-style: none; }
details > summary::-webkit-details-marker { display: none; }
details[open] summary .marker { transform: rotate(90deg); }
.marker { transition: transform 0.15s ease; display: inline-block; }
/* ------------------------------------------------------------ */
/* Phone is the only visual anchor in hero. State Builder lives */
/* in a fixed right-side drawer triggered by the dock attached */
/* to the phone's right edge. */
/* ------------------------------------------------------------ */
.demo-layout {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
/* The phone wrap's inner relative box is the dock anchor — the
dock is positioned outside the phone but inside this element
so transform: scale on .demo-phone-wrap scales them together. */
.phone-rig { overflow: visible; }
/* ============================================================ */
/* State Dock — vertical icon dock attached to phone right edge */
/* Glass-morphism pill, fades in post-boot, hides when drawer */
/* is open (drawer overlaps it on common laptop widths). */
/* ============================================================ */
.state-dock {
position: absolute;
/* Sit 14px to the right of the phone, vertically centered. */
left: calc(100% + 14px);
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 14px 7px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.74);
border: 1px solid rgba(28, 25, 23, 0.08);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.7) inset,
0 12px 32px -10px rgba(15, 23, 42, 0.18),
0 4px 12px -6px rgba(15, 23, 42, 0.08);
backdrop-filter: blur(18px) saturate(140%);
-webkit-backdrop-filter: blur(18px) saturate(140%);
z-index: 4;
/* Pre-boot: hidden alongside the rest of the chrome. */
opacity: 0;
pointer-events: none;
transition:
opacity 540ms cubic-bezier(0.4, 0, 0.2, 1) 320ms,
transform 540ms cubic-bezier(0.4, 0, 0.2, 1) 320ms;
}
.hero-demo.is-booted .state-dock {
opacity: 1;
pointer-events: auto;
transform: translateY(-50%);
}
/* When drawer is open, drawer covers the dock's screen real estate
on common 12801600 viewports. Fade dock out so it doesn't poke
through behind the drawer's left shadow. */
body[data-state-drawer-open="true"] .state-dock {
opacity: 0;
pointer-events: none;
transform: translate(8px, -50%);
transition-delay: 0ms;
}
.state-dock-eyebrow {
writing-mode: vertical-rl;
text-orientation: mixed;
/* writing-mode: vertical-rl reads top-to-bottom on the right side
of its container, which is what we want — small caps stacking
downward like an Apple sidebar caption. */
font: 700 8.5px/1 'Inter', system-ui, sans-serif;
letter-spacing: 0.36em;
text-transform: uppercase;
color: rgba(28, 25, 23, 0.45);
padding: 4px 0 2px;
user-select: none;
}
.state-dock-divider {
width: 16px;
height: 1px;
background: rgba(28, 25, 23, 0.12);
margin: 2px 0 4px;
}
.state-dock-tab {
position: relative;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
border-radius: 12px;
background: transparent;
color: #57534e;
cursor: pointer;
transition:
background-color 180ms ease,
color 180ms ease,
transform 180ms cubic-bezier(0.4, 0, 0.2, 1);
}
.state-dock-tab:hover {
background: rgba(28, 25, 23, 0.05);
color: #1c1917;
transform: scale(1.05);
}
.state-dock-tab[aria-selected="true"] {
background: #1c1917;
color: #ffffff;
}
.state-dock-tab[aria-selected="true"] .state-dock-tab-icon-img {
/* Image-based icons don't respond to currentColor — keep them
bright on the dark active pill via a touch of contrast lift. */
filter: brightness(1.05);
}
.state-dock-tab:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.55);
outline-offset: 2px;
}
.state-dock-tab-icon {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
}
.state-dock-tab-icon svg { width: 18px; height: 18px; }
.state-dock-tab-icon-img {
border-radius: 6px;
overflow: hidden;
}
.state-dock-tab-icon-img img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Language tab badge — small pill at the corner of the icon showing
the current phone locale ("中" / "EN") so visitors can read the
active language without opening the drawer. */
.state-dock-tab-badge {
position: absolute;
bottom: -3px;
right: -3px;
min-width: 18px;
height: 14px;
padding: 0 4px;
border-radius: 7px;
background: #2563eb;
color: #ffffff;
font: 700 9px/14px 'Inter', system-ui, sans-serif;
letter-spacing: 0.04em;
text-align: center;
border: 1.5px solid #fafaf9;
pointer-events: none;
user-select: none;
}
.state-dock-tab[aria-selected="true"] .state-dock-tab-badge {
border-color: #1c1917;
}
.studio-language-choices {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.studio-language-choice {
display: flex;
flex-direction: column;
gap: 2px;
padding: 12px;
border-radius: 10px;
border: 1px solid rgba(28, 25, 23, 0.12);
background: #fafaf9;
text-align: left;
cursor: pointer;
transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease;
}
.studio-language-choice:hover {
border-color: rgba(28, 25, 23, 0.25);
background: #ffffff;
}
.studio-language-choice[aria-checked="true"] {
border-color: #2563eb;
background: rgba(37, 99, 235, 0.06);
color: #1e40af;
}
.studio-language-choice:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.55);
outline-offset: 2px;
}
.studio-language-choice-name {
font: 600 13px/1.2 'Inter', system-ui, sans-serif;
}
.studio-language-choice-meta {
font-size: 11px;
color: rgba(28, 25, 23, 0.55);
}
.studio-language-choice[aria-checked="true"] .studio-language-choice-meta {
color: rgba(30, 64, 175, 0.7);
}
/* First-time pulse: the topmost (Session) tab gently pings to draw
the eye. Fades permanently after first interaction (sessionStorage
sets body[data-state-dock-touched="true"]). */
.state-dock-tab[data-state-dock-tab="session"]::before {
content: '';
position: absolute;
inset: -2px;
border-radius: 14px;
border: 1.5px solid rgba(37, 99, 235, 0.7);
opacity: 0;
pointer-events: none;
animation: state-dock-ping 2.4s ease-out 0.6s infinite;
}
.hero-demo:not(.is-booted) .state-dock-tab[data-state-dock-tab="session"]::before,
body[data-state-dock-touched="true"] .state-dock-tab[data-state-dock-tab="session"]::before {
animation: none;
opacity: 0;
}
@keyframes state-dock-ping {
0% { transform: scale(0.92); opacity: 0; }
20% { opacity: 0.85; }
80% { transform: scale(1.45); opacity: 0; }
100% { transform: scale(1.45); opacity: 0; }
}
/* Callout below the dock pointing UP at it — only visible until
first interaction. Reinforces "this is clickable" without
relying solely on the pulse. */
.state-dock-hint {
position: absolute;
/* Anchor under the phone right edge, slightly indented so it
reads as belonging to the dock column. */
left: calc(100% + 6px);
top: calc(100% + 6px);
width: 80px;
text-align: left;
font: 600 10.5px/1.3 'Inter', system-ui, sans-serif;
letter-spacing: 0.04em;
color: rgba(37, 99, 235, 0.82);
white-space: normal;
opacity: 0;
pointer-events: none;
transition: opacity 540ms ease 700ms;
animation: state-dock-hint-bob 2.4s ease-in-out 1.6s infinite;
}
.state-dock-hint::before {
content: '↑';
display: inline-block;
margin-right: 4px;
font-size: 12px;
vertical-align: -1px;
}
.hero-demo.is-booted .state-dock-hint {
opacity: 1;
}
body[data-state-dock-touched="true"] .state-dock-hint,
body[data-state-drawer-open="true"] .state-dock-hint {
opacity: 0;
animation: none;
}
@keyframes state-dock-hint-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
/* ============================================================ */
/* Gesture Guide — left-side companion to .state-dock. Static */
/* legend explaining the three full-screen gestures the */
/* simulator listens for (edge-swipe back / swipe-up home / */
/* swipe-up-and-hold recents). Same glass-morphism + fade-in */
/* timing as the dock so they read as a matched pair. */
/* ============================================================ */
.gesture-guide {
position: absolute;
/* Mirror of .state-dock: 14px outside the phone's left edge. */
right: calc(100% + 14px);
top: 50%;
transform: translateY(-50%);
width: 168px;
padding: 14px 14px 13px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.74);
border: 1px solid rgba(28, 25, 23, 0.08);
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.7) inset,
0 12px 32px -10px rgba(15, 23, 42, 0.18),
0 4px 12px -6px rgba(15, 23, 42, 0.08);
backdrop-filter: blur(18px) saturate(140%);
-webkit-backdrop-filter: blur(18px) saturate(140%);
z-index: 4;
opacity: 0;
/* Buttons inside (.gesture-guide-icon) need clicks. The container
starts hidden pre-boot — pointer-events is gated by .is-booted
below so the buttons don't catch ghost clicks while invisible. */
pointer-events: none;
transition:
opacity 540ms cubic-bezier(0.4, 0, 0.2, 1) 320ms,
transform 540ms cubic-bezier(0.4, 0, 0.2, 1) 320ms;
}
.hero-demo.is-booted .gesture-guide {
opacity: 1;
pointer-events: auto;
transform: translateY(-50%);
}
.gesture-guide-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
padding: 0 2px 9px;
border-bottom: 1px solid rgba(28, 25, 23, 0.07);
margin-bottom: 9px;
}
.gesture-guide-eyebrow {
font: 700 9.5px/1 'Inter', system-ui, sans-serif;
letter-spacing: 0.32em;
text-transform: uppercase;
color: #1c1917;
}
.gesture-guide-meta {
font: 600 8.5px/1 'Inter', system-ui, sans-serif;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #2563eb;
}
.gesture-guide-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.gesture-guide-list > li { display: block; }
/* Each row is now a real <button> — clicking anywhere on the row
(icon + label) fires the corresponding hardware-key action via
scripts/boot-hero.js. Reset native chrome, lay out like a flex
row with the icon on the left. */
.gesture-guide-item {
display: flex;
align-items: center;
gap: 9px;
width: 100%;
padding: 5px 6px;
border: 0;
border-radius: 10px;
background: transparent;
color: inherit;
cursor: pointer;
text-align: left;
font: inherit;
-webkit-appearance: none;
appearance: none;
transition:
background-color 160ms ease,
transform 120ms cubic-bezier(0.4, 0, 0.2, 1);
}
.gesture-guide-item:hover {
background: rgba(28, 25, 23, 0.06);
}
.gesture-guide-item:hover .gesture-guide-icon {
border-color: rgba(28, 25, 23, 0.28);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.95),
0 2px 6px rgba(15, 23, 42, 0.1);
}
.gesture-guide-item:hover .gesture-guide-label {
color: #1c1917;
}
.gesture-guide-item:active {
background: rgba(28, 25, 23, 0.12);
transform: scale(0.98);
transition-duration: 60ms;
}
.gesture-guide-item:active .gesture-guide-icon {
background: #1c1917;
color: #ffffff;
border-color: #1c1917;
box-shadow:
inset 0 1px 2px rgba(0, 0, 0, 0.45),
0 0 0 transparent;
}
.gesture-guide-item:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.55);
outline-offset: 1px;
}
/* Hint-only variant: not a hardware-key button, just a visual reminder
that the desktop pager responds to horizontal swipe/drag. Suppress
pointer-affordances (cursor, hover/active background) so it reads as
informational text rather than an interactive control. */
.gesture-guide-item--hint {
cursor: default;
}
.gesture-guide-item--hint:hover,
.gesture-guide-item--hint:active {
background: transparent;
transform: none;
}
.gesture-guide-item--hint:hover .gesture-guide-icon,
.gesture-guide-item--hint:active .gesture-guide-icon {
background: #ffffff;
color: #1c1917;
border-color: rgba(28, 25, 23, 0.14);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.95),
0 1px 2px rgba(15, 23, 42, 0.06);
}
.gesture-guide-item--hint:hover .gesture-guide-label {
color: #1c1917;
}
.gesture-guide-icon {
flex: 0 0 auto;
width: 34px;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 9px;
/* Solid fill + 1px stone border + soft top highlight reads as a
physical key, not a tag/badge. Matches the existing app icons in
the simulator below. */
background: #ffffff;
border: 1px solid rgba(28, 25, 23, 0.14);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.95),
0 1px 2px rgba(15, 23, 42, 0.06);
color: #1c1917;
transition: background-color 160ms ease, color 160ms ease,
border-color 160ms ease, box-shadow 160ms ease;
}
.gesture-guide-icon svg {
width: 24px;
height: 24px;
display: block;
}
.gesture-guide-text {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.gesture-guide-label {
font: 600 11.5px/1.2 'Inter', system-ui, sans-serif;
color: #1c1917;
transition: color 160ms ease;
}
.gesture-guide-desc {
font: 400 10.5px/1.35 'Inter', system-ui, sans-serif;
color: #78716c;
}
/* Stroke trails inside each gesture icon — repeatedly "draw" the
swipe path so the SVG reads as motion, not a static glyph. The
three items are staggered so the page doesn't pulse in unison. */
.gesture-guide-icon .gesture-trail {
stroke-dasharray: 22;
stroke-dashoffset: 22;
animation: gesture-trail 2.6s cubic-bezier(0.55, 0, 0.25, 1) infinite;
}
.gesture-guide-item:nth-child(1) .gesture-trail { animation-delay: 0ms; }
.gesture-guide-item:nth-child(2) .gesture-trail { animation-delay: 700ms; }
.gesture-guide-item:nth-child(3) .gesture-trail { animation-delay: 1400ms; }
.gesture-guide-item:nth-child(4) .gesture-trail { animation-delay: 2100ms; }
@keyframes gesture-trail {
0% { stroke-dashoffset: 22; opacity: 0; }
18% { opacity: 1; }
55% { stroke-dashoffset: 0; opacity: 1; }
80% { stroke-dashoffset: 0; opacity: 0.95; }
100% { stroke-dashoffset: 0; opacity: 0; }
}
/* Narrow-viewport responsive rules for .gesture-guide are appended
AFTER the existing .state-dock @media block further down — see
the trailing @media block at the bottom of this file. CSS cascade
only honors the last matching rule, and the dock block re-sets
.state-dock { top: ... } in narrow mode, so the gesture-guide
override that pushes the dock down has to live after it. */
/* ============================================================ */
/* State Popover — anchored card to the right of the dock. No */
/* full-screen drawer, no backdrop scrim. Slides in 10px from */
/* the dock with a fade. Closes on outside click or ESC. */
/* */
/* Position math (relative to .demo-layout, which is centered): */
/* left = 50% + (phone half 180) + (phone↔dock gap 14) */
/* + (dock width ≈50) + (dock↔popover gap 14) */
/* = 50% + 258px */
/* ============================================================ */
.state-drawer {
position: absolute;
left: calc(50% + 258px);
top: 0;
width: 380px;
height: 800px; /* matches phone box height for visual alignment */
max-height: calc(100svh - 80px);
background: #fbfbfd;
border: 1px solid rgba(28, 25, 23, 0.06);
border-radius: 28px;
box-shadow:
0 28px 60px -22px rgba(15, 23, 42, 0.22),
0 10px 18px -8px rgba(15, 23, 42, 0.10);
opacity: 0;
transform: translateX(-10px);
pointer-events: none;
z-index: 5;
display: flex;
flex-direction: column;
overflow: hidden;
transition:
opacity 280ms cubic-bezier(0.22, 0.61, 0.36, 1),
transform 320ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.state-drawer[data-open="true"] {
opacity: 1;
transform: translateX(0);
pointer-events: auto;
}
/* Below 1280px the side-anchored popover would overflow the right
edge AND the vertical dock would visually disconnect from where
the popover lands. Drop the entire group into a column flow:
phone → horizontal dock (below phone) → popover (below dock).
This keeps the click target and the panel that opens on a single
visual axis. */
@media (max-width: 1279px) {
.hero-demo.is-booted {
padding-top: 76px;
}
.hero-demo .demo-phone-wrap {
--phone-scale: clamp(0.62, calc((100svh - 160px) / 876px), 1);
margin-bottom: calc((var(--phone-scale) - 1) * 876px);
}
.demo-layout {
flex-direction: column;
gap: 24px;
}
/* Reserve space below the visual phone for the now-horizontal
dock (which is still position: absolute on .phone-rig at
top: 100% + 14px). Without this padding the next column-flow
item (popover) would render on top of the dock. */
.demo-phone-wrap {
padding-bottom: 76px;
}
.state-drawer {
position: relative;
left: auto;
top: auto;
width: min(420px, calc(100vw - 32px));
height: auto;
max-height: 70svh;
transform: translateY(-8px);
}
.state-drawer[data-open="true"] {
transform: translateY(0);
}
.state-drawer[data-open="false"] {
display: none;
}
}
.state-drawer-head {
position: relative;
padding: 20px 22px 14px;
border-bottom: 1px solid rgba(28, 25, 23, 0.06);
display: flex;
align-items: flex-start;
gap: 14px;
}
.state-drawer-head .state-builder-board-kicker {
margin-bottom: 5px;
}
.state-drawer-head .state-builder-board-title {
font-size: 17px;
line-height: 1.3;
margin-bottom: 4px;
}
.state-drawer-head .state-builder-board-lede {
font-size: 12.5px;
line-height: 1.45;
}
.state-drawer-close {
flex: 0 0 auto;
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: 0;
background: rgba(28, 25, 23, 0.04);
color: #44403c;
border-radius: 999px;
cursor: pointer;
transition: background-color 180ms ease, color 180ms ease, transform 180ms ease;
}
.state-drawer-close:hover {
background: rgba(28, 25, 23, 0.1);
color: #1c1917;
transform: rotate(90deg);
}
.state-drawer-close:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.55);
outline-offset: 2px;
}
.state-drawer-body {
flex: 1 1 auto;
overflow-y: auto;
padding: 14px 22px 22px;
-webkit-overflow-scrolling: touch;
}
.state-drawer-body .state-builder-board {
padding-top: 0;
}
.state-drawer-body .state-builder-board > .state-builder-board-head {
/* Kicker/title/lede already render in the drawer head, hide the
duplicate inside the embedded board (legacy markup keeps it
for the standalone fallback). */
display: none;
}
/* Narrow viewports (matches popover breakpoint): dock collapses to
a horizontal pill below the phone, so dock and popover always
travel together along the same vertical axis. */
@media (max-width: 1279px) {
.state-dock {
left: 50%;
top: calc(100% + 14px);
transform: translate(-50%, 0);
flex-direction: row;
padding: 7px 14px;
gap: 6px;
}
.hero-demo.is-booted .state-dock {
transform: translate(-50%, 0);
}
body[data-state-drawer-open="true"] .state-dock {
/* Keep dock visible under the popover when open — popover
stacks below it in the column flow, not over it. */
opacity: 1;
pointer-events: auto;
transform: translate(-50%, 0);
}
.state-dock-eyebrow {
writing-mode: horizontal-tb;
padding: 0 6px 0 2px;
letter-spacing: 0.28em;
}
.state-dock-divider {
width: 1px;
height: 18px;
margin: 0 4px;
}
.state-dock-tab {
width: 34px;
height: 34px;
}
.state-dock-tab-icon { width: 20px; height: 20px; }
.state-dock-tab-icon svg { width: 16px; height: 16px; }
.state-dock-hint {
/* Sits to the right of the horizontal dock, slightly below to
avoid optical collision with the eyebrow text. */
left: 50%;
top: calc(100% + 76px);
transform: translate(-50%, 0);
width: auto;
text-align: center;
}
.state-dock-hint::before {
/* Arrow now points up at the dock above */
content: '↑';
}
}
/* ----- Capability board (collapsed state) ----- */
.state-builder-board {
display: flex;
flex-direction: column;
gap: 10px;
padding: 6px 4px 0;
}
.state-builder-board-head {
display: flex;
flex-direction: column;
gap: 8px;
}
.state-builder-board-kicker {
color: #2563eb;
font: 800 10px/1 Inter, system-ui, sans-serif;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.state-builder-board-title {
margin: 0;
font: 700 18px/1.3 Inter, system-ui, sans-serif;
color: #1c1917;
}
.state-builder-board-lede {
margin: 0;
font: 400 13.5px/1.55 'Charter', 'Georgia', serif;
color: #57534e;
}
.state-builder-tabs {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 2px;
padding: 4px 0 0;
}
.state-builder-tab {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 4px 2px 6px;
border: 0;
background: transparent;
cursor: pointer;
color: #78716c;
font: 600 10.5px/1.25 Inter, system-ui, sans-serif;
text-align: center;
transition: color 0.14s ease;
}
.state-builder-tab:hover { color: #1c1917; }
.state-builder-tab[aria-selected="true"] {
color: #1c1917;
font-weight: 700;
}
.state-builder-tab[aria-selected="true"] .state-builder-tab-icon {
box-shadow: 0 0 0 1.5px #2563eb;
}
.state-builder-tab:focus-visible {
outline: 2px solid rgba(37, 99, 235, 0.45);
outline-offset: 2px;
border-radius: 4px;
}
.state-builder-tab-icon {
width: 30px;
height: 30px;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: #f5f5f4;
color: #57534e;
overflow: hidden;
}
.state-builder-tab-icon svg { width: 17px; height: 17px; }
.state-builder-tab-icon-img { background: transparent; }
.state-builder-tab-icon-img img { width: 100%; height: 100%; object-fit: cover; }
.state-builder-tab-label { display: block; }
.state-builder-panel-shell {
padding: 4px 0 0;
}
.state-builder-board-foot {
margin: 8px 4px 0;
font: 400 11.5px/1.5 'Charter', 'Georgia', serif;
color: #a8a29e;
}
.state-builder-board-foot code {
font: 500 11px/1.5 'JetBrains Mono', 'SF Mono', monospace;
color: #78716c;
background: transparent;
padding: 0;
}
.studio-action-rail {
display: flex;
gap: 8px;
margin: 9px 0 16px;
overflow-x: auto;
scrollbar-width: none;
}
.studio-action-rail::-webkit-scrollbar { display: none; }
.studio-action-tab {
flex: 0 0 auto;
border: 0;
border-bottom: 2px solid transparent;
border-radius: 0;
background: transparent;
padding: 4px 0;
margin-right: 16px;
color: #78716c;
font: 600 12px/1 Inter, system-ui, sans-serif;
cursor: pointer;
transition: color 0.14s ease, border-color 0.14s ease;
}
.studio-action-tab:hover {
color: #1c1917;
}
.studio-action-tab[aria-selected="true"] {
color: #1c1917;
border-bottom-color: #2563eb;
}
.studio-action-panel { display: none; }
.studio-action-panel[data-active="true"] { display: block; }
#studio-device-location-custom[hidden] { display: none !important; }
.studio-panel { display: none; }
.studio-panel[data-active="true"] {
display: block;
animation: studio-panel-in 0.18s ease-out;
}
.state-builder-panel-shell .studio-action-rail {
margin: 0 0 14px;
}
@keyframes studio-panel-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.studio-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.studio-field label {
font-size: 11px;
line-height: 1.2;
font-weight: 600;
color: #78716c;
}
.studio-input {
width: 100%;
border: 1px solid rgba(28,25,23,0.06);
border-radius: 9px;
background: transparent;
padding: 10px 12px;
font: 600 13px/1.35 Inter, system-ui, sans-serif;
color: #1c1917;
outline: none;
box-shadow: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
select.studio-input {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7 10L12 15L17 10' stroke='%2378716c' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-position: right 11px center;
background-repeat: no-repeat;
background-size: 18px 18px;
padding-right: 34px;
}
input[type="date"].studio-input,
input[type="datetime-local"].studio-input {
color-scheme: light;
}
textarea.studio-input {
line-height: 1.5;
}
.studio-input:focus {
border-color: rgba(37,99,235,0.34);
background: transparent;
box-shadow:
0 0 0 2px rgba(37,99,235,0.05);
}
.studio-input:hover:not(:focus) {
border-color: rgba(28,25,23,0.12);
box-shadow: none;
}
.studio-action {
border-radius: 8px;
background: #44403c;
color: #f5f5f4;
border: 0;
padding: 9px 14px;
font: 600 12.5px/1 Inter, system-ui, sans-serif;
box-shadow: none;
transition: background 0.14s ease;
}
.studio-action:hover {
background: #57534e;
box-shadow: none;
transform: none;
}
.studio-action:disabled { background: #a8a29e; transform: none; cursor: not-allowed; }
.studio-action.secondary {
background: rgba(255,255,255,0.72);
color: #1c1917;
border: 1px solid rgba(214,211,209,0.88);
box-shadow: none;
}
.studio-action.secondary:hover {
background: #fff;
border-color: #a8a29e;
}
.studio-toggle {
display: flex;
align-items: center;
gap: 8px;
font: 600 12px/1.2 Inter, system-ui, sans-serif;
color: #44403c;
}
.avatar-picker {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 6px;
}
.avatar-choice {
aspect-ratio: 1;
border-radius: 11px;
border: 2px solid transparent;
padding: 2px;
background: rgba(245,245,244,0.72);
transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.avatar-choice:hover {
transform: translateY(-1px);
box-shadow: 0 8px 18px -14px rgba(28,25,23,0.7);
}
.avatar-choice[aria-pressed="true"] {
border-color: #2563eb;
background: #eff6ff;
}
.avatar-choice img {
display: block;
width: 100%;
height: 100%;
border-radius: 9px;
object-fit: cover;
}
.studio-status {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 0 4px;
margin-top: 4px;
font: 400 12px/1.5 'Charter', 'Georgia', serif;
}
.studio-status::before {
content: '';
width: 6px;
height: 6px;
flex: 0 0 auto;
border-radius: 999px;
background: currentColor;
margin-top: 6px;
}
.studio-status[data-kind="ok"] { color: #047857; }
.studio-status[data-kind="warn"] { color: #a16207; }
.studio-status[data-kind="error"] { color: #b91c1c; }
.studio-snapshot-meta {
border-radius: 10px;
background: rgba(245,245,244,0.7);
padding: 9px 11px;
color: #78716c;
font: 700 10px/1.45 Inter, system-ui, sans-serif;
}
/* ============================================================ */
/* Reddit case-study trajectory viewer */
/* ============================================================ */
.trace-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; max-width: 640px; margin: 0 auto; }
@media (max-width: 600px) { .trace-grid { grid-template-columns: 1fr; gap: 1rem; } }
.trace-side { display: flex; flex-direction: column; gap: 0.5rem; }
.trace-header {
display: flex; align-items: center; justify-content: flex-start;
padding: 0 2px 6px;
}
.trace-badge {
padding: 3px 8px; border-radius: 4px;
font-family: 'Inter', system-ui, sans-serif;
font-size: 9px; font-weight: 800; letter-spacing: 0.1em;
text-transform: uppercase;
}
.trace-badge-base { background: #1c1917; color: #f5f5f4; }
.trace-badge-trained { background: #047857; color: #fff; }
.trace-image-wrap {
position: relative; border-radius: 10px; overflow: hidden;
background: #f5f5f4; aspect-ratio: 540 / 1150;
}
.trace-image-wrap img { width: 100%; height: 100%; display: block; object-fit: cover; }
.trace-image-wrap.is-base { border: 1px solid #e7e5e4; }
.trace-image-wrap.is-trained { border: 1px solid #a7f3d0; background: #ecfdf5; }
.trace-controls { display: flex; flex-direction: column; gap: 0.5rem; padding: 0 2px; }
.trace-slider {
-webkit-appearance: none; appearance: none;
width: 100%; height: 4px; border-radius: 2px;
background: #e7e5e4; outline: none; cursor: pointer;
margin: 0;
}
.trace-slider::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 16px; height: 16px; border-radius: 50%;
background: #fff; border: 2px solid #44403c;
cursor: pointer; transition: transform .15s ease;
}
.trace-slider::-moz-range-thumb {
width: 16px; height: 16px; border-radius: 50%;
background: #fff; border: 2px solid #44403c;
cursor: pointer; transition: transform .15s ease;
}
.trace-slider.is-trained::-webkit-slider-thumb { border-color: #047857; }
.trace-slider.is-trained::-moz-range-thumb { border-color: #047857; }
.trace-slider:hover::-webkit-slider-thumb { transform: scale(1.15); }
.trace-slider:hover::-moz-range-thumb { transform: scale(1.15); }
.trace-ticks {
display: flex; justify-content: space-between; align-items: center;
padding: 0 7px;
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-size: 9px; color: #a8a29e;
font-variant-numeric: tabular-nums;
}
.trace-tick { display: flex; flex-direction: column; align-items: center; flex: 0 0 auto; }
.trace-tick::before {
content: ''; width: 1px; height: 5px; background: #d6d3d1; margin-bottom: 2px;
}
.trace-action {
font-family: 'Inter', system-ui, sans-serif;
font-size: 11px; line-height: 1.35; color: #44403c;
min-height: 2.7em; padding: 2px 2px 0;
}
.trace-action .step-label {
font-family: 'JetBrains Mono', 'SF Mono', monospace;
font-weight: 700; font-size: 10px;
margin-right: 6px;
color: #1c1917;
}
.trace-action.is-trained .step-label { color: #047857; }
.app-tile { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; user-select: none; }
.app-tile .app-icon {
width: 60px; height: 60px;
display: flex; align-items: center; justify-content: center;
transition: transform .18s ease, filter .18s ease;
position: relative;
}
.app-tile .app-icon img {
width: 100%; height: 100%;
filter: drop-shadow(0 1px 2px rgba(0,0,0,.08)) drop-shadow(0 2px 4px rgba(0,0,0,.05));
}
.app-tile .app-icon.fallback {
border-radius: 14px;
box-shadow: 0 1px 2px rgba(0,0,0,.08), 0 2px 6px -2px rgba(0,0,0,.08);
font-family: 'Inter', system-ui, sans-serif;
font-weight: 600;
overflow: visible;
}
.app-tile .app-icon.fallback.is-light {
box-shadow: 0 0 0 1px rgba(0,0,0,.05), 0 1px 2px rgba(0,0,0,.06), 0 2px 6px -2px rgba(0,0,0,.06);
}
.app-tile:hover .app-icon { transform: translateY(-2px) scale(1.06); }
.app-tile:hover .app-icon img {
filter: drop-shadow(0 4px 10px rgba(0,0,0,.18)) drop-shadow(0 2px 4px rgba(0,0,0,.08));
}
.app-tile .app-name {
font-size: 11px; line-height: 1.15; text-align: center;
color: #57534e; transition: color .15s ease;
font-family: 'Inter', system-ui, sans-serif;
}
.app-tile:hover .app-name { color: #1c1917; }
.app-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 0.5rem;
row-gap: 1.5rem;
}
@media (min-width: 640px) { .app-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
@media (min-width: 768px) { .app-grid { grid-template-columns: repeat(7, minmax(0, 1fr)); } }
.app-section-label {
display: flex; align-items: baseline; gap: 0.625rem; margin-bottom: 1.25rem;
font-family: 'Inter', system-ui, sans-serif;
}
.app-section-label .bar { width: 3px; height: 14px; border-radius: 2px; align-self: center; }
.app-section-label h3 {
font-size: 11px; font-weight: 700; letter-spacing: 0.22em;
text-transform: uppercase; color: #44403c;
}
.app-section-label .count { font-size: 11px; color: #a8a29e; font-weight: 500; }
/* ============================================================ */
/* Apps Wall — auto-scrolling marquee */
/* ============================================================ */
.apps-marquee-row { margin-bottom: 28px; }
.apps-marquee-row:last-of-type { margin-bottom: 0; }
.apps-marquee-meta {
display: flex; align-items: center; gap: 10px;
margin: 0 0 14px;
font-family: 'Inter', system-ui, sans-serif;
}
.apps-marquee-meta .bar {
width: 3px; height: 14px; border-radius: 2px;
flex: 0 0 auto;
}
.apps-marquee-meta-label {
font-size: 11px; font-weight: 700;
letter-spacing: 0.22em; text-transform: uppercase;
color: #44403c;
}
.apps-marquee-meta-count {
font-size: 11px; font-weight: 500; color: #a8a29e;
}
.apps-marquee {
position: relative;
overflow: hidden;
padding: 4px 0 6px;
-webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 6%, #000 94%, transparent 100%);
mask-image: linear-gradient(90deg, transparent 0%, #000 6%, #000 94%, transparent 100%);
}
.apps-marquee-track {
display: flex;
gap: 28px;
width: max-content;
animation: apps-scroll-left 30s linear infinite;
will-change: transform;
}
.apps-marquee-track.is-reverse {
animation-name: apps-scroll-right;
animation-duration: 30s;
}
.apps-marquee:hover .apps-marquee-track,
.apps-marquee:focus-within .apps-marquee-track {
animation-play-state: paused;
}
@keyframes apps-scroll-left {
from { transform: translate3d(0, 0, 0); }
to { transform: translate3d(-50%, 0, 0); }
}
@keyframes apps-scroll-right {
from { transform: translate3d(-50%, 0, 0); }
to { transform: translate3d(0, 0, 0); }
}
.apps-marquee .app-tile {
flex: 0 0 auto;
width: 84px;
}
@media (prefers-reduced-motion: reduce) {
.apps-marquee {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-webkit-mask-image: none;
mask-image: none;
}
.apps-marquee-track {
animation: none !important;
transform: none !important;
}
}
/* ============================================================ */
/* Gesture Guide — narrow-viewport collapse. */
/* Placed at end-of-file so it cascades AFTER the .state-dock */
/* @media block above (which re-sets dock/hint positions). On */
/* narrow viewports the left-side legend collapses into an */
/* icon-only horizontal pill directly under the phone — Android */
/* soft-key bar — and pushes State Dock + hint further down. */
/* ============================================================ */
@media (max-width: 1279px) {
.gesture-guide {
position: absolute;
left: 50%;
right: auto;
top: calc(100% + 14px);
transform: translate(-50%, 0);
width: auto;
padding: 6px 12px;
border-radius: 999px;
}
.hero-demo.is-booted .gesture-guide {
transform: translate(-50%, 0);
}
/* Drop all explanatory text — only the three keys remain visible. */
.gesture-guide-head,
.gesture-guide-text {
display: none;
}
.gesture-guide-list {
flex-direction: row;
gap: 6px;
}
.gesture-guide-item {
padding: 2px;
border-radius: 999px;
}
/* Row-level hover/active background made sense when the row also
held a text label; in icon-only mode the key itself carries all
the affordance, so the surrounding pill stays transparent. */
.gesture-guide-item:hover,
.gesture-guide-item:active {
background: transparent;
}
/* Push the State Dock and its "Patch state" hint below the new
gesture-key row so they don't collide. Phone wrap reserves the
combined height — without it the popover (which renders in
column flow below) would overlap the dock. */
.state-dock {
top: calc(100% + 76px);
}
.state-dock-hint {
top: calc(100% + 138px);
}
.demo-phone-wrap {
padding-bottom: 152px;
}
}