Files
portal/style/main.css
T
Bendik Aagaard LynghaugandClaude Fable 5 5b3ffa62e8
Deploy / deploy (push) Successful in 1m16s
Editor type scales for real: drop prosekit typography.css, fix selectors
The earlier rem override never matched: editor.mount() turns the
.prosekit-editor div itself into the .ProseMirror root, so
'.prosekit-editor .ProseMirror p' selected nothing and prosekit
typography.css's fixed 16px kept winning. Verified by mounting the
editor standalone and reading the mount div's classes.

Rather than out-specificity a stylesheet we don't control, stop
loading it: prosekit's typography.css is optional px-based sugar. Our
own rem scale (now selected as direct children of .prosekit-editor)
plus UA em defaults for lists/blockquote/code is the whole editor
typography, all tracking the responsive root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 23:16:16 +02:00

793 lines
16 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;
--paper-raised: #161616;
--ink: #ededed;
--ink-dim: #9a9a9a;
--line: #2a2a2a;
--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));
}
* {
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;
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 actual "YES - Rasterized Lines" piece live at uhhm.no (see
public/yes.js) - landing page only. Full-viewport so it's a real
moment, not a thumbnail; hero-copy overlays near the bottom rather
than interrupting the canvas. */
.hero-yes {
position: relative;
max-width: none;
width: 100%;
/* svh here is only the pre-JS/no-JS fallback (and first paint before
yes.js's constructor runs). Once yes.js mounts, it overwrites this
with an inline `height: <px>` frozen from a single measurement -
see setupCanvas()'s comment in yes.js for why: on real mobile
Safari, content bottom-aligned inside this box kept sliding down
as the address bar collapsed even with a spec'd-stable viewport
unit here, so the box's actual height can't be trusted to stay
put on that unit alone. An inline style set from JS always wins
the cascade over this rule, so that frozen number is what
actually governs once the page is interactive.
Plain-vh fallback declared first - an engine without svh support
ignores the invalid second line rather than falling through to
auto height, which would collapse this to the height of its
in-flow content and clip the canvas via overflow:hidden below. */
height: 100vh;
height: 100svh;
padding: 0;
justify-content: flex-end;
overflow: hidden;
}
.hero-canvas {
position: absolute;
inset: 0;
}
.hero-canvas canvas {
position: absolute;
top: 0;
left: 0;
cursor: pointer;
}
.hero-canvas #rasterCanvas {
mix-blend-mode: overlay;
opacity: 0.5;
}
.hero-yes .hero-copy {
position: relative;
z-index: 1;
padding: 0 1.5rem 3.5rem;
gap: 0.6rem;
text-shadow: 0 0.12em 1.4em rgba(0, 0, 0, 0.8);
}
/* Softens the otherwise hard cut where the full-bleed canvas meets the
page background right below it - fades to the same --paper color
over the last few ems instead of stopping dead. Non-interactive so
it never steals clicks meant for the canvas underneath. */
.hero-yes::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 8rem;
background: linear-gradient(to bottom, transparent, var(--paper));
pointer-events: none;
z-index: 1;
}
/* alternatives */
.alternatives {
max-width: 75ch;
margin: 0 auto 5rem;
padding: 0 1.5rem;
display: grid;
gap: 1.25rem;
}
@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(--paper-raised);
border: 0.06rem solid var(--line);
border-radius: var(--radius);
padding: 1.75rem 1.9rem;
}
.alt-image {
display: block;
width: 100%;
max-height: 14rem;
object-fit: cover;
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 {
float: left;
width: 1.3rem;
height: 1.3rem;
margin: 0.1rem 0.6rem 0.4rem 0;
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;
}
.feature h3 {
font-size: 1rem;
margin-bottom: 0.15em;
}
.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;
}
.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: #0a0a0a;
background: var(--accent);
border: none;
border-radius: 0.7rem;
padding: 0.75rem 1.4rem;
cursor: pointer;
transition: filter 120ms, transform 120ms;
}
.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;
}
.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: #ff6b6b;
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;
}
}