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 1280–1600 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