Files
portal/style/main.css
T

859 lines
18 KiB
CSS
Raw Normal View History

/* 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;
}
2026-08-09 21:24:43 +02:00
@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;
}
2026-08-09 21:24:43 +02:00
@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 the sticky YES canvas - 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. */
2026-08-17 18:05:20 +02:00
--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). yes.js mirrors this palette on its own
(matchMedia in setupTheme) 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;
2026-08-17 18:05:20 +02:00
--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 wordmark SVG is a hardcoded white stroke (also used raw in
dark contexts elsewhere) - flip it to ink here rather than fork
the asset. */
.wordmark img {
filter: invert(0.92);
}
/* overlay against near-white paper resolves to ~white and the
raster ghost vanishes; multiply lets the light theme's gray YES
(see rasterizeText's themed repaint in yes.js) show as ink. */
.hero-canvas #rasterCanvas {
mix-blend-mode: multiply;
}
}
2026-08-09 21:24:43 +02:00
* {
box-sizing: border-box;
}
2026-08-09 21:24:43 +02:00
html,
body {
margin: 0;
padding: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
line-height: 1.55;
}
2026-08-09 21:24:43 +02:00
h1,
h2,
h3 {
font-family: var(--heading);
font-weight: 600;
line-height: 1.2;
margin: 0 0 0.4em;
}
2026-08-09 21:24:43 +02:00
p {
margin: 0 0 0.9em;
}
2026-08-09 21:24:43 +02:00
a {
color: var(--accent);
}
2026-08-09 21:24:43 +02:00
::selection {
background: var(--accent-soft);
color: var(--ink);
}
2026-08-09 21:24:43 +02:00
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;
}
2026-08-09 21:24:43 +02:00
.wordmark:hover {
opacity: 1;
}
.wordmark img {
height: 2.1rem;
width: auto;
display: block;
}
2026-08-09 21:24:43 +02:00
.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 {
/* Sticky at the viewport top for the whole scroll (its containing
block is the page itself), so the piece stays animating behind
everything that follows - the translucent cards scroll over it
and it shows through them and in the gaps around them. z-index 0
so positioned content below can stack above with z-index 1. */
position: sticky;
top: 0;
z-index: 0;
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;
2026-08-06 18:57:17 +00:00
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 var(--hero-glow);
}
/* 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;
2026-08-13 14:46:30 +02:00
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) {
2026-08-09 21:24:43 +02:00
.alternatives {
padding: 0;
gap: 3.5rem;
}
.alt-card {
border-width: 0;
border-radius: 0rem;
}
}
2026-08-12 23:09:55 +02:00
.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);
2026-08-12 23:09:55 +02:00
}
.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;
}
2026-08-09 21:24:43 +02:00
.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%;
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;
}
2026-08-09 21:24:43 +02:00
.alt-card h2 {
font-size: 1.3rem;
}
2026-08-09 21:24:43 +02:00
.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;
}
2026-08-09 21:24:43 +02:00
.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;
}
2026-08-09 21:24:43 +02:00
.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);
}
2026-08-09 21:24:43 +02:00
.required {
color: var(--accent);
margin-left: 0.2em;
}
2026-08-09 21:24:43 +02:00
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%;
}
2026-08-09 21:24:43 +02:00
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;
}
2026-08-09 21:24:43 +02:00
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;
}
2026-08-09 21:24:43 +02:00
.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;
2026-08-09 21:24:43 +02:00
margin: 0 0 0.6em;
}
.prosekit-editor p:last-child {
2026-08-09 21:24:43 +02:00
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;
}
2026-08-09 21:24:43 +02:00
.alt-submit:hover {
filter: brightness(1.08);
}
2026-08-09 21:24:43 +02:00
.alt-submit:active {
transform: translateY(0.06em);
2026-08-09 21:24:43 +02:00
}
.alt-submit:disabled {
filter: grayscale(0.5) brightness(0.85);
cursor: wait;
}
.gate-card {
text-align: center;
}
/* resources */
2026-08-09 21:24:43 +02:00
.resource {
margin-top: 0.6rem;
}
2026-08-09 21:24:43 +02:00
.resource-loading,
.resource-empty {
color: var(--ink-dim);
font-size: 0.9rem;
}
2026-08-09 21:24:43 +02:00
.resource-error {
color: var(--error);
2026-08-09 21:24:43 +02:00
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;
}
2026-08-09 21:24:43 +02:00
.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;
}
2026-08-09 21:24:43 +02:00
.answer-row.decided {
opacity: 0.55;
}
.answer-fields {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10.5rem, 1fr));
gap: 0.5rem 1rem;
}
2026-08-09 21:24:43 +02:00
.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);
}
2026-08-09 21:24:43 +02:00
.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;
}
2026-08-09 21:24:43 +02:00
.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;
}
2026-08-09 21:24:43 +02:00
.item-card-title a {
color: var(--ink);
text-decoration: none;
}
2026-08-09 21:24:43 +02:00
.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;
}
2026-08-09 21:24:43 +02:00
.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;
}
2026-08-09 21:24:43 +02:00
.select-field {
margin-top: 0.3rem;
}
2026-08-09 21:24:43 +02:00
.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;
}
2026-08-09 21:24:43 +02:00
.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) {
2026-08-09 21:24:43 +02:00
.hero {
padding: 3rem 1.25rem 2rem;
}
.alt-card {
padding: 1.4rem 1.3rem;
}
.alt-cta {
flex-direction: column;
align-items: flex-start;
}
}