Files
portal/style/main.css
T
Bendik Aagaard LynghaugandClaude Fable 5 7b5d4dbf52
Deploy / deploy (push) Successful in 1m15s
No translucent input chrome: solid ink-dim placeholders and embed meta
One placeholder rule for native inputs and the prosekit editor alike;
kills ProseKit's 0.3-opacity placeholder and Firefox's UA dimming. The
embed description drops inline opacity for a fixed mid-gray that stays
dim on both the newsletter's light background and our dark paper.

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

789 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 {
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);
}
/* ProseKit's typography.css (loaded from esm.sh at mount time) sets
fixed px font sizes on .ProseMirror, so they ignore the responsive
root font-size. Re-state them in rem/em at the same proportions,
with .prosekit-editor prefixed so these win on specificity. */
.prosekit-editor .ProseMirror p {
font-size: 1rem;
line-height: 1.5;
margin: 0 0 0.6em;
}
.prosekit-editor .ProseMirror p:last-child {
margin-bottom: 0;
}
.prosekit-editor .ProseMirror h1 {
font-size: 2.5rem;
line-height: 1.1;
margin: 0.9em 0 0.1em;
}
.prosekit-editor .ProseMirror h2 {
font-size: 1.875rem;
line-height: 1.3;
margin: 1.05em 0 0.13em;
}
.prosekit-editor .ProseMirror h3 {
font-size: 1.5rem;
line-height: 1.33;
margin: 0.9em 0 0.04em;
}
.prosekit-editor .ProseMirror h4 {
font-size: 1.25rem;
line-height: 1.3;
margin: 0.8em 0 0.05em;
}
.prosekit-editor .ProseMirror h5 {
font-size: 1.125rem;
line-height: 1.22;
margin: 0.78em 0 0.06em;
}
.prosekit-editor .ProseMirror h6 {
font-size: 1rem;
line-height: 1.25;
margin: 0.75em 0 0.06em;
}
.prosekit-editor .ProseMirror :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;
}
}