diff --git a/frontend/src/pages/LandingPage.css b/frontend/src/pages/LandingPage.css index 239e868..1e52b4e 100644 --- a/frontend/src/pages/LandingPage.css +++ b/frontend/src/pages/LandingPage.css @@ -361,9 +361,6 @@ gap: 28px; align-items: start; } .lp-mode { margin: 0; } -.lp-mode figcaption { margin-bottom: 14px; } -.lp-mode figcaption strong { display: block; font-size: 1.05rem; font-weight: 700; } -.lp-mode figcaption span { display: block; margin-top: 4px; font-size: .88rem; line-height: 1.5; color: var(--text-muted); } .lp-screen { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; @@ -542,3 +539,237 @@ @media (min-width: 1100px) { .lp-modes { grid-template-columns: repeat(3, minmax(0, 1fr)); } } + + +/* ── The deck studio ────────────────────────────────────────────────── + A slide building itself, and the deck beside it filling up. No prose: the + third thing this site does is make a talk out of the same evidence, and a + picture of that lands before a sentence about it would have been read. + + The colours here are a deliberate exception to the tokens-only rule at the + top of this file, in the same way the mimed question screens are: these are + pictures of slides, and a slide with no colour of its own is not a picture + of a slide. They are declared once, as local properties, and every one of + them is mixed out of --primary so the warm theme stays warm. + + Timing. One 21s loop shared by everything. Each slide is a third of a turn + behind the last, done with a negative delay (`--i * -14s`), so the three + take the stage in order with no clock in JavaScript. Every part inside a + slide carries the same delay plus its own `--d`, and that — not a separate + keyframe per part — is what staggers the build. */ +.lp-studio { + --sl-ink: #1e293b; + --sl-paper: #ffffff; + --sl-accent: var(--primary); + --sl-tint: color-mix(in srgb, var(--primary) 12%, #ffffff); + --sl-tint-2: color-mix(in srgb, var(--primary) 26%, #ffffff); + --sl-quiet: color-mix(in srgb, var(--primary) 16%, #94a3b8); + --sl-warm: color-mix(in srgb, var(--primary) 55%, #f0a04b); + display: grid; grid-template-columns: minmax(0, 1fr) 168px; gap: 22px; + align-items: stretch; max-width: 880px; margin: 0 auto; +} + +.lp-stage { margin: 0; min-width: 0; } + +/* 16:9, because that is the shape people recognise before they read + anything on it. */ +.lp-deck { + position: relative; aspect-ratio: 16 / 9; overflow: hidden; + border: 1px solid var(--border); border-radius: 14px; background: var(--sl-paper); + box-shadow: 0 20px 48px rgba(15, 23, 42, .16); +} + +.lp-slide { + position: absolute; inset: 0; padding: 7% 8%; + display: flex; flex-direction: column; gap: 2.4%; + background: var(--sl-paper); opacity: 0; +} +/* Calm motion, or no motion preference: the three sit side by side, finished. + Nothing is lost — a deck is a thing you look at, not a thing you watch. */ +.lp-studio.is-calm .lp-deck { aspect-ratio: auto; display: grid; gap: 10px; padding: 10px; } +.lp-studio.is-calm .lp-slide { position: static; opacity: 1; aspect-ratio: 16 / 9; border: 1px solid var(--border); border-radius: 8px; } + +/* Slide 1 — the opener. */ +.lp-slide.is-title { justify-content: center; padding-left: 11%; } +.lp-sl-wash { + position: absolute; inset: 0; opacity: 0; + background: + radial-gradient(120% 90% at 8% 110%, var(--sl-tint-2), transparent 62%), + radial-gradient(90% 80% at 100% 0%, color-mix(in srgb, var(--sl-warm) 34%, #ffffff), transparent 58%), + var(--sl-paper); +} +.lp-sl-mark { + position: absolute; top: 9%; right: 8%; width: 26px; height: 26px; border-radius: 50%; + background: var(--sl-accent); opacity: 0; + box-shadow: 0 0 0 6px color-mix(in srgb, var(--sl-accent) 16%, transparent); +} +.lp-sl-rule { width: 52px; height: 5px; border-radius: 3px; background: var(--sl-accent); opacity: 0; } +.lp-sl-h1 { width: 74%; height: 15%; border-radius: 6px; background: var(--sl-ink); opacity: 0; } +.lp-sl-h2 { width: 46%; height: 8%; border-radius: 5px; background: var(--sl-quiet); opacity: 0; } +.lp-sl-foot { position: absolute; left: 11%; bottom: 8%; width: 30%; height: 5px; border-radius: 3px; background: var(--sl-tint-2); opacity: 0; } + +/* Shared furniture on slides 2 and 3. */ +.lp-sl-eyebrow { width: 16%; height: 5px; border-radius: 3px; background: var(--sl-accent); opacity: 0; } +.lp-sl-title { width: 58%; height: 9%; border-radius: 5px; background: var(--sl-ink); opacity: 0; } +.lp-sl-cite { margin-top: auto; width: 34%; height: 4px; border-radius: 2px; background: var(--sl-tint-2); opacity: 0; } + +/* Slide 2 — the figure. */ +.lp-sl-cols { display: grid; grid-template-columns: 1fr 1.15fr; gap: 5%; flex: 1; min-height: 0; margin-top: 2%; } +.lp-sl-copy { display: flex; flex-direction: column; justify-content: center; gap: 11%; } +.lp-sl-copy span { display: block; height: 5px; border-radius: 3px; background: var(--sl-quiet); opacity: 0; } +.lp-sl-copy span:nth-child(1) { width: 100%; } +.lp-sl-copy span:nth-child(2) { width: 92%; } +.lp-sl-copy span:nth-child(3) { width: 96%; } +.lp-sl-copy span:nth-child(4) { width: 64%; } +.lp-sl-figure { + border-radius: 8px; overflow: hidden; opacity: 0; + background: linear-gradient(150deg, var(--sl-tint), color-mix(in srgb, var(--sl-warm) 20%, #ffffff)); +} +.lp-sl-figure svg { display: block; width: 100%; height: 100%; } +.lp-sl-band { fill: var(--sl-accent); opacity: .16; } +.lp-sl-curve { fill: none; stroke: var(--sl-accent); stroke-width: 3; stroke-linecap: round; + stroke-dasharray: 100; stroke-dashoffset: 100; } +.lp-sl-dot { fill: var(--sl-accent); opacity: 0; } + +/* Slide 3 — the numbers. */ +.lp-sl-data { display: grid; grid-template-columns: 1fr 86px; gap: 6%; align-items: end; flex: 1; min-height: 0; margin-top: 3%; } +.lp-sl-bars { list-style: none; margin: 0; padding: 0 0 6px; height: 100%; + display: flex; align-items: flex-end; gap: 7%; border-bottom: 2px solid var(--sl-tint-2); } +.lp-sl-bars li { + flex: 1; height: 0; border-radius: 5px 5px 0 0; + background: linear-gradient(180deg, var(--sl-accent), color-mix(in srgb, var(--sl-accent) 55%, #ffffff)); +} +.lp-sl-bars li:nth-child(4) { background: linear-gradient(180deg, var(--sl-warm), color-mix(in srgb, var(--sl-warm) 50%, #ffffff)); } +.lp-sl-ring { align-self: center; justify-self: end; width: 86px; opacity: 0; } +.lp-sl-ring svg { display: block; width: 100%; height: auto; transform: rotate(-90deg); } +.lp-sl-ring-track { fill: none; stroke: var(--sl-tint); stroke-width: 7; } +.lp-sl-ring-fill { fill: none; stroke: var(--sl-accent); stroke-width: 7; stroke-linecap: round; + stroke-dasharray: 100; stroke-dashoffset: 100; } + +/* A single pass of light once a slide is standing. It is the only part of + this that is purely decorative, and it is why the thing reads as finished + rather than as still loading. */ +.lp-sl-sheen { + position: absolute; inset: -40% -60%; pointer-events: none; opacity: 0; + background: linear-gradient(72deg, transparent 42%, rgba(255,255,255,.55) 50%, transparent 58%); +} + +/* The deck filling up beside the stage. */ +.lp-strip { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; } +.lp-thumb { + position: relative; aspect-ratio: 16 / 9; overflow: hidden; padding: 11% 10%; + display: flex; flex-direction: column; gap: 7%; + border: 1px solid var(--border); border-radius: 8px; background: var(--sl-paper); + box-shadow: 0 4px 12px rgba(15, 23, 42, .07); +} +.lp-th-wash { position: absolute; inset: 0; + background: radial-gradient(120% 90% at 10% 110%, var(--sl-tint-2), transparent 62%); } +.lp-th-bar { position: relative; height: 5px; border-radius: 3px; background: var(--sl-ink); } +.lp-th-bar.w70 { width: 70%; } .lp-th-bar.w52 { width: 52%; } .lp-th-bar.w44 { width: 44%; background: var(--sl-quiet); } +.lp-th-split { display: grid; grid-template-columns: 1fr 1fr; gap: 8%; flex: 1; } +.lp-th-lines { border-radius: 3px; + background: repeating-linear-gradient(180deg, var(--sl-quiet) 0 3px, transparent 3px 9px); } +.lp-th-fig { border-radius: 4px; background: linear-gradient(150deg, var(--sl-tint-2), var(--sl-tint)); } +.lp-th-cols { display: flex; align-items: flex-end; gap: 8%; flex: 1; } +.lp-th-cols i { flex: 1; height: var(--h); border-radius: 2px 2px 0 0; background: var(--sl-accent); opacity: .75; } +.lp-th-cols i:last-child { background: var(--sl-warm); } + +@media (prefers-reduced-motion: no-preference) { + .lp-studio:not(.is-calm) .lp-slide { + animation: lp-sl-stand 21s linear infinite; + animation-delay: calc(var(--i) * -14s); + } + /* Everything a slide is made of, on the one loop and the one delay, each + part late by its own --d. */ + .lp-studio:not(.is-calm) .lp-slide > *, + .lp-studio:not(.is-calm) .lp-sl-copy span { + animation: lp-sl-part 21s cubic-bezier(.2, .7, .3, 1) infinite; + animation-delay: calc(var(--i) * -14s + var(--d, 0s)); + } + .lp-studio:not(.is-calm) .lp-sl-wash { animation-name: lp-sl-wash-in; } + .lp-studio:not(.is-calm) .lp-sl-curve { + animation: lp-sl-draw 21s cubic-bezier(.4, 0, .2, 1) infinite; + animation-delay: calc(var(--i) * -14s + 1s); + } + .lp-studio:not(.is-calm) .lp-sl-dot { + animation: lp-sl-part 21s ease-out infinite; + animation-delay: calc(var(--i) * -14s + 2.1s); + } + .lp-studio:not(.is-calm) .lp-sl-ring-fill { + animation: lp-sl-draw 21s cubic-bezier(.4, 0, .2, 1) infinite; + animation-delay: calc(var(--i) * -14s + 1.2s); + } + .lp-studio:not(.is-calm) .lp-sl-bars li { + animation: lp-sl-grow 21s cubic-bezier(.2, .8, .25, 1) infinite; + animation-delay: calc(var(--i) * -14s + var(--d)); + } + .lp-studio:not(.is-calm) .lp-sl-sheen { + animation: lp-sl-sheen 21s linear infinite; + } + .lp-studio:not(.is-calm) .lp-thumb { + animation: lp-sl-land 21s ease-in-out infinite; + animation-delay: calc(var(--i) * -14s); + } +} + +/* A slide holds the stage for a third of the loop and leaves before the next + arrives, so two are never on it at once. */ +@keyframes lp-sl-stand { + 0% { opacity: 0; transform: translate3d(14px, 0, 0) scale(.985); } + 3% { opacity: 1; transform: none; } + 29% { opacity: 1; transform: none; } + 32%, 100% { opacity: 0; transform: translate3d(-14px, 0, 0) scale(.985); } +} +/* Parts clear out well before the slide does, so the next turn always starts + on an empty sheet rather than on the last one's leftovers. */ +@keyframes lp-sl-part { + 0% { opacity: 0; transform: translate3d(0, 9px, 0); } + 4% { opacity: 1; transform: none; } + 26% { opacity: 1; transform: none; } + 27%, 100% { opacity: 0; transform: none; } +} +@keyframes lp-sl-wash-in { + 0% { opacity: 0; } + 5%, 26% { opacity: 1; } + 27%, 100% { opacity: 0; } +} +@keyframes lp-sl-draw { + 0%, 3% { stroke-dashoffset: 100; } + 14%, 26% { stroke-dashoffset: 0; } + 27%, 100% { stroke-dashoffset: 100; } +} +@keyframes lp-sl-grow { + 0%, 3% { height: 0; } + 13%, 26% { height: var(--h); } + 27%, 100% { height: 0; } +} +@keyframes lp-sl-sheen { + 0%, 20% { opacity: 0; transform: translate3d(-70%, 0, 0); } + 23% { opacity: 1; } + 28% { opacity: 0; transform: translate3d(70%, 0, 0); } + 29%, 100% { opacity: 0; transform: translate3d(-70%, 0, 0); } +} +/* The thumbnail lifts while its slide is the one on the stage. */ +@keyframes lp-sl-land { + 0%, 2% { transform: none; border-color: var(--border); box-shadow: 0 4px 12px rgba(15, 23, 42, .07); } + 6%, 28% { transform: translate3d(-6px, 0, 0); + border-color: color-mix(in srgb, var(--primary) 60%, transparent); + box-shadow: 0 8px 22px color-mix(in srgb, var(--primary) 22%, transparent); } + 33%, 100% { transform: none; border-color: var(--border); box-shadow: 0 4px 12px rgba(15, 23, 42, .07); } +} + +@media (max-width: 720px) { + .lp-studio { grid-template-columns: minmax(0, 1fr); } + .lp-strip { flex-direction: row; } + .lp-thumb { flex: 1; } + /* Sideways on a narrow screen, so the lift has to be sideways too. */ + @keyframes lp-sl-land { + 0%, 2% { transform: none; border-color: var(--border); } + 6%, 28% { transform: translate3d(0, -5px, 0); + border-color: color-mix(in srgb, var(--primary) 60%, transparent); + box-shadow: 0 8px 22px color-mix(in srgb, var(--primary) 22%, transparent); } + 33%, 100% { transform: none; border-color: var(--border); } + } +} + +.lp-studio-cta { margin-top: 34px; text-align: center; } diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx index bfa6896..3c2a044 100644 --- a/frontend/src/pages/LandingPage.jsx +++ b/frontend/src/pages/LandingPage.jsx @@ -511,13 +511,11 @@ function ModeShowcase({ calm }) { const cls = `lp-modes${calm ? ' is-calm' : ''}` return (
-
-
- Study - Marked as you answer, with the reasoning underneath. -
- {/* aria-hidden throughout: this is a picture of the product, and a - screen reader should hear the caption, not a mimed question. */} +
+ {/* No caption. The panel is a picture of a study question marking + itself, which is the whole of what a caption would have said — and + a screen reader is told that by the figure's own label rather than + by a line of prose under a mimed screen. */}