`list_qualifying_questions` reduced the visitor's `?chain=` to a
bool, so one followup path unlocked every followup on the site -
redoal's `/watching` (an email-link page whose header says "never in
the nav") showed up for anyone who had just subscribed.
The nav now resolves the chain hash to the one question it verifiably
answered (the same `lookup_node` a `requires_chain` gate uses) and
offers a followup only when it is on that path or further down from
the current page over `action` edges. `content::outcomes_reachable_from`
walks those edges through followup pages only, so a "back to the
start" action ends the walk at the front door instead of fanning out.
Templated actions (`/shape/{curve.key}`) resolve to their dynamic
page's pattern. No schema change: the `followup` flag keeps meaning
"not a front door", and the edges were already in the content.
Also: `main.loading`/`main.not-found` gets the same `svh` ladder as
`.hero-piece`, the last bare `vh` in the stylesheet, so wrapper
browsers that resize the webview as their toolbar animates don't
reflow the placeholder.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L4jrCgLiKKHAEFuZUJjckH
1120 lines
24 KiB
CSS
1120 lines
24 KiB
CSS
/* Same self-hosted Quicksand used for headings on Gitea/the homepage
|
|
dashboard elsewhere in this stack (see
|
|
~/repos/infrastructure/gitea-custom-header.tmpl) - reused as-is
|
|
(same files, same unicode-range split) rather than pulled from a
|
|
font CDN.
|
|
font-display: optional, not swap, on all three: swap always shows
|
|
the fallback font first then reflows to Quicksand once it loads,
|
|
which is a real layout shift if that swap lands after the page is
|
|
already scrolled past that heading - reported as "text jumps" on
|
|
real mobile Safari. optional skips the swap entirely unless the
|
|
font is already cached/ready almost immediately, trading an
|
|
occasional fallback-font render for zero shift. */
|
|
@font-face {
|
|
font-family: "Quicksand";
|
|
font-style: normal;
|
|
font-weight: 600;
|
|
font-display: optional;
|
|
src: url("/fonts/quicksand-semibold-latin.woff2") format("woff2");
|
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Quicksand";
|
|
font-style: normal;
|
|
font-weight: 600;
|
|
font-display: optional;
|
|
src: url("/fonts/quicksand-semibold-latin-ext.woff2") format("woff2");
|
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Quicksand";
|
|
font-style: normal;
|
|
font-weight: 600;
|
|
font-display: optional;
|
|
src: url("/fonts/quicksand-semibold-vietnamese.woff2") format("woff2");
|
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
|
}
|
|
|
|
:root {
|
|
/* the one brand color - everything else is neutral. A muted,
|
|
desaturated echo of the YES canvas's cyan ink: same family as the
|
|
canvas so chrome harmonizes with it, but dusty enough to read as a
|
|
quiet spot color on dark stock rather than compete. Legible
|
|
against near-black both as text and under #0a0a0a button text. */
|
|
--accent: #8ec2c0;
|
|
--accent-soft: rgba(142, 194, 192, 0.16);
|
|
|
|
--paper: #0a0a0a;
|
|
--ink: #ededed;
|
|
--ink-dim: #9a9a9a;
|
|
--line: #2a2a2a;
|
|
|
|
/* Cards float over a sticky hero piece - translucent so the piece
|
|
reads faintly through them (paired with backdrop-filter on
|
|
.alt-card), shadowed so their edge against the animated backdrop
|
|
is a soft lift rather than a hard contrast line. */
|
|
--card-bg: rgba(22, 22, 22, 0.35);
|
|
--card-bg-opaque: rgba(22, 22, 22, 0.84);
|
|
--card-shadow: rgba(0, 0, 0, 0.5);
|
|
|
|
--error: #ff6b6b;
|
|
/* Behind .hero-copy: dark halo on dark stock, paper halo on light. */
|
|
--hero-glow: rgba(0, 0, 0, 0.8);
|
|
|
|
--heading: "Quicksand", var(--sans);
|
|
--sans: -apple-system, "SF Pro Text", ui-sans-serif, "Segoe UI", system-ui, sans-serif;
|
|
|
|
--radius: 1.1rem;
|
|
|
|
/* The single place absolute units are allowed: this IS the base every
|
|
other length derives from (rem/em everywhere else - px is banned in
|
|
stylesheets). First line is the fallback for engines without lvmin. */
|
|
font-size: 17px;
|
|
font-size: max(12px, calc(2.1lvmin + 3pt));
|
|
}
|
|
|
|
/* Light theme: the same muted-press idea on white stock - warm paper,
|
|
near-black ink, and the accent deepened from dusty cyan to a teal
|
|
ink that actually carries as text on light paper (the dark theme's
|
|
#8ec2c0 washes out there). Canvas-drawing modules (gesture.js, and
|
|
content-shipped hero modules) mirror this palette on their own,
|
|
since canvas paint can't read CSS vars. */
|
|
@media (prefers-color-scheme: light) {
|
|
:root {
|
|
--accent: #47807e;
|
|
--accent-soft: rgba(71, 128, 126, 0.16);
|
|
|
|
--paper: #f6f5f1;
|
|
--ink: #1d1d1b;
|
|
--ink-dim: #6d6c66;
|
|
--line: #d9d7cf;
|
|
|
|
--card-bg: rgba(252, 251, 248, 0.35);
|
|
--card-bg-opaque: rgba(252, 251, 248, 0.84);
|
|
--card-shadow: rgba(50, 48, 42, 0.18);
|
|
|
|
--error: #a83a32;
|
|
--hero-glow: rgba(246, 245, 241, 0.85);
|
|
}
|
|
|
|
/* The house wordmark SVG is a hardcoded white stroke (also used
|
|
raw in dark contexts elsewhere) - flip it to ink here rather
|
|
than fork the asset. A content-provided logo keeps its colors. */
|
|
.wordmark-house img {
|
|
filter: invert(0.92);
|
|
}
|
|
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: var(--paper);
|
|
color: var(--ink);
|
|
font-family: var(--sans);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
h1,
|
|
h2,
|
|
h3 {
|
|
font-family: var(--heading);
|
|
font-weight: 600;
|
|
line-height: 1.2;
|
|
margin: 0 0 0.4em;
|
|
}
|
|
|
|
p {
|
|
margin: 0 0 0.9em;
|
|
}
|
|
|
|
a {
|
|
color: var(--accent);
|
|
}
|
|
|
|
::selection {
|
|
background: var(--accent-soft);
|
|
color: var(--ink);
|
|
}
|
|
|
|
main.loading,
|
|
main.not-found {
|
|
min-height: 60vh;
|
|
/* svh so wrapper browsers that resize the webview as their toolbar
|
|
animates (Firefox/Chrome iOS) don't reflow this placeholder - the
|
|
small viewport is a fixed value. Same ladder as .hero-piece. */
|
|
min-height: 60svh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 1rem;
|
|
text-align: center;
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
/* hero */
|
|
|
|
.hero {
|
|
max-width: 46rem;
|
|
margin: 0 auto;
|
|
padding: 4rem 1.5rem 2.5rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.wordmark {
|
|
display: inline-flex;
|
|
opacity: 0.85;
|
|
transition: opacity 120ms;
|
|
}
|
|
|
|
.wordmark:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
.wordmark img {
|
|
height: 2.1rem;
|
|
width: auto;
|
|
display: block;
|
|
}
|
|
|
|
.hero-copy h1 {
|
|
font-size: 2.1rem;
|
|
}
|
|
|
|
.hero-copy p {
|
|
color: var(--ink-dim);
|
|
font-size: 1.05rem;
|
|
max-width: 46ch;
|
|
/* The measure cap shrinks the box below the copy column; without
|
|
auto margins the box left-anchors and its centered text centers
|
|
in the wrong frame. */
|
|
margin-inline: auto;
|
|
}
|
|
|
|
/* A content-shipped hero module (site.yaml hero.kind: module) draws
|
|
into .hero-piece; the module owns its look (it may restyle .hero
|
|
itself). Portal only reserves the box so the copy doesn't jump when
|
|
the module mounts - plain-vh fallback first, as everywhere. */
|
|
.hero-module .hero-piece {
|
|
position: relative;
|
|
width: 100%;
|
|
min-height: 55vh;
|
|
min-height: 55svh;
|
|
}
|
|
|
|
/* announcements - live event pages, a strip above the hero copy.
|
|
Header, not footer: the one place a page claims attention before
|
|
the question is asked. */
|
|
.announce {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
gap: 0.6rem 1.2rem;
|
|
margin: -2rem 0 0.5rem;
|
|
font-size: 0.88rem;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.announce-item {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 0.35rem 0.7rem;
|
|
padding: 0.45rem 0.9rem;
|
|
border: 0.06rem solid var(--line);
|
|
border-left: 0.2rem solid var(--accent);
|
|
border-radius: 0.3rem;
|
|
color: var(--ink);
|
|
text-decoration: none;
|
|
background: var(--paper);
|
|
transition: border-color 120ms;
|
|
}
|
|
|
|
.announce-item:hover,
|
|
.announce-item[aria-current="page"] {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.announce-name {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.announce-when {
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.announce-relative {
|
|
color: var(--accent);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
font-size: 0.72rem;
|
|
}
|
|
|
|
/* alternatives */
|
|
|
|
/* position + z-index on these three: everything that scrolls over the
|
|
sticky canvas must be a positioned box above the hero's z-index 0,
|
|
or the (positioned) hero would paint over it. */
|
|
.alternatives {
|
|
position: relative;
|
|
z-index: 1;
|
|
max-width: 75ch;
|
|
margin: 0 auto 5rem;
|
|
padding: 0 1.5rem;
|
|
display: grid;
|
|
gap: 1.25rem;
|
|
}
|
|
|
|
.question-nav,
|
|
.question-responsible {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
@media (max-width: 43.75rem) {
|
|
.alternatives {
|
|
padding: 0;
|
|
gap: 3.5rem;
|
|
}
|
|
|
|
.alt-card {
|
|
border-width: 0;
|
|
border-radius: 0rem;
|
|
}
|
|
}
|
|
|
|
.question-nav {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
align-items: baseline;
|
|
gap: 0.5rem 1.2rem;
|
|
margin: 2.2rem auto 0;
|
|
max-width: 44rem;
|
|
padding: 0 1rem;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.question-nav-lead {
|
|
color: var(--ink-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
font-size: 0.72rem;
|
|
}
|
|
|
|
.question-nav a {
|
|
color: var(--ink-dim);
|
|
text-decoration: none;
|
|
border-bottom: 0.06rem solid var(--line);
|
|
}
|
|
|
|
.question-nav a:hover {
|
|
color: var(--accent);
|
|
border-bottom-color: var(--accent);
|
|
}
|
|
|
|
/* Meta information above the question nav - dimmer and smaller than
|
|
anything the question itself says. */
|
|
.question-responsible {
|
|
display: block;
|
|
text-align: center;
|
|
color: var(--ink-dim);
|
|
opacity: 0.7;
|
|
font-size: 0.85rem;
|
|
line-height: 1.6;
|
|
margin: 2.2rem 1rem 0;
|
|
}
|
|
|
|
.question-responsible a {
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.question-responsible a:hover {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.alt-card {
|
|
background: var(--card-bg);
|
|
/* Frosted glass over the animated canvas behind: the blur is what
|
|
keeps text on a translucent card readable while the piece still
|
|
reads through it in color and motion. */
|
|
-webkit-backdrop-filter: blur(0.8rem);
|
|
backdrop-filter: blur(0.8rem);
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: var(--radius);
|
|
padding: 1.75rem 1.9rem;
|
|
box-shadow: 0 1.1rem 2.4rem var(--card-shadow);
|
|
}
|
|
|
|
/* Without backdrop blur, 0.55-alpha cards put text straight over the
|
|
moving canvas - illegible. Engines lacking the filter get a
|
|
near-opaque card instead. */
|
|
@supports not ((backdrop-filter: blur(1rem)) or (-webkit-backdrop-filter: blur(1rem))) {
|
|
.alt-card {
|
|
background: var(--card-bg-opaque);
|
|
}
|
|
}
|
|
|
|
.alt-image {
|
|
display: block;
|
|
width: 100%;
|
|
height: auto;
|
|
/* Natural aspect, not a cropped band; a very tall image is still
|
|
bounded so it can't tower. The deck below overrides for its
|
|
fixed-height cards. */
|
|
max-height: 32rem;
|
|
object-fit: contain;
|
|
border-radius: calc(var(--radius) - 0.3rem);
|
|
margin-bottom: 1.1rem;
|
|
}
|
|
|
|
/* Sized down from full card width so the cards effect's fanned-out
|
|
neighbors stay inside the card - swiper positions slides absolutely,
|
|
so the container needs its own height. Before the bundle upgrades
|
|
the element (or without JS) slides stack as plain blocks; capping
|
|
the container height keeps that fallback from towering. */
|
|
.alt-image-deck {
|
|
display: block;
|
|
width: min(100%, 22rem);
|
|
height: 14rem;
|
|
margin: 0 auto 1.1rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.alt-image-deck .alt-image {
|
|
height: 100%;
|
|
max-height: none;
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.alt-card h2 {
|
|
font-size: 1.3rem;
|
|
}
|
|
|
|
.alt-description {
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.alt-encouragement {
|
|
font-size: 0.9rem;
|
|
color: var(--accent);
|
|
font-style: italic;
|
|
margin: 0;
|
|
}
|
|
|
|
.alt-cta {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1.2rem;
|
|
margin-top: 1.4rem;
|
|
}
|
|
|
|
.alt-encouragements {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
}
|
|
|
|
.features {
|
|
display: grid;
|
|
gap: 1.1rem;
|
|
margin-top: 0.6rem;
|
|
}
|
|
|
|
.feature {
|
|
border-left: 0.18rem solid var(--feature-accent, transparent);
|
|
}
|
|
|
|
.feature-icon {
|
|
width: 1.3rem;
|
|
height: 1.3rem;
|
|
margin-top: 0.1rem;
|
|
background-color: var(--feature-accent, currentColor);
|
|
mask-repeat: no-repeat;
|
|
mask-size: contain;
|
|
mask-position: center;
|
|
-webkit-mask-repeat: no-repeat;
|
|
-webkit-mask-size: contain;
|
|
-webkit-mask-position: center;
|
|
}
|
|
|
|
/* With an icon the feature is two columns: the icon in the first,
|
|
everything else - name, description, fields, resources - in the
|
|
second, so every line of text shares one left edge instead of
|
|
wrapping back under the icon. */
|
|
.feature:has(> .feature-icon) {
|
|
display: grid;
|
|
grid-template-columns: 1.3rem minmax(0, 1fr);
|
|
column-gap: 0.7rem;
|
|
align-items: start;
|
|
}
|
|
|
|
.feature:has(> .feature-icon) > .feature-icon {
|
|
grid-column: 1;
|
|
grid-row: 1;
|
|
}
|
|
|
|
.feature:has(> .feature-icon) > :not(.feature-icon) {
|
|
grid-column: 2;
|
|
}
|
|
|
|
.feature h3 {
|
|
font-size: 1rem;
|
|
margin-bottom: 0.15em;
|
|
}
|
|
|
|
/* voice field (voice.js): a record button, a status line, a preview */
|
|
.voice-wrap {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.6rem 1rem;
|
|
}
|
|
|
|
.voice-button {
|
|
font: inherit;
|
|
padding: 0.55rem 1.1rem;
|
|
border: 0.06rem solid var(--accent);
|
|
border-radius: 2rem;
|
|
background: transparent;
|
|
color: var(--ink);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.voice-button:hover:not(:disabled) {
|
|
background: var(--accent);
|
|
color: var(--paper);
|
|
}
|
|
|
|
.voice-button.recording {
|
|
background: var(--accent);
|
|
color: var(--paper);
|
|
animation: voice-pulse 1.2s ease-in-out infinite;
|
|
}
|
|
|
|
.voice-button:disabled {
|
|
opacity: 0.5;
|
|
cursor: default;
|
|
}
|
|
|
|
@keyframes voice-pulse {
|
|
50% { opacity: 0.6; }
|
|
}
|
|
|
|
.voice-status {
|
|
font-size: 0.85rem;
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.voice-preview,
|
|
.item-card-audio {
|
|
display: block;
|
|
width: 100%;
|
|
max-width: 28rem;
|
|
margin-top: 0.4rem;
|
|
}
|
|
|
|
/* display:block above would beat the hidden attribute */
|
|
.voice-preview[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
/* links inside markdown descriptions */
|
|
.alt-description a,
|
|
.feature p a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
border-bottom: 0.06rem solid var(--accent);
|
|
}
|
|
|
|
.alt-description a:hover,
|
|
.feature p a:hover {
|
|
color: var(--accent);
|
|
}
|
|
|
|
/* images inside markdown descriptions: full-width figures in the
|
|
card's flow, framed like the rest of the press sheet */
|
|
.alt-description img,
|
|
.item-card-description img,
|
|
.feature p img {
|
|
display: block;
|
|
width: 100%;
|
|
margin: 0.6rem 0 0.2rem;
|
|
border-radius: 0.5rem;
|
|
border: 0.06rem solid var(--line);
|
|
}
|
|
|
|
/* Content hint via a markdown image title (``):
|
|
float and shrink so text wraps around a portrait. */
|
|
.alt-description img.md-float-left,
|
|
.item-card-description img.md-float-left,
|
|
.feature p img.md-float-left,
|
|
.alt-description img.md-float-right,
|
|
.item-card-description img.md-float-right,
|
|
.feature p img.md-float-right {
|
|
width: auto;
|
|
max-width: 45%;
|
|
}
|
|
.alt-description img.md-float-left,
|
|
.item-card-description img.md-float-left,
|
|
.feature p img.md-float-left {
|
|
float: left;
|
|
margin: 0.2rem 1.1rem 0.5rem 0;
|
|
}
|
|
.alt-description img.md-float-right,
|
|
.item-card-description img.md-float-right,
|
|
.feature p img.md-float-right {
|
|
float: right;
|
|
margin: 0.2rem 0 0.5rem 1.1rem;
|
|
}
|
|
|
|
.alt-description code,
|
|
.feature p code {
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.feature p {
|
|
color: var(--ink-dim);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.35rem;
|
|
margin-top: 0.6rem;
|
|
}
|
|
|
|
.field-label {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.required {
|
|
color: var(--accent);
|
|
margin-left: 0.2em;
|
|
}
|
|
|
|
input,
|
|
textarea {
|
|
font: inherit;
|
|
color: var(--ink);
|
|
background: var(--paper);
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: 0.6rem;
|
|
padding: 0.65rem 0.8rem;
|
|
width: 100%;
|
|
}
|
|
|
|
textarea {
|
|
min-height: 5rem;
|
|
resize: vertical;
|
|
}
|
|
|
|
/* A bound textarea holds loaded source text - monospace, roomy, no
|
|
soft-wrap surprises. */
|
|
textarea.bound {
|
|
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
font-size: 0.85rem;
|
|
min-height: 16rem;
|
|
white-space: pre;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
input:focus,
|
|
textarea:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 0.18rem var(--accent-soft);
|
|
}
|
|
|
|
/* One placeholder treatment for every input-like surface: solid
|
|
ink-dim, no translucency. opacity: 1 also undoes the UA's own
|
|
placeholder dimming (Firefox) and ProseKit's 0.3 on its
|
|
placeholder pseudo-element, which read far dimmer than the
|
|
native inputs next to it. */
|
|
::placeholder,
|
|
.prosekit-editor .prosekit-placeholder::before,
|
|
.prosekit-editor.prosekit-placeholder::before {
|
|
color: var(--ink-dim);
|
|
opacity: 1;
|
|
}
|
|
|
|
.prosekit-wrap {
|
|
background: var(--paper);
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: 0.6rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* The gesture drawing field (gesture.js). Stroke/ghost/echo colors
|
|
live in the JS module's own palette table - canvas can't read these
|
|
custom properties - so palette changes go there AND here. */
|
|
.gesture-wrap {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
|
|
/* The canvas is created by JS only after wasm hydration - reserve its
|
|
3/2 box until then so content below doesn't jump. Scoped to :empty
|
|
on purpose: on the mounted wrap (a stretched item inside the flex
|
|
.field) the same aspect-ratio turned the echo strip's extra height
|
|
into extra WIDTH and the whole card ballooned past the viewport
|
|
after the first stroke. */
|
|
.gesture-wrap:empty {
|
|
aspect-ratio: 3 / 2;
|
|
}
|
|
|
|
.gesture-canvas {
|
|
display: block;
|
|
width: 100%;
|
|
aspect-ratio: 3 / 2;
|
|
background: var(--paper);
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: 0.6rem;
|
|
cursor: crosshair;
|
|
/* Load-bearing on touch: without it the browser pans instead of
|
|
letting the stroke happen. */
|
|
touch-action: none;
|
|
}
|
|
|
|
/* Results overlay the canvas instead of growing the widget - the
|
|
page never jumps when places arrive (or don't). */
|
|
.gesture-echoes {
|
|
position: absolute;
|
|
left: 0.6rem;
|
|
right: 0.6rem;
|
|
bottom: 0.6rem;
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.gesture-echoes:empty {
|
|
display: none;
|
|
}
|
|
|
|
.gesture-echo {
|
|
width: 3rem;
|
|
height: 3rem;
|
|
background: var(--paper);
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: 0.4rem;
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease;
|
|
pointer-events: auto;
|
|
box-shadow: 0 0.15rem 0.6rem rgba(0, 0, 0, 0.18);
|
|
}
|
|
|
|
.gesture-echo.shown {
|
|
opacity: var(--echo-strength, 1);
|
|
}
|
|
|
|
/* places are pickable: the pick re-derives the input's key */
|
|
.gesture-echo {
|
|
cursor: pointer;
|
|
transition: opacity 0.5s ease, border-color 120ms, transform 120ms;
|
|
}
|
|
|
|
.gesture-echo:hover,
|
|
.gesture-echo:focus-visible {
|
|
border-color: var(--accent);
|
|
outline: none;
|
|
}
|
|
|
|
.gesture-echo.selected {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 0.12rem var(--accent);
|
|
transform: translateY(-0.1rem);
|
|
}
|
|
|
|
.gesture-empty {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0.7rem;
|
|
margin: 0;
|
|
text-align: center;
|
|
font-size: 0.85rem;
|
|
color: var(--ink-dim);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.gesture-empty:empty {
|
|
display: none;
|
|
}
|
|
|
|
.gesture-status {
|
|
position: absolute;
|
|
top: 0.55rem;
|
|
right: 0.55rem;
|
|
width: 0.45rem;
|
|
height: 0.45rem;
|
|
border-radius: 50%;
|
|
background: var(--ink-dim);
|
|
opacity: 0.35;
|
|
transition: opacity 0.3s ease, background 0.3s ease;
|
|
}
|
|
|
|
.gesture-status.live {
|
|
background: var(--accent);
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.prosekit-toolbar {
|
|
display: flex;
|
|
gap: 0.2rem;
|
|
padding: 0.4rem;
|
|
border-bottom: 0.06rem solid var(--line);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.prosekit-toolbar button {
|
|
font: inherit;
|
|
font-size: 0.9rem;
|
|
color: var(--ink-dim);
|
|
background: transparent;
|
|
border: 0.06rem solid transparent;
|
|
border-radius: 0.4rem;
|
|
padding: 0.3rem 0.55rem;
|
|
cursor: pointer;
|
|
line-height: 1;
|
|
}
|
|
|
|
.prosekit-toolbar button:hover {
|
|
color: var(--ink);
|
|
border-color: var(--line);
|
|
}
|
|
|
|
.prosekit-toolbar button.is-active {
|
|
color: var(--paper);
|
|
background: var(--accent);
|
|
}
|
|
|
|
.prosekit-editor {
|
|
font: inherit;
|
|
color: var(--ink);
|
|
padding: 0.65rem 0.8rem;
|
|
min-height: 5rem;
|
|
}
|
|
|
|
.prosekit-editor:focus {
|
|
outline: none;
|
|
}
|
|
|
|
.prosekit-wrap:focus-within {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 0.18rem var(--accent-soft);
|
|
}
|
|
|
|
/* The editor's whole type scale, in rem. ProseKit's own typography.css
|
|
is deliberately not loaded (it sizes content in fixed px - see
|
|
mountEditor in prosekit-editor.js), so these rules are the only
|
|
typography the editor gets, on top of UA defaults (em-based, they
|
|
scale fine for lists/blockquote/code). NOTE: editor.mount() turns the
|
|
.prosekit-editor div itself into the .ProseMirror root, so content
|
|
elements are its direct children - no .ProseMirror in the selector. */
|
|
.prosekit-editor p {
|
|
font-size: 1rem;
|
|
line-height: 1.5;
|
|
margin: 0 0 0.6em;
|
|
}
|
|
|
|
.prosekit-editor p:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.prosekit-editor h1 {
|
|
font-size: 2.5rem;
|
|
line-height: 1.1;
|
|
margin: 0.9em 0 0.1em;
|
|
}
|
|
|
|
.prosekit-editor h2 {
|
|
font-size: 1.875rem;
|
|
line-height: 1.3;
|
|
margin: 1.05em 0 0.13em;
|
|
}
|
|
|
|
.prosekit-editor h3 {
|
|
font-size: 1.5rem;
|
|
line-height: 1.33;
|
|
margin: 0.9em 0 0.04em;
|
|
}
|
|
|
|
.prosekit-editor h4 {
|
|
font-size: 1.25rem;
|
|
line-height: 1.3;
|
|
margin: 0.8em 0 0.05em;
|
|
}
|
|
|
|
.prosekit-editor h5 {
|
|
font-size: 1.125rem;
|
|
line-height: 1.22;
|
|
margin: 0.78em 0 0.06em;
|
|
}
|
|
|
|
.prosekit-editor h6 {
|
|
font-size: 1rem;
|
|
line-height: 1.25;
|
|
margin: 0.75em 0 0.06em;
|
|
}
|
|
|
|
.prosekit-editor :is(h1, h2, h3, h4, h5, h6):first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
.alt-submit {
|
|
font-family: var(--sans);
|
|
font-weight: 600;
|
|
font-size: 0.95rem;
|
|
color: var(--paper);
|
|
background: var(--accent);
|
|
border: none;
|
|
border-radius: 0.7rem;
|
|
padding: 0.75rem 1.4rem;
|
|
cursor: pointer;
|
|
transition: filter 120ms, transform 120ms;
|
|
/* A gateway alternative renders this as an <a> (navigation, no POST);
|
|
kill the link chrome so it reads as the button it looks like. */
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
}
|
|
|
|
.alt-submit:hover {
|
|
filter: brightness(1.08);
|
|
}
|
|
|
|
.alt-submit:active {
|
|
transform: translateY(0.06em);
|
|
}
|
|
|
|
.alt-submit:disabled {
|
|
filter: grayscale(0.5) brightness(0.85);
|
|
cursor: wait;
|
|
}
|
|
|
|
/* A content-disabled alternative (`disabled: true`): announced, not
|
|
yet takeable - reads as a promise, not as a stuck form. */
|
|
.alt-submit.soon:disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.gate-card {
|
|
text-align: center;
|
|
}
|
|
|
|
/* resources */
|
|
|
|
.resource {
|
|
margin-top: 0.6rem;
|
|
}
|
|
|
|
.resource-loading,
|
|
.resource-empty {
|
|
color: var(--ink-dim);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.resource-error {
|
|
color: var(--error);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.resource-raw {
|
|
background: var(--paper);
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: 0.6rem;
|
|
padding: 0.8rem;
|
|
font-size: 0.82rem;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.answer-list {
|
|
display: grid;
|
|
gap: 0.8rem;
|
|
}
|
|
|
|
.answer-row {
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: 0.8rem;
|
|
padding: 1rem 1.1rem;
|
|
display: grid;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.answer-row.decided {
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.answer-fields {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
|
|
gap: 0.5rem 1rem;
|
|
}
|
|
|
|
.answer-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.1rem;
|
|
}
|
|
|
|
.answer-field-key {
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.answer-field-value {
|
|
font-size: 0.92rem;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.answer-meta {
|
|
display: flex;
|
|
gap: 0.8rem;
|
|
align-items: center;
|
|
font-size: 0.78rem;
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.answer-state {
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
}
|
|
|
|
.answer-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 0.6rem;
|
|
}
|
|
|
|
.item-list {
|
|
display: grid;
|
|
gap: 0.8rem;
|
|
}
|
|
|
|
.item-card {
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: 0.8rem;
|
|
padding: 1rem 1.1rem;
|
|
display: grid;
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.item-card-title {
|
|
font-family: var(--sans);
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.item-card-title a {
|
|
color: var(--ink);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.item-card-title a:hover {
|
|
color: var(--accent);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.item-card-description {
|
|
font-size: 0.9rem;
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.item-card-fields {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.4rem 0.9rem;
|
|
margin-top: 0.2rem;
|
|
}
|
|
|
|
.item-card-field {
|
|
font-size: 0.82rem;
|
|
color: var(--ink-dim);
|
|
}
|
|
|
|
.item-card-field-key {
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
font-size: 0.68rem;
|
|
margin-right: 0.3rem;
|
|
}
|
|
|
|
.select-field {
|
|
margin-top: 0.3rem;
|
|
}
|
|
|
|
.select-options {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.select-option {
|
|
font-family: var(--sans);
|
|
font-size: 0.85rem;
|
|
color: var(--ink);
|
|
background: var(--paper);
|
|
border: 0.06rem solid var(--line);
|
|
border-radius: 999rem;
|
|
padding: 0.4rem 0.9rem;
|
|
cursor: pointer;
|
|
transition: border-color 120ms, color 120ms, background 120ms;
|
|
}
|
|
|
|
.select-option:hover {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.select-option.selected {
|
|
border-color: var(--accent);
|
|
color: var(--paper);
|
|
background: var(--accent);
|
|
}
|
|
|
|
.select-option:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
@media (max-width: 40rem) {
|
|
.hero {
|
|
padding: 3rem 1.25rem 2rem;
|
|
}
|
|
|
|
.alt-card {
|
|
padding: 1.4rem 1.3rem;
|
|
}
|
|
|
|
.alt-cta {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
} |