Loading files
ea39f3ad2This file is pinned to the selected release. Download the original to open it in your design tool.
/* Community — the dark room.
The rest of the site is cream on ink; here that inverts, so a running LED
pattern is the only light on the page. Tokens live in globals.css as
--pfc-* (see the "Community: the dark room" block there); nothing in this
file should hard-code a colour that has a token.
Radius is 0 everywhere, same as the light site. */
.page {
min-height: 100vh;
background: var(--pfc-ground);
color: var(--pfc-ink);
font-family: var(--pf-sans, ui-sans-serif, system-ui, sans-serif);
display: flex;
flex-direction: column;
}
/* globals.css underlines every link on hover. In here a link changes colour
instead — the wall is dense enough that underlines on hover read as noise. */
.page a:hover {
text-decoration: none;
}
.inner {
width: 100%;
max-width: 100%;
min-height: 100vh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* Page content sits inside this gutter; full-bleed sections (the marquee)
break out of it with a matching negative margin. */
.pageBody {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 40px;
box-sizing: border-box;
}
/* ── Header ── */
.pageHeader {
display: flex;
align-items: center;
gap: 18px;
padding: 16px 40px;
border-bottom: 1px solid var(--pfc-hair);
flex: none;
}
.brandBlock {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.brand {
color: var(--pfc-ink);
font-size: 16px;
font-weight: 700;
text-decoration: none;
line-height: 1.15;
}
/* "COMMUNITY" under the wordmark, in the accent: the one place on the site
where the orange names a section rather than a live signal, because the
section IS the live one. */
.pageTitle {
font-family: var(--pf-mono, monospace);
font-size: 10px;
font-weight: 400;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--pfc-led);
text-decoration: none;
}
.headerSpacer {
flex: 1;
}
.headerNav {
display: flex;
align-items: center;
gap: 14px;
font-size: 13px;
}
.headerNav a {
color: var(--pfc-ink-muted);
text-decoration: none;
}
.headerNav a:hover {
color: var(--pfc-ink);
}
/* The one CTA. Publishing happens in Pattern Lab, so this replaces both the
old "Pattern Lab ↗" link and any separate share button. */
.makeCta {
border: 1px solid var(--pfc-led);
background: var(--pfc-led);
color: var(--pfc-on-led) !important;
font-weight: 500;
padding: 8px 16px;
white-space: nowrap;
text-decoration: none;
}
.makeCta:hover {
color: var(--pfc-on-led) !important;
background: #f16c47;
border-color: #f16c47;
}
/* Section switch in the header: Patterns / Decks / Discussions. Underline
rather than a pill, so it reads as "where am I" and not as another filter —
the wall already has tab-shaped controls for sort. */
.sectionNav {
display: flex;
align-items: baseline;
gap: 18px;
margin-left: 14px;
}
.sectionNav a {
padding: 3px 0;
border-bottom: 2px solid transparent;
color: var(--pfc-ink-muted);
font-size: 13.5px;
font-weight: 500;
text-decoration: none;
}
.sectionNav a:hover {
color: var(--pfc-ink);
}
.sectionNav a[data-active="true"] {
color: var(--pfc-ink);
border-bottom-color: var(--pfc-led);
}
.userChip {
font-family: var(--pf-mono, monospace);
font-size: 12px;
color: var(--pfc-ink) !important;
text-decoration: none;
}
.signOutLink {
appearance: none;
border: 0;
padding: 0;
background: none;
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
letter-spacing: 0.06em;
color: var(--pfc-ink-faint);
cursor: pointer;
}
.signOutLink:hover {
color: var(--pfc-ink);
}
/* Deck size, in the header, as a chip — MOBILE ONLY.
On desktop the dock along the bottom carries the count and everything you
would do with it, so a second copy in the far corner was the same fact said
twice. A phone has no dock (the tab bar owns that edge), so there this chip
is the deck's only way in. */
.deckChip {
display: none;
align-items: center;
gap: 6px;
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
letter-spacing: 0.08em;
border: 1px solid var(--pfc-edge);
background: transparent;
color: var(--pfc-ink);
padding: 6px 10px;
cursor: pointer;
}
@media (max-width: 900px), (hover: none) {
.deckChip {
display: inline-flex;
}
}
.deckChip:hover {
border-color: var(--pfc-led);
color: var(--pfc-led);
}
/* ── Footer ── */
/* globals.css styles the bare `footer` element for the marketing site, cream
ground included — an element selector this module's single class does not
beat on any property it does not itself declare. The background has to be
named explicitly or the dark room gets a cream stripe across the bottom. */
.pageFooter {
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
background: transparent;
padding: 26px 0 30px;
margin-top: 40px;
border-top: 1px solid var(--pfc-hair);
font-family: var(--pf-mono);
font-size: 11px;
letter-spacing: var(--pf-track-mono);
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
.pageFooter a {
color: var(--pfc-ink-faint);
text-decoration: none;
}
.pageFooter a:hover {
color: var(--pfc-led);
}
/* ── Mobile tab bar ──
A phone has no header room for the section nav and no hover for the dock,
so navigation moves to the thumb. Desktop never sees it. */
.tabBar {
display: none;
}
@media (max-width: 768px), (hover: none) {
.tabBar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 40;
display: grid;
grid-template-columns: repeat(4, 1fr);
border-top: 1px solid rgba(244, 239, 230, 0.14);
background: var(--pfc-panel);
}
}
.tabBar a {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
/* ≥44px of thumb, per the design's hit-target rule. */
padding: 12px 0 14px;
font-family: var(--pf-mono, monospace);
font-size: 9.5px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--pfc-ink-muted);
text-decoration: none;
}
.tabBar a[data-active="true"] {
color: var(--pfc-led);
}
/* Glyphs are drawn, not typeset: an icon font would be one more request for
four shapes that are literally squares. */
.tabGlyphBars {
display: flex;
gap: 2px;
}
.tabGlyphBars i {
width: 4px;
height: 16px;
background: currentColor;
display: block;
}
.tabGlyphSquare {
width: 16px;
height: 16px;
border: 2px solid currentColor;
}
.tabGlyphDiamond {
width: 16px;
height: 16px;
border: 2px solid currentColor;
transform: rotate(45deg);
}
.tabGlyphFill {
width: 16px;
height: 16px;
background: currentColor;
}
/* ── Marquee ──
Five equal panels, edge to edge. The first carries the intro; the rest are
patterns, playing. It is the stage: the first thing on /community is the
thing the site is for. */
.marquee {
display: grid;
grid-template-columns: repeat(5, 1fr);
border-bottom: 1px solid var(--pfc-hair);
/* Out of the page gutter, so the panels touch both edges. */
margin: 0 -40px;
}
.marqueePanel {
position: relative;
aspect-ratio: 1 / 2;
background: var(--pfc-well);
overflow: hidden;
border-right: 1px solid rgba(244, 239, 230, 0.1);
display: block;
text-decoration: none;
color: inherit;
}
.marqueePanel:last-child {
border-right: 0;
}
.marqueeIntro {
display: flex;
flex-direction: column;
justify-content: flex-end;
}
/* The pattern behind the intro is scenery, not content — dimmed so the
headline stays the thing you read. */
.marqueeIntroArt {
position: absolute;
inset: 0;
opacity: 0.35;
pointer-events: none;
}
/* Scenery, not a preview: it should reach the panel's edges even when the
panel is not 1:2 (it goes wide and short on a phone). Cropping a backdrop
is fine; letterboxing one into a stripe down the middle is not.
The quarter-turn every other surface applies to landscape patterns is
dropped here too — its 200%×50% box is built for a 1:2 well and collapses
to a ribbon in a wide one, and which way up the wallpaper sits is not a
fact anyone reads. Two classes deep because `.screenUpright img` sets
`contain` further down this file. */
.marqueeIntroArt .screenRotator {
top: 0;
left: 0;
width: 100%;
height: 100%;
transform: none;
}
.marqueeIntroArt .screenUpright img,
.marqueeIntroArt .screenRotator img {
object-fit: cover;
}
.marqueeIntroBody {
position: relative;
padding: 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.marqueeKicker {
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--pfc-led);
}
.marqueeTitle {
margin: 0;
font-size: 30px;
line-height: 1.12;
font-weight: 600;
letter-spacing: -0.02em;
text-wrap: pretty;
}
.marqueeBody {
margin: 0;
font-size: 13.5px;
line-height: 1.6;
color: var(--pfc-ink-2);
}
.marqueeLink {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--pf-mono, monospace);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--pfc-ink);
text-decoration: none;
}
.marqueeLink span {
color: var(--pfc-led);
}
.marqueeCaption {
position: absolute;
left: 14px;
bottom: 12px;
z-index: 2;
display: flex;
flex-direction: column;
gap: 2px;
}
/* The caption sits ON the pattern, so it carries its own contrast rather
than a scrim — a scrim would dim the one thing worth looking at. */
.marqueeCaption b {
font-size: 13.5px;
font-weight: 500;
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.8);
}
.marqueeCaption span {
font-size: 11.5px;
color: var(--pfc-ink-2);
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.8);
}
@media (max-width: 1100px) {
.marquee {
grid-template-columns: repeat(3, 1fr);
}
.marqueePanel:nth-child(n + 4) {
display: none;
}
.marqueeTitle {
font-size: 24px;
}
}
@media (max-width: 768px), (hover: none) {
.marquee {
grid-template-columns: 1fr;
}
/* On a phone the intro panel alone says it; four silent thumbnails below
the fold do not earn a screen each. */
.marqueePanel:not(.marqueeIntro) {
display: none;
}
.marqueeIntro {
aspect-ratio: auto;
min-height: 300px;
}
}
/* ── Wall ──
The feed. Bare canvases in a grid, with their furniture underneath: no card
border, no panel — every rectangle on this page that has an edge is a
pattern. */
.feedWrapper {
flex: 1;
display: flex;
flex-direction: column;
padding: 28px 0 60px;
}
.centeredFeedBody {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.feedGrid {
display: grid;
gap: 14px;
width: 100%;
max-width: 100%;
justify-content: center;
}
/* The infinite-scroll loader's target: a sliver under the grid whose
approach triggers the next batch. Height, not emptiness — a 0px element
never intersects. */
.feedSentinel {
width: 100%;
height: 1px;
}
.feedEndNote {
margin: 28px 0 0;
text-align: center;
font-family: var(--pf-mono, monospace);
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
/* ── Feed controls ── */
.feedControls {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 18px;
}
.feedCount {
font-family: var(--pf-mono, monospace);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--pfc-led);
white-space: nowrap;
}
.sortTabs {
display: flex;
gap: 18px;
font-size: 13.5px;
font-weight: 500;
}
.sortTabs a {
padding-bottom: 4px;
border-bottom: 2px solid transparent;
color: var(--pfc-ink-muted);
text-decoration: none;
white-space: nowrap;
}
.sortTabs a:hover {
color: var(--pfc-ink);
}
.sortTabs a[data-active="true"] {
color: var(--pfc-ink);
border-bottom-color: var(--pfc-led);
}
.controlRule {
width: 1px;
height: 16px;
background: rgba(244, 239, 230, 0.18);
}
.filterChip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
color: var(--pfc-ink-muted);
text-decoration: none;
white-space: nowrap;
}
.filterChip:hover {
color: var(--pfc-ink);
}
.filterChip[data-active="true"] {
color: var(--pfc-led);
}
/* Card size has no widget — Ctrl+scroll (or pinch) IS the control, so this
hint is its entire discoverability. Quiet on purpose. */
.zoomHint {
margin-left: auto;
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
color: var(--pfc-ink-faint);
white-space: nowrap;
}
/* ── Pattern card ── */
.card {
display: flex;
flex-direction: column;
gap: 8px;
border: 0;
background: transparent;
text-decoration: none;
color: inherit;
box-sizing: border-box;
}
.cardThumb {
aspect-ratio: 1 / 2;
position: relative;
background: var(--pfc-well);
overflow: hidden;
}
/* A hovered card lifts by lighting its own edge — nothing moves, because the
grid is dense and a transform would shove its neighbours. */
.card:hover .cardThumb {
outline: 1px solid var(--pfc-edge);
outline-offset: -1px;
}
.screenRotator {
position: absolute;
top: 50%;
left: 50%;
width: 200%;
height: 50%;
transform: translate(-50%, -50%) rotate(-90deg);
transform-origin: center center;
background: var(--pfc-well);
overflow: hidden;
z-index: 1;
}
/* Patterns composed for a portrait frame (`// @matrix 64x128`) are already the
right way up for these 1:2 cards — rotating them again would lay them on
their side. Same box, no transform. Landscape and legacy patterns keep using
.screenRotator above. */
.screenUpright {
position: absolute;
inset: 0;
background: var(--pfc-well);
overflow: hidden;
z-index: 1;
}
.screenUpright img,
.screenUpright iframe,
.screenRotator img,
.screenRotator iframe,
.cardHoverIframe {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
height: 100%;
border: 0;
margin: 0;
padding: 0;
object-fit: fill;
image-rendering: pixelated;
}
/* The rotator's box is built to match a landscape pattern exactly, so stretching
is a no-op there. An upright pattern can be any shape (64×128, 64×64, …), so
contain it rather than distorting it to the card. */
.screenUpright img {
object-fit: contain;
}
.cardHoverIframe {
z-index: 2;
background: transparent;
pointer-events: none;
}
/* The default well for a bare PatternCanvas: its own 1:2 slot. */
.canvasWell {
position: relative;
aspect-ratio: 1 / 2;
background: var(--pfc-well);
overflow: hidden;
}
/* …and the variant for a canvas dropped INTO an already-positioned well —
a card thumb, a marquee panel, an alert chip — where the parent owns the
shape and the badges sit beside the canvas as siblings. */
.canvasFill {
position: absolute;
inset: 0;
background: var(--pfc-well);
overflow: hidden;
}
.cardThumbNote {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
color: var(--pfc-ink-faint);
font-family: var(--pf-mono, monospace);
font-size: 11px;
text-align: center;
}
/* Badges ON the canvas: the card has no furniture left to hang them from,
and "runs on hardware" belongs to the pattern, not to its caption. */
.cardHwBadge {
position: absolute;
top: 8px;
left: 8px;
z-index: 3;
font-family: var(--pf-mono, monospace);
font-size: 10px;
line-height: 1.5;
color: var(--pfc-on-led);
background: var(--pfc-led);
padding: 1px 5px;
}
/* Only for patterns young enough that "when" is still news (<5 days). */
.ageChip {
position: absolute;
top: 8px;
right: 8px;
z-index: 3;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink);
background: rgba(11, 11, 12, 0.72);
border: 1px solid rgba(244, 239, 230, 0.28);
padding: 1px 5px;
}
.cardMeta {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.cardTitle {
display: flex;
align-items: center;
gap: 6px;
font-size: 13.5px;
font-weight: 500;
color: var(--pfc-ink);
line-height: 1.3;
min-width: 0;
}
.cardTitleText {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* One row: the handle takes what it needs and truncates, the stats are fixed
facts and keep their space at the right. */
.cardByline {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 12px;
color: var(--pfc-ink-muted);
min-width: 0;
line-height: 1.2;
}
.cardByline .userLink {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cardStats {
display: inline-flex;
align-items: baseline;
gap: 8px;
margin-left: auto;
flex: none;
font-variant-numeric: tabular-nums;
font-family: var(--pf-mono);
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
.cardDate {
font-family: var(--pf-mono, monospace);
font-size: 10px;
color: var(--pfc-ink-faint);
flex: none;
}
/* ── Small view ──
Half-size cards. The canvas keeps its 1:2 shape, so only the text comes
down — at ~104px wide a 13.5px title and a full byline wrap into the next
card's worth of height. */
.feedGrid[data-view="small"] .cardMeta {
gap: 2px;
}
.feedGrid[data-view="small"] .cardTitle {
font-size: 11.5px;
gap: 4px;
}
.feedGrid[data-view="small"] .cardByline {
font-size: 10px;
gap: 4px;
}
.feedGrid[data-view="small"] .cardDate,
.feedGrid[data-view="small"] .cardStats,
.feedGrid[data-view="small"] .frameChip {
display: none;
}
.feedGrid[data-view="small"] .cardHwBadge,
.feedGrid[data-view="small"] .ageChip {
font-size: 9px;
padding: 0 4px;
top: 6px;
}
.feedGrid[data-view="small"] .cardHwBadge {
left: 6px;
}
.feedGrid[data-view="small"] .ageChip {
right: 6px;
}
.feedGrid[data-view="small"] .knobOverlay {
left: 4px;
right: 4px;
gap: 4px;
padding: 5px 6px;
}
/* ── Knob overlay ──
Dodges to whichever half of the canvas the cursor is not in. */
.knobOverlay {
position: absolute;
left: 8px;
right: 8px;
z-index: 4;
display: flex;
flex-direction: column;
gap: 6px;
background: rgba(9, 9, 11, 0.92);
backdrop-filter: blur(8px);
border: 1px solid rgba(244, 239, 230, 0.2);
border-radius: 0;
padding: 8px 10px;
transition: opacity 150ms ease, top 220ms cubic-bezier(0.16, 1, 0.3, 1), bottom 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.overlayBottom {
bottom: 8px;
top: auto;
}
.overlayTop {
top: 8px;
bottom: auto;
}
.knobZoneBarInMeta {
display: flex;
background: rgba(244, 239, 230, 0.1);
overflow: hidden;
border: 1px solid rgba(244, 239, 230, 0.2);
width: 100%;
}
.knobSegmentInMeta {
flex: 1;
text-align: center;
padding: 3px 0;
font-family: var(--pf-mono, monospace);
font-size: 10px;
font-weight: 500;
color: var(--pfc-ink-faint);
border-right: 1px solid rgba(244, 239, 230, 0.15);
transition: all 120ms ease;
}
.knobSegmentInMeta:last-child {
border-right: 0;
}
.activeKnobSegment {
background: var(--pfc-ink);
color: var(--pfc-ground);
}
.metaKnobStatus {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
font-family: var(--pf-mono, monospace);
}
.metaKnobInfoRow {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
}
.metaKnobLabel {
color: var(--pfc-ink-faint);
white-space: nowrap;
}
.metaKnobVal {
color: var(--pfc-ink);
font-weight: 500;
}
.metaKnobTrack {
width: 100%;
height: 5px;
background: rgba(244, 239, 230, 0.2);
overflow: hidden;
}
.metaKnobFill {
height: 100%;
background: var(--pfc-ink);
transition: width 80ms ease-out;
}
/* Add-to-deck on a wall card.
The card is one big link, so this floats over the canvas rather than sitting
in the caption: it cannot reflow the grid, and it stays out of the knob
overlay's way (which dodges to whichever half the cursor is not in).
It is never fully hidden. On the old cream page a white-framed card made an
empty corner obviously empty, so revealing this on hover was enough; on a
frameless card in a dark room a hover-only, near-black square on a near-black
well is an affordance nobody finds. So it sits at rest, quiet, and comes up
to full weight when the card is hovered. */
.cardDeckBtn {
position: absolute;
right: 6px;
z-index: 3;
width: 29px;
height: 29px;
display: grid;
place-items: center;
font: inherit;
/* The marks are SVG (see PatternCard's ICON), so nothing here depends on
font metrics: a viewBox is centred by geometry and the pair cannot drift
apart the way ♥ and + did when they were glyphs. */
line-height: 0;
color: var(--pfc-ink);
/* Dense enough to read as an object against a lit pattern, not just against
the unlit well. */
background: rgba(11, 11, 12, 0.82);
border: 1px solid rgba(244, 239, 230, 0.45);
cursor: pointer;
opacity: 0.5;
/* Same easing as .knobOverlay, so the pair reads as one system moving. */
transition: opacity 0.15s, background 0.15s, border-color 0.15s,
top 220ms cubic-bezier(0.16, 1, 0.3, 1), bottom 220ms cubic-bezier(0.16, 1, 0.3, 1);
backdrop-filter: blur(2px);
}
.deckBottom {
bottom: 6px;
top: auto;
}
.deckTop {
top: 6px;
bottom: auto;
}
.card:hover .cardDeckBtn,
.cardDeckBtn:focus-visible {
opacity: 1;
}
.cardDeckBtn:hover {
background: var(--pfc-led);
border-color: var(--pfc-led);
color: var(--pfc-on-led);
}
.cardDeckBtnOn {
opacity: 1;
background: rgba(232, 85, 46, 0.92);
border-color: var(--pfc-led);
color: var(--pfc-on-led);
}
/* The card's corner marks: block so the grid centres the box itself rather
than sitting it on a text baseline. */
.cardDeckBtn svg {
display: block;
}
/* Busy stand-in for the deck button — a dot that keeps the box the same size
as the mark it replaces, so nothing jumps while a header is fetched. */
.cardBtnBusy {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
opacity: 0.7;
}
/* The card heart: same object as the deck button, one slot to its left when
the + is present, the corner itself when it is not. */
.cardLikeBtnBeside {
right: 41px;
}
.cardLikeBtnOn {
opacity: 1;
color: var(--pfc-led);
border-color: var(--pfc-led);
}
.cardLikeBtnOn:hover {
color: var(--pfc-on-led);
}
.cardDeckNote {
position: absolute;
right: 6px;
z-index: 3;
max-width: calc(100% - 12px);
padding: 3px 7px;
font-family: var(--pf-mono, monospace);
font-size: 10px;
line-height: 1.4;
color: var(--pfc-ink);
background: rgba(11, 11, 12, 0.86);
}
/* Sits just clear of the button, on whichever edge it went to. */
.deckNoteBottom {
bottom: 40px;
top: auto;
}
.deckNoteTop {
top: 40px;
bottom: auto;
}
/* Coarse pointers have no hover, so the button has to be there already. */
@media (hover: none) {
.cardDeckBtn {
opacity: 1;
}
}
/* ── Deck dock ──
Fixed to the bottom of the window across the community: the deck is the
running order you are assembling while you browse, so it has to stay where
you can see it filling up. */
/* No vertical padding: the slots ARE the dock's height, so the patterns run
edge to edge and the bar is as short as the thing it is showing. */
.deckDock {
/* Two rhythms, named so the bin can cancel one against the other below.
--slot-h is every box in the row AND the bar's own height: they are the
same number by definition, so it is written once. */
--dock-gap: 16px;
--slot-gap: 4px;
--slot-h: 68px;
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 30;
display: flex;
align-items: center;
gap: var(--dock-gap);
padding: 0 40px 0 0;
height: var(--slot-h);
background: rgba(16, 16, 18, 0.96);
border-top: 1px solid rgba(244, 239, 230, 0.16);
backdrop-filter: blur(6px);
box-sizing: border-box;
}
/* While a slot is in the air, nothing else on the bar should look clickable. */
.deckDock[data-dragging="true"] {
cursor: grabbing;
user-select: none;
}
/* A card from the wall is being held over the bar: the whole bar is the
target, so the whole bar answers. */
.deckDock[data-dropping="true"] {
border-top-color: var(--pfc-led);
background: rgba(232, 85, 46, 0.1);
}
.deckDock[data-dropping="true"] .deckDockNext {
border-color: var(--pfc-led);
border-style: solid;
color: var(--pfc-led);
}
.deckDockLabel {
display: flex;
flex-direction: column;
gap: 2px;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--pfc-led);
flex: none;
padding-left: 40px;
}
/* The count that used to be a header chip. It belongs beside the thing it is
counting, not in the far corner of the screen. */
.deckDockLabel b {
font-weight: 400;
font-size: 11px;
letter-spacing: 0.08em;
color: var(--pfc-ink);
font-variant-numeric: tabular-nums;
}
/* A full deck is 20 slots — 754px at full width — so the row is the one
thing on this bar allowed to give: it narrows its slots rather than
pushing the send button off the end.
NOT a scroll container, deliberately. Scrolling one axis makes the other
compute to auto, which would clip the lifted slot's shadow mid-drag, and
dragging is what this row is for. Narrowing is free instead: the drag
measures one step off the live DOM (see DeckDock's startDrag), so the
maths follows whatever width the slots actually end up at. */
.deckDockSlots {
display: flex;
gap: var(--slot-gap);
align-items: stretch;
align-self: stretch;
flex: 0 1 auto;
min-width: 0;
}
/* A filled slot is the pattern itself; the next one is an invitation; the
rest are just the shape of the thing you are filling. */
.deckDockSlot {
position: relative;
width: 34px;
height: var(--slot-h);
background: var(--pfc-well);
display: block;
overflow: visible;
/* Shrinks toward 20px on a narrow screen with a long deck, and sits at 34
the rest of the time. Below 20 a thumbnail stops being a thumbnail. */
flex: 0 1 34px;
min-width: 20px;
cursor: grab;
touch-action: none;
user-select: none;
-webkit-user-select: none;
/* The step-aside motion. The slot being dragged turns this off (below) so
it tracks the pointer exactly. */
transition: transform 180ms cubic-bezier(0.16, 1, 0.3, 1);
}
.deckDockSlot > :first-child {
overflow: hidden;
}
/* The pattern is scenery for the drag, not a target. Left alone, the image
inside is a draggable object in its own right and the browser's own
drag-and-drop wins the gesture. */
.deckDockSlot img,
.deckDockSlot iframe {
-webkit-user-drag: none;
pointer-events: none;
}
/* Lifted: follows the pointer with no easing, and rides above its neighbours
so it reads as picked up rather than swapped. */
.deckDockSlot[data-drag="true"] {
transition: none;
z-index: 2;
cursor: grabbing;
outline: 1px solid var(--pfc-led);
outline-offset: -1px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.55);
}
/* The frame a finished drag is written back in. The DOM order changes and the
translates clear together; easing the second half would drag each slot a
whole step past where it belongs and walk it back. */
.deckDock[data-settling="true"] .deckDockSlot {
transition: none;
}
@media (prefers-reduced-motion: reduce) {
.deckDockSlot {
transition: none;
}
}
/* Its position in the running order, bottom-left, on the pattern. The deck's
whole content is the sequence, so every slot says where it is in it. */
.deckDockOrder {
position: absolute;
left: 0;
bottom: 0;
z-index: 3;
padding: 0 3px;
font-family: var(--pf-mono, monospace);
font-size: 9px;
line-height: 1.5;
color: var(--pfc-ink);
background: rgba(11, 11, 12, 0.72);
pointer-events: none;
}
/* A slot on its way to the bin: it stops pretending it is going to land. */
.deckDockSlot[data-doomed="true"] {
outline-color: var(--pfc-danger);
opacity: 0.55;
}
/* The bin, at the end of the row it empties.
A target rather than a ✕ on every slot: ten hover-only buttons over ten
patterns is ten chances to delete the wrong one, and dragging is already the
gesture that moves them. Quiet until something is actually held over it. */
/* The bin and the hint share one box, so the bin knows exactly how far it may
grow: over the hint, and not one pixel past it. No measuring, no magic
width — the area is the target's own parent. */
/* The bin and the hint share one box, so the bin knows exactly how far it may
grow: over the hint, and not one pixel past it. No measuring, no magic
width — the area is the target's own parent. */
.deckDockBinArea {
position: relative;
display: flex;
align-items: center;
align-self: stretch;
/* The bin is the end of the slot row, but it is a SIBLING of that row, so
the dock's own 16px gap sits between them on top of anything set here.
Cancel it and put the row's gap back, so ✕ sits the same distance from +
as + sits from the slot before it. */
margin-left: calc(var(--slot-gap) - var(--dock-gap));
min-width: 0;
}
/* Taken out of the flow and given an explicit width, because `width` animates
and `inset: 0` does not: the bin has to GROW across the hint, not appear at
full size. The hint keeps its own place in the flex row and just gets
covered. */
.deckDockTrash {
position: absolute;
left: 0;
top: 0;
/* Stated, not inferred from top/bottom: as an absolutely positioned box its
height was whatever its parent happened to stretch to, which is one more
thing that can quietly disagree with the row it is supposed to match. */
height: var(--slot-h);
width: 34px;
box-sizing: border-box;
display: grid;
place-items: center;
padding: 0;
font: inherit;
font-size: 13px;
line-height: 1;
color: var(--pfc-danger);
background: transparent;
border: 1px dashed rgba(232, 107, 94, 0.4);
cursor: default;
transition: width 220ms cubic-bezier(0.16, 1, 0.3, 1), background 140ms ease,
border-color 140ms ease, color 140ms ease;
}
/* Clears the bin at its resting width. */
.deckDockBinArea .deckDockCount {
margin-left: 44px;
}
/* In the air: the bin spreads across the hint, which has nothing to say
mid-drag. `visibility` rather than `display` so the hint keeps holding the
width the bin is expanding into. */
.deckDock[data-dragging="true"] .deckDockTrash {
width: 100%;
border-style: solid;
border-color: rgba(232, 107, 94, 0.55);
background: rgba(232, 107, 94, 0.06);
}
.deckDock[data-dragging="true"] .deckDockCount {
visibility: hidden;
}
.deckDockTrash:enabled {
cursor: pointer;
}
.deckDockTrash:enabled:hover {
border-color: var(--pfc-danger);
border-style: solid;
}
.deckDockTrash:disabled {
opacity: 0.25;
}
/* Asked once, about the whole deck. Filled in, because the next press is not
reversible. */
.deckDockTrash[data-confirm="true"] {
background: var(--pfc-danger);
border-color: var(--pfc-danger);
border-style: solid;
color: var(--pfc-ground);
}
.deckDockCount[data-warn="true"] {
color: var(--pfc-danger);
}
/* Held over it: the target fills in. Has to out-specify the dragging rule
above — that one is (0,3,0) and would otherwise keep its 6%-alpha wash no
matter what is hovering. */
.deckDock[data-dragging="true"] .deckDockTrash[data-armed="true"],
.deckDockTrash[data-armed="true"] {
background: var(--pfc-danger);
border-color: var(--pfc-danger);
border-style: solid;
color: var(--pfc-ground);
}
@media (prefers-reduced-motion: reduce) {
.deckDockTrash {
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
}
}
/* The ordinal is the slot's content: without a canvas to show, the number IS
the information — this pattern plays third. */
.deckDockSlotIndex {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: var(--pf-mono, monospace);
font-size: 9.5px;
color: var(--pfc-led);
}
/* One box, three meanings. A filled slot, an empty place and the bin are the
same rectangle to the pixel — the row has a rhythm and nothing gets to sit
outside it. Only the border and what is inside them differ.
(The bin sets its own box just above: it is positioned, because it grows.) */
.deckDockNext,
.deckDockEmpty {
width: 34px;
height: var(--slot-h);
align-self: stretch;
/* Same give as a filled slot, or the row loses its rhythm the moment it
starts to narrow — the empty places would stay wide while the patterns
shrank around them. */
flex: 0 1 34px;
min-width: 20px;
box-sizing: border-box;
display: grid;
place-items: center;
font: inherit;
line-height: 1;
padding: 0;
background: transparent;
}
.deckDockNext {
border: 1px dashed rgba(244, 239, 230, 0.26);
color: var(--pfc-ink-faint);
/* Matched to the ✕ by eye rather than by number: a "+" carries far less
ink than a "✕" at the same size and reads as the smaller of the two. */
font-size: 17px;
}
.deckDockEmpty {
border: 1px dashed rgba(244, 239, 230, 0.16);
}
.deckDockCount {
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
color: var(--pfc-ink-muted);
white-space: nowrap;
/* First to go when the row runs out of width: it is a hint, and everything
else on the bar is a control. */
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.deckDockSpacer {
flex: 1;
}
.deckDockLink {
font-size: 12.5px;
color: var(--pfc-ink-muted);
background: none;
border: 0;
padding: 0;
font-family: inherit;
cursor: pointer;
white-space: nowrap;
flex: none;
}
.deckDockLink:hover {
color: var(--pfc-ink);
}
.deckDockSend {
appearance: none;
display: inline-flex;
align-items: center;
border: 1px solid var(--pfc-led);
background: var(--pfc-led);
color: var(--pfc-on-led);
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 9px 16px;
cursor: pointer;
white-space: nowrap;
flex: none;
text-decoration: none;
}
.deckDockSend:hover {
color: var(--pfc-on-led);
}
.deckDockSend:disabled {
opacity: 0.45;
cursor: default;
}
/* ── Build, reported in the bar ──
The build takes a couple of seconds and produces something you act on, so
it says so where the button was rather than opening a window to say it. */
.buildRunningNote,
.buildDoneNote,
.buildFailNote {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
letter-spacing: 0.06em;
white-space: nowrap;
flex: none;
}
.buildRunningNote {
color: var(--pfc-ink-muted);
}
/* The expectation, set quieter than the clock beside it — it is context, not
news, right up until it becomes news. */
.buildRunningNote em {
font-style: normal;
color: var(--pfc-ink-faint);
}
.buildRunningNote[data-slow="true"] em {
color: var(--pfc-led);
}
.buildDoneNote {
color: var(--pfc-led);
}
.buildFailNote {
color: var(--pfc-danger);
}
/* The one thing the bar has no room for. Opens directly above it, so it still
reads as part of the dock rather than as somewhere else. */
.dockLog {
position: fixed;
left: 0;
right: 0;
bottom: var(--slot-h, 68px);
z-index: 30;
max-height: 40vh;
overflow: auto;
padding: 12px 40px;
background: rgba(16, 16, 18, 0.98);
border-top: 1px solid rgba(232, 107, 94, 0.4);
backdrop-filter: blur(6px);
}
.dockLog pre {
margin: 0;
font-family: var(--pf-mono, monospace);
font-size: 11.5px;
line-height: 1.5;
color: var(--pfc-ink-2);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* The dock overlaps the page, so the page has to end above it. */
.dockSpacer {
height: 84px;
flex: none;
}
@media (max-width: 900px), (hover: none) {
/* No dock on a phone — the header chip and the deck panel carry it, and the
tab bar already owns the bottom edge. */
.deckDock,
.dockSpacer {
display: none;
}
}
/* ── Empty / notice ── */
.empty {
padding: 48px 0;
text-align: center;
color: var(--pfc-ink-muted);
font-size: 14px;
}
/* The designed empty states: a titled panel, not a shrug. */
.emptyPanel {
border: 1px solid rgba(244, 239, 230, 0.14);
padding: 28px 28px 30px;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 620px;
margin: 24px 0;
}
.emptyKicker {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
.emptyTitle {
font-size: 15px;
font-weight: 500;
color: var(--pfc-ink);
}
.emptyBody {
font-size: 13px;
line-height: 1.6;
color: var(--pfc-ink-muted);
}
.emptyCta {
font-size: 13px;
color: var(--pfc-led) !important;
margin-top: 4px;
text-decoration: none;
}
.notice {
max-width: 560px;
margin: 80px auto;
padding: 28px;
border: 1px solid rgba(244, 239, 230, 0.16);
background: var(--pfc-panel);
text-align: center;
font-size: 14.5px;
line-height: 1.7;
}
.notice a {
color: var(--pfc-led);
}
/* ── Buttons ── */
.btn {
appearance: none;
border: 1px solid var(--pfc-edge-strong);
border-radius: 0;
background: transparent;
color: var(--pfc-ink);
font-family: inherit;
font-size: 13px;
padding: 8px 14px;
cursor: pointer;
}
.btn:hover:not(:disabled) {
border-color: var(--pfc-ink);
}
.btn:disabled {
opacity: 0.5;
cursor: default;
}
/* Cream solid — the "this leaves the page and touches your board" weight. */
.btnPrimary {
composes: btn;
background: var(--pfc-ink);
border-color: var(--pfc-ink);
color: var(--pfc-ground);
font-weight: 500;
}
.btnPrimary:hover:not(:disabled) {
background: #fffdf7;
border-color: #fffdf7;
}
.btnAccent {
composes: btn;
background: var(--pfc-led);
border-color: var(--pfc-led);
color: var(--pfc-on-led);
font-weight: 500;
}
.btnAccent:hover:not(:disabled) {
background: #f16c47;
border-color: #f16c47;
}
.btnDanger {
composes: btn;
border-color: rgba(232, 107, 94, 0.55);
color: var(--pfc-danger);
}
.btnDanger:hover:not(:disabled) {
background: var(--pfc-danger);
border-color: var(--pfc-danger);
color: var(--pfc-ground);
}
.btnSmall {
appearance: none;
border: 1px solid var(--pfc-edge-strong);
border-radius: 0;
background: transparent;
color: var(--pfc-ink);
font-family: inherit;
font-size: 11.5px;
padding: 3px 9px;
cursor: pointer;
}
.btnSmall:hover:not(:disabled) {
border-color: var(--pfc-ink);
}
.btnSmall:disabled {
opacity: 0.5;
cursor: default;
}
.btnSmallDanger {
composes: btnSmall;
border-color: rgba(232, 107, 94, 0.55);
color: var(--pfc-danger);
}
.btnSmallDanger:hover:not(:disabled) {
background: var(--pfc-danger);
border-color: var(--pfc-danger);
color: var(--pfc-ground);
}
/* ── Detail page ── */
.detailWrap {
display: flex;
flex-direction: column;
gap: 26px;
padding: 18px 0 60px;
}
.breadcrumb {
font-size: 13px;
color: var(--pfc-ink-muted);
text-decoration: none;
}
.breadcrumb:hover {
color: var(--pfc-ink);
}
.detailLayout {
display: grid;
grid-template-columns: minmax(0, 4.4fr) minmax(0, 4.6fr);
gap: 24px;
align-items: stretch;
}
.detailLeftCol {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.matrixFrame {
position: relative;
aspect-ratio: 1 / 2;
width: 100%;
max-width: 320px;
background: var(--pfc-well);
overflow: hidden;
border: 1px solid rgba(244, 239, 230, 0.2);
}
.matrixFrame iframe {
display: block;
width: 100%;
height: 100%;
border: 0;
}
.errorBox {
margin-top: 10px;
width: 100%;
max-width: 320px;
padding: 8px 12px;
border: 1px solid rgba(232, 107, 94, 0.5);
background: rgba(232, 107, 94, 0.08);
color: var(--pfc-danger);
font-family: var(--pf-mono, monospace);
font-size: 12px;
white-space: pre-wrap;
}
.knobs {
margin: 18px 0 0;
width: 100%;
max-width: 460px;
display: grid;
gap: 10px;
}
.knobLine {
display: grid;
grid-template-columns: 70px minmax(0, 1fr) 60px;
align-items: center;
gap: 8px;
font-family: var(--pf-mono, monospace);
font-size: 11px;
}
.knobLine span:first-child {
color: var(--pfc-ink-muted);
text-align: right;
padding-right: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.knobLine input[type="range"] {
width: 100%;
margin: 0;
accent-color: var(--pfc-led);
cursor: pointer;
}
.knobValue {
text-align: left;
font-weight: 500;
color: var(--pfc-ink);
font-variant-numeric: tabular-nums;
padding-left: 4px;
}
.actionRow {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
margin: 20px 0 0;
width: 100%;
max-width: 440px;
}
/* ── Code panel ── */
.editorWrap {
border: 1px solid rgba(244, 239, 230, 0.16);
overflow: hidden;
height: 100%;
/* Column so the tab bar sits above an editor that fills the rest. */
display: flex;
flex-direction: column;
min-height: 0;
background: var(--pfc-panel);
}
.codeTabs {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 8px;
background: var(--pfc-well);
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
flex: none;
}
.codeTabs button {
appearance: none;
border: 0;
background: transparent;
color: rgba(255, 255, 255, 0.55);
font-family: var(--pf-mono, monospace);
font-size: 11.5px;
padding: 4px 10px;
cursor: pointer;
white-space: nowrap;
}
.codeTabs button:hover {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
.codeTabs button[data-active="true"] {
color: #fff;
background: rgba(255, 255, 255, 0.14);
}
.codeTabSpacer {
flex: 1;
}
.codeTabAction {
color: var(--pfc-led) !important;
}
.codeTabAction:hover {
background: rgba(232, 85, 46, 0.22) !important;
}
.editorBody {
flex: 1;
min-height: 480px;
}
.codeFootNote {
flex: none;
margin: 0;
padding: 8px 12px;
background: var(--pfc-well);
border-top: 1px solid rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.62);
font-size: 11.5px;
line-height: 1.5;
}
.codeFootNote code {
color: var(--pfc-led);
font-family: var(--pf-mono, monospace);
}
/* ── Detail meta ── */
.metaBlock {
padding-top: 20px;
border-top: 1px solid rgba(244, 239, 230, 0.14);
max-width: 100%;
}
.metaTitleRow {
display: flex;
align-items: center;
/* Sits right beside the title on purpose: the meta block is full-width, so
pushing it to the far edge put it off in empty space where nobody saw it. */
gap: 14px;
flex-wrap: wrap;
}
.metaTitle {
font-size: 26px;
font-weight: 600;
letter-spacing: -0.02em;
}
.metaByline {
margin-top: 8px;
font-size: 13px;
color: var(--pfc-ink-muted);
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
}
.metaByline a {
color: var(--pfc-ink);
}
.forkNote {
color: var(--pfc-led);
}
.forkNote a {
color: var(--pfc-led) !important;
}
.hwNote {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--pfc-led);
}
/* Sits in the meta row, styled to recede: reporting should be findable without
being an invitation. */
.reportLink {
border: 0;
padding: 0;
background: none;
font: inherit;
color: var(--pfc-ink-faint);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.reportLink:hover {
color: var(--pfc-ink);
}
/* Which decks carry this pattern — the pattern's own record of being chosen,
which is a different fact from how many people liked it. */
.inDecks {
margin-top: 10px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 13px;
}
.inDecksLabel {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--pfc-led);
}
.inDecksChip {
border: 1px solid rgba(244, 239, 230, 0.2);
padding: 2px 9px;
color: var(--pfc-ink);
text-decoration: none;
}
.inDecksChip:hover {
border-color: var(--pfc-led);
color: var(--pfc-led);
}
.metaDescription {
margin-top: 14px;
font-size: 14.5px;
line-height: 1.65;
white-space: pre-wrap;
color: var(--pfc-ink-2);
max-width: 72ch;
}
.ownerBar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-top: 16px;
padding: 12px 14px;
border: 1px dashed rgba(244, 239, 230, 0.22);
background: rgba(244, 239, 230, 0.03);
}
.ownerBarSpacer {
flex: 1;
}
/* ── Chips ── */
.hwChip {
flex: none;
display: inline-block;
font-family: var(--pf-mono, monospace);
font-size: 10px;
font-weight: 500;
line-height: 1.5;
letter-spacing: 0.04em;
color: var(--pfc-on-led);
background: var(--pfc-led);
border: 1px solid var(--pfc-led);
padding: 0 6px;
white-space: nowrap;
vertical-align: 1px;
}
.forkChip {
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-led);
border: 1px solid var(--pfc-led-edge);
padding: 1px 7px;
}
/* Shown only for patterns composed for something other than the stock panel. */
.frameChip {
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.04em;
color: var(--pfc-ink-muted);
border: 1px solid rgba(244, 239, 230, 0.28);
padding: 1px 7px;
white-space: nowrap;
}
/* Same family as .frameChip, dashed and muted rather than alarming:
unlisted/private are states the owner chose, not warnings. */
.visChip {
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink-muted);
border: 1px dashed rgba(244, 239, 230, 0.35);
padding: 1px 7px;
white-space: nowrap;
}
.selfChip {
font-family: var(--pf-mono, monospace);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink-muted);
border: 1px solid var(--pfc-edge-strong);
padding: 1px 7px;
}
/* One moderator-pinned post at the top of the board. Solid, like .hwChip —
the strongest chip in the family, because it is the one the site itself is
saying rather than reporting. */
.noticeChip {
display: inline-block;
font-family: var(--pf-mono, monospace);
font-size: 10px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ground);
background: var(--pfc-ink);
border: 1px solid var(--pfc-ink);
padding: 1px 7px;
margin-right: 8px;
vertical-align: 2px;
white-space: nowrap;
}
/* ── Provenance ──
How the pattern came to exist. Quiet: this is a record, not a badge to
earn, and a pattern with none of it is not being accused of anything. */
.provenance {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 12px;
}
.provenanceMade,
.provenanceSignal {
font-family: var(--pf-mono);
font-size: 10.5px;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 2px 7px;
border: 1px solid rgba(244, 239, 230, 0.18);
color: var(--pfc-ink-muted);
cursor: help;
}
/* The author's own declaration reads a shade stronger than the derived
signals, because it is a claim someone made rather than something we read. */
.provenanceMade {
border-color: rgba(244, 239, 230, 0.4);
color: var(--pfc-ink);
}
/* ── Like button ── */
.likeButton {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 7px 14px;
border: 1px solid var(--pfc-edge-strong);
background: transparent;
color: var(--pfc-ink-muted);
font-family: inherit;
font-size: 13.5px;
font-variant-numeric: tabular-nums;
cursor: pointer;
}
.likeButton:hover {
border-color: var(--pfc-led);
color: var(--pfc-led);
}
.likeButtonOn {
background: rgba(232, 85, 46, 0.12);
border-color: var(--pfc-led);
color: var(--pfc-led);
}
/* ── Firmware ports ──
Somebody else's board finishing the author's pattern. A quiet ledger, not a
leaderboard: handle, date, state — and the author's Pin as the only verb. */
.portsSection {
margin-top: 20px;
border-top: 1px solid rgba(244, 239, 230, 0.1);
padding-top: 12px;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 860px;
}
.portsHead {
display: flex;
align-items: center;
gap: 10px;
}
.portsTitle {
font-family: var(--pf-mono, monospace);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--pfc-ink-muted);
}
.portsList {
list-style: none;
margin: 0;
padding: 0;
}
.portRow {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 0;
border-bottom: 1px solid rgba(244, 239, 230, 0.1);
font-size: 13px;
min-width: 0;
}
.portRow[data-stale="true"] .portHandle {
color: var(--pfc-ink-faint);
}
.portHandle {
font-weight: 500;
color: var(--pfc-ink);
white-space: nowrap;
}
.portDate {
font-family: var(--pf-mono, monospace);
font-size: 10px;
color: var(--pfc-ink-faint);
flex: none;
}
.portChip {
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-led);
border: 1px solid var(--pfc-led-edge);
padding: 0 6px;
}
.portStale {
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.04em;
color: var(--pfc-ink-faint);
border: 1px dashed rgba(244, 239, 230, 0.25);
padding: 0 6px;
}
/* A moderator repaired the C++ under somebody else's name. Not a warning and
not a badge — a note about whose code is actually in there, so it reads
solid rather than borrowing the dashed "expired" look above. */
.portFixed {
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.04em;
color: var(--pfc-ink-2);
border: 1px solid var(--pfc-edge);
padding: 0 6px;
}
.portNote {
color: var(--pfc-ink-muted);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* A performance row carries more than a port does — a title, a shape (cues,
duration) and prose — so it stacks instead of competing for one line. */
.perfRow {
display: flex;
flex-direction: column;
gap: 3px;
padding: 9px 0;
border-bottom: 1px solid rgba(244, 239, 230, 0.1);
font-size: 13px;
min-width: 0;
}
.perfMain {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.perfSub {
display: flex;
align-items: baseline;
gap: 10px;
min-width: 0;
}
/* The recording's identity: never truncated, because it is what tells two
rides apart. The note beside it is what gives way. */
.perfSummary {
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 11.5px;
color: var(--pfc-ink);
white-space: nowrap;
}
.perfNote {
min-width: 0;
color: var(--pfc-ink-muted);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* btnSmall is built for <button>; as an <a> it needs the box behaviour back. */
.perfLink {
display: inline-flex;
align-items: center;
text-decoration: none;
}
@media (max-width: 640px) {
/* The note is the first thing worth losing on a phone — the title, cue
count and duration still fit. */
.perfSub {
flex-direction: column;
gap: 2px;
}
.perfNote {
max-width: 100%;
}
}
/* ── Comments ── */
.commentsBlock {
margin-top: 30px;
max-width: 760px;
}
.commentsTitle {
font-size: 15px;
font-weight: 500;
padding-bottom: 10px;
border-bottom: 1px solid rgba(244, 239, 230, 0.14);
}
.commentItem {
padding: 12px 0;
border-bottom: 1px solid var(--pfc-hair-soft);
font-size: 14px;
}
.commentHead {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 4px;
}
.commentAuthor {
font-weight: 500;
color: var(--pfc-ink);
text-decoration: none;
}
.commentDate {
font-size: 12px;
color: var(--pfc-ink-faint);
}
.commentBody {
line-height: 1.6;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: var(--pfc-ink-2);
}
.commentForm {
margin-top: 16px;
display: grid;
gap: 10px;
}
.commentForm textarea {
min-height: 84px;
resize: vertical;
/* Grown by useAutoGrow; this is the ceiling. Replies sit in a list of other
people's replies, so one long one should not push the thread off screen. */
max-height: 60vh;
}
/* Sit in the head row beside the date, muted: available to the person who
wrote it, invisible to everyone else, and never louder than the comment. */
.commentActions {
display: inline-flex;
gap: 8px;
margin-left: auto;
}
.commentActions button {
appearance: none;
border: 0;
padding: 0;
background: none;
font: inherit;
font-size: 11.5px;
color: var(--pfc-ink-faint);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.commentActions button:hover:not(:disabled) {
color: var(--pfc-led);
}
.commentActions button:disabled {
opacity: 0.5;
cursor: default;
}
/* An edited comment says so. Replies sit under it, and one that quietly became
something else makes them read as answers to something never said. */
.commentEdited {
font-size: 11px;
color: var(--pfc-ink-faint);
font-style: italic;
}
.commentEditRow {
display: flex;
gap: 8px;
}
/* ── Forms / modals ── */
.textInput,
.commentForm textarea,
.field select {
width: 100%;
border: 1px solid var(--pfc-edge);
border-radius: 0;
background: var(--pfc-ground);
color: var(--pfc-ink);
font-size: 14px;
font-family: inherit;
padding: 9px 11px;
box-sizing: border-box;
}
.textInput::placeholder,
.commentForm textarea::placeholder {
color: var(--pfc-ink-faint);
}
.textInput:focus,
.commentForm textarea:focus,
.field select:focus {
outline: none;
border-color: var(--pfc-led);
}
.modalOverlay {
position: fixed;
inset: 0;
z-index: 60;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(9, 9, 11, 0.6);
}
.modalCard {
width: min(460px, 100%);
max-height: 90vh;
overflow: auto;
background: var(--pfc-panel);
border: 1px solid var(--pfc-edge);
color: var(--pfc-ink);
}
.modalCardWide {
width: min(720px, 100%);
}
.modalHeader {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 20px;
border-bottom: 1px solid var(--pfc-hair);
font-weight: 500;
font-size: 13.5px;
}
/* The modal's own name, in the accent — mono, uppercase, tiny: it labels the
dialog without competing with what the dialog is about. */
.modalKicker {
font-family: var(--pf-mono, monospace);
font-size: 10px;
font-weight: 400;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--pfc-led);
}
.modalHeader button {
appearance: none;
border: 0;
background: none;
font-size: 16px;
cursor: pointer;
color: var(--pfc-ink-faint);
margin-left: auto;
}
.modalHeader button:hover {
color: var(--pfc-ink);
}
.modalBody {
padding: 16px 20px 18px;
display: grid;
gap: 12px;
}
.modalFoot {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 20px 18px;
border-top: 1px solid var(--pfc-hair);
}
.field {
display: grid;
gap: 5px;
font-size: 13px;
}
.fieldLabel {
font-weight: 500;
}
.fieldHint {
font-size: 12px;
color: var(--pfc-ink-muted);
}
/* A hint that has turned into "this will not save". Same slot, so the field
does not reflow when it flips — only the colour changes. */
.fieldHint[data-trouble="true"] {
color: var(--pfc-danger);
}
.formError {
padding: 8px 10px;
background: rgba(232, 107, 94, 0.1);
border: 1px solid rgba(232, 107, 94, 0.4);
color: var(--pfc-danger);
font-size: 13px;
}
.formNote {
font-size: 12px;
color: var(--pfc-ink-muted);
line-height: 1.5;
}
.formNote a {
color: var(--pfc-ink);
text-decoration: underline;
text-underline-offset: 2px;
}
.authTabs {
display: flex;
gap: 6px;
}
.authTabs button {
flex: 1;
appearance: none;
border: 1px solid var(--pfc-edge);
border-radius: 0;
background: transparent;
color: var(--pfc-ink-muted);
font-family: inherit;
font-size: 13px;
padding: 7px 0;
cursor: pointer;
}
.authTabs button[data-active="true"] {
background: var(--pfc-ink);
border-color: var(--pfc-ink);
color: var(--pfc-ground);
}
.srOnly {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.reasonList {
border: 0;
padding: 0;
margin: 0;
display: grid;
gap: 8px;
}
/* A reason in the report modal: the whole row is the target, and the chosen
one takes the accent border + wash rather than a native radio dot. */
.reasonOption {
position: relative;
display: flex;
align-items: center;
gap: 10px;
border: 1px solid rgba(244, 239, 230, 0.16);
padding: 11px 13px;
font-size: 13.5px;
color: var(--pfc-ink-2);
cursor: pointer;
}
.reasonOption[data-active="true"] {
border-color: var(--pfc-led);
background: var(--pfc-led-wash);
color: var(--pfc-ink);
}
.reasonMark {
width: 10px;
height: 10px;
flex: none;
border: 1px solid rgba(244, 239, 230, 0.4);
}
.reasonOption[data-active="true"] .reasonMark {
background: var(--pfc-led);
border-color: var(--pfc-led);
}
.reasonOption input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.cppInput {
min-height: 220px;
font-family: var(--pf-mono, monospace);
font-size: 12px;
line-height: 1.5;
resize: vertical;
white-space: pre;
overflow-wrap: normal;
overflow-x: auto;
}
.warnNote {
margin: 10px 0 0;
padding: 8px 12px;
border: 1px solid var(--pfc-led-edge);
background: var(--pfc-led-wash);
color: var(--pfc-led);
font-size: 12.5px;
line-height: 1.5;
}
.warnNote code {
font-family: var(--pf-mono, monospace);
}
.confirmRow {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.confirmText {
font-size: 12.5px;
color: var(--pfc-danger);
max-width: 46ch;
line-height: 1.45;
}
.confirmError {
font-size: 12.5px;
color: var(--pfc-danger);
font-weight: 500;
}
/* ── Sign-in gate ──
Never a wall: everything plays without an account, so the prompt appears
inline at the moment something needs keeping, with the knobs left alone. */
.gatePanel {
display: flex;
gap: 14px;
align-items: center;
border: 1px solid rgba(244, 239, 230, 0.16);
background: var(--pfc-panel);
padding: 16px 18px;
max-width: 560px;
}
.gateText {
display: flex;
flex-direction: column;
gap: 3px;
}
.gateTitle {
font-size: 14px;
font-weight: 500;
}
.gateSub {
font-size: 12.5px;
color: var(--pfc-ink-muted);
}
/* ── Firmware build modal ── */
.buildStatusLine {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 500;
color: var(--pfc-ink);
}
.buildSpinner {
flex: none;
width: 14px;
height: 14px;
border: 2px solid rgba(232, 85, 46, 0.25);
border-top-color: var(--pfc-led);
border-radius: 50%;
animation: buildSpin 700ms linear infinite;
}
@keyframes buildSpin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.buildSpinner { animation-duration: 2.4s; }
}
.buildLog {
margin: 0;
max-height: 260px;
overflow: auto;
padding: 10px 12px;
background: var(--pfc-well);
color: var(--pfc-ink-2);
font-family: var(--pf-mono, monospace);
font-size: 11.5px;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* ── Decks ── */
.decksPage,
.deckPage {
display: flex;
flex-direction: column;
padding: 26px 0 60px;
}
.sectionHead {
display: flex;
align-items: baseline;
gap: 14px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.sectionTitle {
margin: 0;
font-size: 24px;
font-weight: 600;
letter-spacing: -0.02em;
}
.sectionLede {
font-size: 13.5px;
color: var(--pfc-ink-muted);
max-width: 76ch;
}
.sectionKicker {
font-family: var(--pf-mono, monospace);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--pfc-led);
}
.sectionLink {
font-family: var(--pf-mono, monospace);
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--pfc-ink);
text-decoration: none;
margin-left: auto;
}
.sectionLink:hover {
color: var(--pfc-led);
}
.deckSection {
padding-bottom: 30px;
margin-bottom: 28px;
border-bottom: 1px solid var(--pfc-hair);
}
.deckSection:last-child {
border-bottom: 0;
margin-bottom: 0;
}
.deckGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
/* A deck is a panel — one of the few raised surfaces here — because it is an
arrangement somebody made, not a single pattern. */
.deckCard {
display: flex;
flex-direction: column;
gap: 12px;
border: 1px solid rgba(244, 239, 230, 0.14);
padding: 14px;
background: var(--pfc-panel);
text-decoration: none;
color: inherit;
box-sizing: border-box;
}
.deckCard:hover {
border-color: var(--pfc-edge-strong);
}
/* First patterns in running order, left to right — the card IS the order. */
.deckStrip {
position: relative;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 4px;
}
.deckStripSlot {
aspect-ratio: 1 / 2;
position: relative;
background: var(--pfc-well);
overflow: hidden;
}
/* The overflow cell: a well with no pattern in it, so it sits a shade above
the lit ones rather than pretending to be one. */
.deckStripMore {
aspect-ratio: 1 / 2;
background: var(--pfc-well-2);
display: grid;
place-items: center;
font-family: var(--pf-mono, monospace);
font-size: 11px;
color: var(--pfc-ink-muted);
}
/* A slot this deck does not have. Darker than the wells, so the strip reads
as "three of five" rather than as a rendering failure. */
.deckStripBlank {
aspect-ratio: 1 / 2;
background: rgba(244, 239, 230, 0.03);
}
.deckStripEmpty {
grid-column: 1 / -1;
aspect-ratio: 5 / 2;
display: flex;
align-items: center;
justify-content: center;
background: var(--pfc-well-2);
color: var(--pfc-ink-faint);
font-family: var(--pf-mono, monospace);
font-size: 11px;
}
.deckCardMeta {
display: flex;
flex-direction: column;
gap: 4px;
}
.deckCardTitleRow {
display: flex;
align-items: baseline;
gap: 8px;
}
.deckCardTitle {
font-size: 15px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.deckCardSlots {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
flex: none;
}
.deckCardUser {
font-size: 12.5px;
color: var(--pfc-ink-muted);
margin-left: auto;
flex: none;
}
.deckCardDesc {
font-size: 12.5px;
line-height: 1.55;
color: var(--pfc-ink-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.deckCardStats {
display: flex;
gap: 12px;
margin-top: 4px;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
/* An anchor that has to sit in a row of buttons and match them. It is a real
link rather than a button because it leaves for the device's own origin —
middle-click and "open in new tab" should work, and a click handler would
take those away. */
.btnLink {
composes: btn;
display: inline-flex;
align-items: center;
text-decoration: none;
/* No line-height override: the buttons beside it inherit the body's, and
resetting it here made the link 6px shorter than everything in the row. */
}
.btnLink[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
/* The accent weight of the same thing — "this leaves the page and touches
your board", which is exactly what it does.
`composes: btn btnAccent` names both on purpose: composing btnAccent alone
emits only two classes here, dropping the btn underneath it, so the fill
arrives without the padding or border that make it a button shape. */
.btnAccentLink {
composes: btn btnAccent;
display: inline-flex;
align-items: center;
text-decoration: none;
}
.btnAccentLink[aria-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
/* ── Firmware notice ──────────────────────────────────────────────────────
A thin strip above the header. Deliberately quiet: it is news, not an
error, and the person reading it came here to look at patterns. It uses
the LED accent only on the tag and the link, so it registers without
turning the top of every page into a warning. */
.fwNotice {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 9px 12px;
margin-bottom: 14px;
border: 1px solid rgba(255, 92, 46, 0.28);
background: rgba(255, 92, 46, 0.06);
font-size: 12.5px;
}
.fwNoticeTag {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-led);
flex: none;
}
.fwNoticeText {
color: var(--pfc-ink-muted);
min-width: 0;
}
.fwNoticeText strong {
color: var(--pfc-ink);
font-weight: 600;
}
.fwNoticeLink {
color: var(--pfc-led);
text-decoration: none;
white-space: nowrap;
flex: none;
}
.fwNoticeLink:hover {
text-decoration: underline;
}
.fwNoticeClose {
appearance: none;
border: 0;
background: none;
color: var(--pfc-ink-faint);
font-size: 13px;
line-height: 1;
padding: 4px;
cursor: pointer;
margin-left: auto;
flex: none;
}
.fwNoticeClose:hover {
color: var(--pfc-ink);
}
@media (max-width: 640px) {
/* The close button keeps its place; the link drops below the text rather
than squeezing it to two words a line. */
.fwNoticeText {
flex: 1 1 100%;
order: 3;
}
}
/* ── Share panel ──────────────────────────────────────────────────────────
The link is shown, not just copied: a button that says "Copied" about an
address you never saw asks for trust it has not earned, and the field is
also the fallback when the clipboard API refuses. */
.shareLead {
margin: 0 0 12px;
font-size: 13px;
line-height: 1.6;
color: var(--pfc-ink-muted);
}
.shareLinkRow {
display: flex;
gap: 8px;
}
.shareLinkField {
flex: 1;
min-width: 0;
appearance: none;
border: 1px solid var(--pfc-edge-strong);
border-radius: 0;
background: var(--pfc-well);
color: var(--pfc-ink);
font-family: var(--pf-mono, monospace);
font-size: 12px;
padding: 8px 10px;
/* Long URLs scroll rather than stretching the dialog. */
text-overflow: ellipsis;
}
.shareLinkField:focus {
outline: none;
border-color: var(--pfc-ink);
}
.shareNote {
margin: 10px 0 0;
font-size: 11.5px;
line-height: 1.6;
color: var(--pfc-ink-faint);
}
.shareAlt {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--pfc-hair);
}
.shareAltLink {
font-size: 12.5px;
color: var(--pfc-ink-muted);
text-decoration: none;
}
.shareAltLink:hover {
color: var(--pfc-led);
}
/* The shipped pack carries its own actions, because unlike a deck card it is
not a link to a detail page — there is nowhere else for it to go. Pushed to
the bottom of the panel so its height still matches the deck cards beside
it whatever the description wraps to. */
.packActions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: auto;
padding-top: 4px;
}
.packActions a {
text-decoration: none;
text-align: center;
}
.packHint {
font-size: 11.5px;
line-height: 1.5;
color: var(--pfc-ink-faint);
}
/* Fresh from the wall: a single dense row under the decks, so the newest work
is one glance away from the curated shelf. */
.freshStrip {
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 12px;
}
@media (max-width: 1100px) {
.freshStrip {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
}
@media (max-width: 768px), (hover: none) {
.freshStrip {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
}
/* ── Deck page ── */
.deckHead {
display: flex;
align-items: flex-end;
gap: 16px;
flex-wrap: wrap;
padding-bottom: 18px;
border-bottom: 1px solid rgba(244, 239, 230, 0.14);
margin-bottom: 20px;
}
.deckHeadTitle {
display: flex;
flex-direction: column;
gap: 6px;
}
.deckHeadTitle h1 {
margin: 0;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.02em;
}
.deckByline {
font-family: var(--pf-mono, monospace);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink-muted);
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.deckByline a {
color: var(--pfc-ink-muted);
}
.deckByline a:hover {
color: var(--pfc-ink);
}
/* The running order: numbered slots. The number sits above the card, not on
it — it belongs to the deck, not to the pattern. */
.deckSlots {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 12px;
}
.deckSlot {
display: flex;
flex-direction: column;
gap: 7px;
min-width: 0;
}
.deckSlotIndex {
font-family: var(--pf-mono, monospace);
font-size: 11px;
color: var(--pfc-ink-faint);
}
/* A slot whose pattern is gone. The deck keeps the position and the name —
the arrangement is the deck author's work; the pattern was its author's. */
.deckGap {
flex: 1;
aspect-ratio: 1 / 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px dashed rgba(244, 239, 230, 0.3);
padding: 10px;
text-align: center;
box-sizing: border-box;
}
.deckGapTitle {
font-size: 12.5px;
color: var(--pfc-ink-muted);
text-decoration: line-through;
}
.deckGapReason {
font-family: var(--pf-mono, monospace);
font-size: 9px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
.deckVisControl {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12.5px;
color: var(--pfc-ink-muted);
}
.deckVisControl select {
border: 1px solid var(--pfc-edge);
background: var(--pfc-ground);
color: var(--pfc-ink);
font-family: inherit;
font-size: 12.5px;
padding: 5px 8px;
}
/* ── Deck panel (modal) ──
Two lists in one dialog: the deck (capped, ordered, buildable) and saved
(unbounded). */
.deckTabs {
display: flex;
gap: 4px;
margin-bottom: 14px;
border-bottom: 1px solid var(--pfc-hair);
}
.deckTabs button {
appearance: none;
border: 0;
background: none;
padding: 6px 10px;
font-family: var(--pf-mono);
font-size: 11px;
letter-spacing: var(--pf-track-mono);
text-transform: uppercase;
color: var(--pfc-ink-faint);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.deckTabs button[data-active="true"] {
color: var(--pfc-ink);
border-bottom-color: var(--pfc-led);
}
.deckList {
list-style: none;
margin: 0 0 14px;
padding: 0;
max-height: 46vh;
overflow-y: auto;
}
.deckRow {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 0;
border-bottom: 1px solid var(--pfc-hair-soft);
font-size: 14px;
}
.deckRow a {
color: var(--pfc-ink);
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.deckRow a:hover {
color: var(--pfc-led);
}
/* The running order is the deck's whole reason to exist, so it is numbered. */
.deckIndex {
flex: none;
width: 18px;
font-family: var(--pf-mono);
font-size: 11px;
color: var(--pfc-ink-faint);
text-align: right;
}
/* ── Profile ── */
.profilePage {
display: flex;
flex-direction: column;
padding: 26px 0 60px;
}
.profileHeader {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 16px;
padding-bottom: 18px;
margin-bottom: 22px;
border-bottom: 1px solid rgba(244, 239, 230, 0.14);
}
.profileName {
display: flex;
align-items: center;
gap: 10px;
font-size: 26px;
font-weight: 600;
letter-spacing: -0.02em;
}
.profileMeta {
margin-top: 8px;
font-family: var(--pf-mono, monospace);
font-size: 12px;
color: var(--pfc-ink-muted);
}
/* Your own profile's right-hand side: the CTA, and on a phone the sign-out
that the header cannot fit. */
.profileSelfActions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 10px;
}
/* Off by default — the header's copy is the one you see on a desktop. */
.profileSignOut {
display: none;
}
/* A profile is an archive, so it wraps and scrolls — unlike the wall's
zoomable grid. */
.profileGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 14px;
}
.profileSectionTitle {
margin: 30px 0 14px;
font-size: 17px;
font-weight: 600;
color: var(--pfc-ink);
display: flex;
align-items: baseline;
gap: 10px;
}
.profileSlotNote {
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
font-weight: 400;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
/* The unused public deck slot, drawn rather than described: two is the whole
allowance, so the empty one is information. */
.deckSlotFree {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed rgba(244, 239, 230, 0.2);
color: var(--pfc-ink-faint);
font-size: 13px;
min-height: 120px;
padding: 16px;
text-align: center;
}
.profileFootNote {
margin-top: 24px;
font-size: 12.5px;
color: var(--pfc-ink-faint);
}
/* ── Alerts ──
Delivery is the next page load — no polling, no sockets. The header link
turns LED-orange while anything is unread, and goes quiet again. */
.notifyLink[data-unread="true"] {
color: var(--pfc-led) !important;
}
.notifyHead {
padding: 24px 0 18px;
}
.notifyHead h1 {
margin: 0 0 6px;
font-size: 22px;
font-weight: 600;
letter-spacing: -0.02em;
}
.notifyHead p {
margin: 0;
font-size: 13px;
color: var(--pfc-ink-muted);
}
.notifyList {
list-style: none;
margin: 0;
padding: 0 0 60px;
max-width: 820px;
}
.notifyRow {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 2px;
border-bottom: 1px solid rgba(244, 239, 230, 0.1);
text-decoration: none;
color: var(--pfc-ink);
}
.notifyRow:hover {
background: rgba(244, 239, 230, 0.03);
}
/* The unread marker keeps its place when read, so rows do not shift as the
visit marks them — it just empties. */
.notifyMark {
flex: none;
width: 7px;
height: 7px;
align-self: center;
border: 1px solid transparent;
}
.notifyRow[data-unread="true"] .notifyMark {
background: var(--pfc-led);
border-color: var(--pfc-led);
}
/* The pattern the alert is about, at thumbnail scale: which one it happened
to is the first thing you want to know. */
.notifyThumb {
position: relative;
flex: none;
width: 22px;
height: 44px;
background: var(--pfc-well);
overflow: hidden;
}
.notifyBody {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.notifySentence {
font-size: 14px;
line-height: 1.4;
color: var(--pfc-ink-2);
}
.notifySentence strong {
font-weight: 500;
color: var(--pfc-ink);
}
.notifySnippet {
font-size: 12.5px;
color: var(--pfc-ink-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.notifyDate {
margin-left: auto;
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 10px;
color: var(--pfc-ink-faint);
}
/* ── Discussions ──
Plain-text threads. Deliberately quiet next to the wall: no previews, no
thumbnails, nothing that needs a sandbox. */
.discussionWrap {
display: flex;
flex-direction: column;
gap: 18px;
max-width: 860px;
width: 100%;
margin: 0 auto;
padding: 26px 0 60px;
}
.backLink {
color: var(--pfc-ink-muted);
font-size: 13px;
text-decoration: none;
}
.backLink:hover {
color: var(--pfc-ink);
}
.introRow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
font-size: 13.5px;
color: var(--pfc-ink-muted);
flex: none;
}
.postComposer {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px;
border: 1px solid rgba(244, 239, 230, 0.14);
background: var(--pfc-panel);
}
.postTitleInput,
.postBodyInput {
width: 100%;
box-sizing: border-box;
border: 1px solid var(--pfc-edge);
background: var(--pfc-ground);
color: var(--pfc-ink);
font-family: inherit;
padding: 11px 12px;
}
.postTitleInput::placeholder,
.postBodyInput::placeholder {
color: var(--pfc-ink-faint);
}
.postTitleInput {
font-size: 15px;
font-weight: 500;
}
.postBodyInput {
font-size: 14px;
line-height: 1.6;
min-height: 180px;
resize: vertical;
}
/* useAutoGrow sets an inline height from scrollHeight; these caps are what
stops it. It reports the full content height regardless, so max-height wins
and the textarea starts scrolling — which is the right ceiling in each case:
an editor that owns the page can take most of the viewport, one inside a
modal has to leave room for the buttons under it. */
.postBodyInput[data-grow="page"] {
max-height: 75vh;
}
.postBodyInput[data-grow="modal"] {
max-height: 42vh;
}
.postTitleInput:focus,
.postBodyInput:focus {
outline: none;
border-color: var(--pfc-led);
}
.composerActions {
display: flex;
align-items: center;
gap: 10px;
}
.composerCount {
flex: 1;
font-family: var(--pf-mono, monospace);
font-size: 11px;
color: var(--pfc-ink-faint);
}
.postList {
list-style: none;
margin: 0;
padding: 0;
border-top: 1px solid rgba(244, 239, 230, 0.14);
}
.postRow {
display: flex;
align-items: baseline;
gap: 16px;
padding: 13px 2px;
border-bottom: 1px solid rgba(244, 239, 230, 0.1);
}
.postRowLink {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
text-decoration: none;
color: inherit;
}
.postRowTitle {
display: flex;
align-items: center;
gap: 7px;
font-size: 14.5px;
font-weight: 500;
color: var(--pfc-ink);
}
.postRowLink:hover .postRowTitle {
color: var(--pfc-led);
}
/* Reply count. Only rendered when there is one, so it never shows a bare 0. */
.postRowCount {
flex: none;
font-family: var(--pf-mono, monospace);
font-size: 10px;
font-weight: 500;
color: var(--pfc-led);
}
.postRowPreview {
font-size: 12.5px;
color: var(--pfc-ink-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.postRowMeta {
flex: none;
display: flex;
align-items: baseline;
gap: 10px;
font-size: 12px;
color: var(--pfc-ink-faint);
}
.postArticle {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(244, 239, 230, 0.14);
}
.postTitle {
font-size: 26px;
font-weight: 600;
line-height: 1.3;
letter-spacing: -0.02em;
margin: 0;
}
.postMeta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 13px;
color: var(--pfc-ink-muted);
}
.postBody {
font-size: 14.5px;
line-height: 1.7;
color: var(--pfc-ink-2);
padding-bottom: 20px;
max-width: 76ch;
}
/* ── Marquee picker (admin) ── */
.featuredRow {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 14px;
}
.featuredSlot {
display: flex;
flex-direction: column;
gap: 7px;
min-width: 0;
}
.featuredControls {
display: flex;
gap: 6px;
}
.featuredControls button {
flex: 1;
}
/* ── The map ──
Directions the project could be taken in, and who is working on which.
Two views of the same facts: a constellation (everything hangs off the
device) and a floor plan (the same nodes as rooms you can walk into). */
.mapPage {
display: flex;
flex-direction: column;
gap: 0;
padding: 22px 0 60px;
}
.mapHead {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.mapTitle {
margin: 0;
font-size: 22px;
font-weight: 600;
letter-spacing: -0.02em;
}
.mapLede {
font-size: 13px;
color: var(--pfc-ink-muted);
}
.viewToggle {
display: flex;
border: 1px solid var(--pfc-edge);
}
.viewToggle button {
appearance: none;
border: 0;
background: transparent;
color: var(--pfc-ink-muted);
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 8px 14px;
cursor: pointer;
}
.viewToggle button[data-active="true"] {
background: var(--pfc-led);
color: var(--pfc-on-led);
}
/* ── Territory editor (admin) ──
A miniature of the constellation you can click, and a list you expand in
place. Editing the map inside a modal would mean not seeing the map. */
.adminStage {
position: relative;
width: 100%;
aspect-ratio: 1440 / 640;
min-height: 220px;
border: 1px solid var(--pfc-hair);
background-color: var(--pfc-ground);
background-image: radial-gradient(rgba(244, 239, 230, 0.05) 1px, transparent 1px);
background-size: 26px 26px;
overflow: hidden;
}
/* Armed = a territory is open, so a click means "put it here". */
.adminStage[data-armed="true"] {
cursor: crosshair;
border-color: var(--pfc-led-edge);
}
.adminStageCore {
position: absolute;
left: 50%;
top: 50%;
width: 26px;
height: 46px;
transform: translate(-50%, -50%);
background: var(--pfc-well);
border: 1px solid rgba(244, 239, 230, 0.35);
box-shadow: 0 0 30px rgba(232, 85, 46, 0.25);
}
.adminStageNode {
position: absolute;
transform: translate(-50%, -50%);
padding: 3px 7px;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
color: var(--pfc-ink-muted);
background: var(--pfc-panel);
border: 1px solid var(--pfc-hair);
pointer-events: none;
transition: left 160ms ease, top 160ms ease;
}
.adminStageNode[data-active="true"] {
color: var(--pfc-on-led);
background: var(--pfc-led);
border-color: var(--pfc-led);
}
.adminList {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.adminRow {
border: 1px solid var(--pfc-hair);
background: var(--pfc-panel);
}
.adminRow[data-open="true"] {
border-color: var(--pfc-edge-strong);
}
.adminRowHead {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
padding: 12px 14px;
}
.adminRowBody {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0 14px 14px;
border-top: 1px solid var(--pfc-hair-soft);
margin-top: 2px;
padding-top: 14px;
}
/* Refusals live in the row that caused them, not in a banner at the top of a
list you have already scrolled past. */
.adminRow .formError {
margin: 0 14px 14px;
}
.adminRow[data-open="true"] .formError {
margin-top: -2px;
}
/* Inside a row, a field takes what it needs; the title one takes the rest. */
.fieldWide {
flex: 1;
min-width: 0;
}
/* ── Latest strip ──
The newest threads anywhere, one line each, above the geography. */
.recentStrip {
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 14px;
}
.recentStrip > .workingLabel {
padding-bottom: 6px;
}
.recentRow {
display: flex;
align-items: baseline;
gap: 10px;
padding: 7px 2px;
border-bottom: 1px solid var(--pfc-hair-soft);
text-decoration: none;
color: inherit;
min-width: 0;
}
.recentRow:hover .threadCardTitle {
color: var(--pfc-led);
}
.recentRow .threadCardByline {
margin-left: auto;
flex: none;
}
/* Sits beside the counts in the drawer head, quiet — a door, not a button. */
.drawerAll {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
text-decoration: none;
}
.drawerAll:hover {
color: var(--pfc-led);
}
/* ── Constellation ──
Node coordinates are stored against a 1440×640 stage and laid out here as
percentages, so one arrangement survives every viewport. */
.stage {
position: relative;
width: 100%;
aspect-ratio: 1440 / 640;
min-height: 420px;
overflow: hidden;
border: 1px solid var(--pfc-hair);
background-color: var(--pfc-ground);
background-image: radial-gradient(rgba(244, 239, 230, 0.05) 1px, transparent 1px);
background-size: 26px 26px;
/* Draggable. Without this a drag across the map selects the node labels
instead of moving anything, which is the usual way a pan feels broken. */
cursor: grab;
user-select: none;
touch-action: none;
}
.stage[data-panning="true"] {
cursor: grabbing;
}
/* The layer that actually moves. useDragPan writes transform straight to this
element from an animation frame — going through React state would queue a
render per pointermove, which is the difference between following your hand
and lagging behind it. */
.stagePan {
position: absolute;
inset: 0;
will-change: transform;
}
/* The world: a fixed 1440×640 canvas everything on the map is laid out on in
plain pixels, scaled uniformly to the box (transform set inline). One
composition at every viewport — percentages of a box whose aspect bends
(min-height) made each screen size a different picture, with fixed-size
cards crowding on narrow ones. */
.stageWorld {
position: absolute;
left: 50%;
top: 50%;
width: 1440px;
height: 640px;
}
.stageLines {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
/* The device in the middle. Everything on this map is a direction off it, so
it is drawn as the object rather than as another node. */
.stageCore {
position: absolute;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
pointer-events: none;
}
.coreScreen {
width: 84px;
height: 148px;
background: var(--pfc-well);
border: 1px solid rgba(244, 239, 230, 0.35);
box-shadow: 0 0 60px rgba(232, 85, 46, 0.25);
}
.stageCore span {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.stageNode {
position: absolute;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
gap: 6px;
width: 196px;
padding: 12px 14px;
border: 1px solid var(--pfc-hair);
background: var(--pfc-panel);
color: var(--pfc-ink);
font-family: inherit;
text-align: left;
cursor: pointer;
}
.stageNode:hover {
border-color: var(--pfc-edge-strong);
}
/* Selected. The tint is LAYERED over the panel rather than replacing it: as a
plain rgba() it made the node 10%-opaque, and on the constellation that meant
every line in the map showed through the node you had just picked — all of
them visibly converging on the core behind it. */
.stageNode[data-active="true"],
.floorZone[data-active="true"] {
border-color: var(--pfc-led);
background-color: var(--pfc-panel);
background-image: linear-gradient(rgba(232, 85, 46, 0.1), rgba(232, 85, 46, 0.1));
}
.nodeHead {
display: flex;
align-items: baseline;
gap: 8px;
min-width: 0;
}
.nodeCode {
font-family: var(--pf-mono, monospace);
font-size: 9.5px;
color: var(--pfc-ink-faint);
flex: none;
}
.nodeTitle {
font-size: 13.5px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* One orange square per person working here — the map's whole payload. */
.nodeCounts {
display: flex;
gap: 3px;
align-items: center;
flex-wrap: wrap;
}
.nodeCounts i {
width: 8px;
height: 8px;
background: var(--pfc-led);
display: block;
flex: none;
}
.nodeCounts span {
font-family: var(--pf-mono, monospace);
font-size: 9.5px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
margin-left: 5px;
}
/* Open questions, hanging off the selected node as dashed chips. */
.nodeQuestions {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 2px;
min-width: 0;
}
.nodeQuestions span {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
color: var(--pfc-led);
border: 1px dashed var(--pfc-led-edge);
padding: 2px 7px;
/* The node is a fixed 196px and flex-wrap only breaks BETWEEN chips, so a
real question ("128×128 without a frame buffer?") used to hang out over
its neighbours. It wraps inside its own dashes instead — the node grows a
line, and only while it is the selected one. */
max-width: 100%;
overflow-wrap: anywhere;
line-height: 1.4;
}
/* ── People on the map ──
One small square per person, walked around with WASD. The layer itself takes
no pointer events — the map underneath still pans — only the bodies do. */
.presenceLayer {
position: absolute;
inset: 0;
pointer-events: none;
}
.presencePerson {
position: absolute;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: 6px;
pointer-events: auto;
text-decoration: none;
cursor: pointer;
/* A poll landing moves people in one jump; the glide is what makes that
read as strolling rather than teleporting. */
transition: left 900ms ease, top 900ms ease;
}
/* My square is painted per frame by the walk loop — a transition here would
turn WASD into steering a boat. */
.presencePerson[data-me="true"] {
transition: none;
}
.presenceSquare {
width: 9px;
height: 9px;
flex: none;
background: var(--pfc-ink-muted);
}
.presencePerson[data-me="true"] .presenceSquare {
background: var(--pfc-led);
}
.presenceName {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.06em;
color: var(--pfc-ink-muted);
white-space: nowrap;
}
.presencePerson:hover .presenceName {
color: var(--pfc-ink);
}
.presencePerson[data-me="true"] .presenceName {
color: var(--pfc-led);
}
/* The said-thing, hanging under the name like a quiet speech bubble. */
.presenceStatus {
position: absolute;
top: 100%;
left: 15px;
margin-top: 1px;
font-size: 10px;
color: var(--pfc-ink-faint);
white-space: nowrap;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
}
/* An account that exists and has never walked: a fleck in the ring around the
core. The pile is the member count, before anyone has done anything. */
.presenceSeed {
position: absolute;
transform: translate(-50%, -50%);
width: 5px;
height: 5px;
background: var(--pfc-ink-faint);
opacity: 0.55;
}
.presenceCount {
position: absolute;
transform: translate(-50%, 0);
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
color: var(--pfc-ink-faint);
white-space: nowrap;
}
/* Enter's little input, floating above the square that asked for it. */
.statusEditor {
position: absolute;
transform: translate(-50%, calc(-100% - 16px));
display: flex;
flex-direction: column;
gap: 3px;
padding: 8px;
background: var(--pfc-panel);
border: 1px solid var(--pfc-led-edge);
pointer-events: auto;
}
.statusEditor input {
width: 200px;
border: 1px solid var(--pfc-edge);
background: var(--pfc-ground);
color: var(--pfc-ink);
font-family: inherit;
font-size: 12px;
padding: 5px 7px;
}
.statusEditor input:focus {
outline: none;
border-color: var(--pfc-led);
}
.statusEditor span {
font-family: var(--pf-mono, monospace);
font-size: 9px;
color: var(--pfc-ink-faint);
}
.stageLegend {
position: absolute;
right: 16px;
bottom: 14px;
display: flex;
gap: 16px;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
/* The walking hint — and, signed out, the invitation. Same type as the legend
it sits in; the button variant only adds the underline that says "click". */
.legendWalk {
color: var(--pfc-ink-muted);
}
button.legendWalk {
appearance: none;
border: 0;
padding: 0;
background: none;
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
button.legendWalk:hover {
color: var(--pfc-led);
}
.stageLegend span {
display: inline-flex;
align-items: center;
gap: 6px;
}
.legendSquare {
width: 8px;
height: 8px;
background: var(--pfc-led);
}
.legendLine {
width: 14px;
height: 2px;
background: var(--pfc-led);
}
.legendDashed {
width: 14px;
border-top: 1px dashed rgba(232, 85, 46, 0.6);
}
/* ── Floor plan ── */
.floorPlan {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-auto-rows: 148px;
gap: 10px;
padding: 20px;
border: 1px solid var(--pfc-hair);
background-image: repeating-linear-gradient(0deg, rgba(244, 239, 230, 0.03) 0 1px, transparent 1px 28px),
repeating-linear-gradient(90deg, rgba(244, 239, 230, 0.03) 0 1px, transparent 1px 28px);
}
.floorZone {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
padding: 14px 16px;
border: 1px solid var(--pfc-hair);
background: var(--pfc-panel);
color: var(--pfc-ink);
font-family: inherit;
text-align: left;
cursor: pointer;
overflow: hidden;
}
.floorZone:hover {
border-color: var(--pfc-edge-strong);
}
.floorTitle {
font-size: 15.5px;
font-weight: 600;
letter-spacing: -0.01em;
}
.floorDesc {
font-size: 12.5px;
line-height: 1.55;
color: var(--pfc-ink-muted);
max-width: 44ch;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.nextChip {
font-family: var(--pf-mono, monospace);
font-size: 9.5px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--pfc-led);
border: 1px solid var(--pfc-led-edge);
padding: 1px 6px;
flex: none;
}
/* ── Drawer ──
The selected territory, opened underneath whichever view is showing. */
.mapDrawer {
border: 1px solid var(--pfc-hair);
border-top: 0;
background: var(--pfc-panel);
padding: 18px 20px 22px;
display: flex;
flex-direction: column;
gap: 14px;
}
.drawerHead {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.drawerCode {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.1em;
color: var(--pfc-ink-faint);
}
.drawerTitle {
font-size: 16px;
font-weight: 600;
}
.drawerCounts {
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
.pinnedChip {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-led);
border: 1px solid var(--pfc-led-edge);
padding: 3px 8px;
}
.pinnedChip i {
width: 7px;
height: 7px;
background: var(--pfc-led);
}
.mapGhostBtn {
appearance: none;
border: 1px solid var(--pfc-edge-strong);
background: transparent;
color: var(--pfc-ink-muted);
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 9px 15px;
cursor: pointer;
}
.mapGhostBtn:hover:not(:disabled) {
color: var(--pfc-ink);
border-color: var(--pfc-ink);
}
.mapGhostBtn:disabled {
opacity: 0.5;
cursor: default;
}
.workingRow {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.workingLabel {
font-family: var(--pf-mono, monospace);
font-size: 9.5px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
.workingEmpty {
font-size: 12.5px;
color: var(--pfc-ink-faint);
}
.workingChip {
display: inline-flex;
align-items: center;
gap: 5px;
border: 1px solid rgba(244, 239, 230, 0.2);
padding: 2px 8px;
font-size: 12px;
}
.workingChip i {
width: 7px;
height: 7px;
background: var(--pfc-led);
flex: none;
}
/* Your own pin reads a shade louder — on a row of handles, finding yourself
should not take a scan. */
.workingChip[data-self="true"] {
border-color: rgba(232, 85, 46, 0.6);
color: var(--pfc-led);
}
.threadGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 12px;
}
.threadCard {
display: flex;
flex-direction: column;
gap: 6px;
border: 1px solid var(--pfc-hair);
padding: 13px 15px;
background: var(--pfc-ground);
text-decoration: none;
color: inherit;
}
.threadCard:hover {
border-color: var(--pfc-edge-strong);
}
.threadCardHead {
display: flex;
align-items: baseline;
gap: 8px;
}
.threadCardTitle {
font-size: 13.5px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.threadCardCount {
font-family: var(--pf-mono, monospace);
font-size: 10px;
color: var(--pfc-led);
flex: none;
}
.threadCardPreview {
font-size: 12px;
line-height: 1.55;
color: var(--pfc-ink-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.threadCardByline {
font-family: var(--pf-mono, monospace);
font-size: 9.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
/* The lowest-effort way onto the map: say what you are doing, press the
button, you are on it. No thread required. */
.pinComposer {
display: flex;
gap: 8px;
}
.pinComposer input {
flex: 1;
border: 1px solid var(--pfc-edge);
background: var(--pfc-ground);
color: var(--pfc-ink);
font-family: inherit;
font-size: 13px;
padding: 10px 12px;
outline: none;
}
.pinComposer input::placeholder {
color: var(--pfc-ink-faint);
}
.pinComposer input:focus {
border-color: var(--pfc-led);
}
/* ── New-thread modal ── */
.modalCardThread {
width: min(560px, 100%);
}
.territorySelect {
border: 1px solid var(--pfc-edge);
background: var(--pfc-ground);
color: var(--pfc-ink);
font-family: inherit;
font-size: 12.5px;
padding: 4px 8px;
}
.composerRow {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.composerLink {
appearance: none;
border: 0;
background: none;
padding: 0;
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
color: var(--pfc-ink-faint);
cursor: pointer;
}
.composerLink:hover {
color: var(--pfc-ink);
}
/* Checked by default: writing a thread about a direction IS working on it. */
.pinCheck {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12.5px;
color: var(--pfc-ink-muted);
cursor: pointer;
}
.pinCheck input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.pinCheck span {
width: 12px;
height: 12px;
border: 1px solid var(--pfc-edge-strong);
display: grid;
place-items: center;
color: var(--pfc-on-led);
font-size: 9px;
line-height: 1;
}
.pinCheck input:checked + span {
background: var(--pfc-led);
border-color: var(--pfc-led);
}
.fileChips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* Attached images, shown in the thread. Capped in height, not width: a build
photo is landscape, a phone screenshot is portrait, and a row of mixed
shapes at one height reads as a contact sheet rather than a mess. */
.attachImages {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
.attachImages a {
display: block;
line-height: 0;
}
/* Holds the × in the corner of the thumbnail. */
.attachImage {
position: relative;
}
.attachImages img {
max-height: 220px;
max-width: 100%;
border: 1px solid var(--pfc-hair);
background: var(--pfc-well);
}
.attachImages a:hover img {
border-color: var(--pfc-edge-strong);
}
.fileChip {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid var(--pfc-edge);
padding: 6px 10px;
font-family: var(--pf-mono, monospace);
font-size: 11px;
color: var(--pfc-ink);
text-decoration: none;
}
.fileChip:hover {
border-color: var(--pfc-led);
}
/* ── Taking a file back off ──
Both removes are two-step: × arms, "sure?" commits. There is no undo behind
this — the bytes go — so a single misclick must not be enough, in exactly
the way a single misclick used to be enough to publish the wrong file. */
.attachImageRemove,
.fileChipRemove {
appearance: none;
border: 1px solid var(--pfc-hair);
background: var(--pfc-ground);
color: var(--pfc-ink-faint);
font: inherit;
font-family: var(--pf-mono, monospace);
line-height: 1;
cursor: pointer;
}
.attachImageRemove:hover,
.fileChipRemove:hover,
.attachImageRemove[data-confirming="true"],
.fileChipRemove[data-confirming="true"] {
border-color: var(--pfc-danger);
color: var(--pfc-danger);
}
/* On a thumbnail: top-right, and only once you are looking at the image —
a row of × marks over a contact sheet is noise. Always visible on touch,
where there is no hover to reveal it. */
.attachImageRemove {
position: absolute;
top: 5px;
right: 5px;
padding: 3px 7px;
font-size: 12px;
opacity: 0;
transition: opacity 120ms ease;
}
.attachImage:hover .attachImageRemove,
.attachImageRemove:focus-visible,
.attachImageRemove[data-confirming="true"] {
opacity: 1;
}
@media (hover: none) {
.attachImageRemove {
opacity: 1;
}
}
/* On a chip: inline at the end, after the size. */
.fileChipRemove {
padding: 1px 6px;
font-size: 11px;
margin-left: 2px;
}
.fileChip span {
color: var(--pfc-ink-faint);
}
.fileChip button {
appearance: none;
border: 0;
background: none;
color: var(--pfc-ink-faint);
cursor: pointer;
font-size: 11px;
padding: 0;
}
.fileChip button:hover {
color: var(--pfc-danger);
}
.patternPicker {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 10px;
border: 1px solid var(--pfc-hair);
background: var(--pfc-ground);
}
.patternPicker button {
appearance: none;
border: 1px solid var(--pfc-edge);
background: transparent;
color: var(--pfc-ink);
font-family: inherit;
font-size: 12px;
padding: 3px 9px;
cursor: pointer;
}
.patternPicker button:hover {
border-color: var(--pfc-led);
color: var(--pfc-led);
}
/* ── Thread page ── */
.threadLayout {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
align-items: start;
gap: 0;
padding: 20px 0 60px;
}
.threadMain {
padding-right: 32px;
border-right: 1px solid var(--pfc-hair);
display: flex;
flex-direction: column;
}
.threadAside {
padding: 0 0 0 24px;
display: flex;
flex-direction: column;
gap: 18px;
}
.territoryCard {
border: 1px solid var(--pfc-led);
background: var(--pfc-led-wash);
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.territoryCardDesc {
font-size: 12.5px;
line-height: 1.55;
color: var(--pfc-ink-muted);
}
.pinHereBtn {
appearance: none;
border: 1px solid var(--pfc-led);
background: transparent;
color: var(--pfc-led);
font-family: var(--pf-mono, monospace);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 9px 0;
cursor: pointer;
margin-top: 4px;
}
.pinHereBtn:hover:not(:disabled) {
background: var(--pfc-led);
color: var(--pfc-on-led);
}
.pinHereBtn:disabled {
opacity: 0.5;
cursor: default;
}
.asideList {
display: flex;
flex-direction: column;
gap: 10px;
}
.asideRow {
display: flex;
flex-direction: column;
gap: 3px;
padding: 9px 0;
border-bottom: 1px solid var(--pfc-hair-soft);
text-decoration: none;
color: inherit;
}
.asideRow:hover .threadCardTitle {
color: var(--pfc-led);
}
/* "▪ WORKING HERE" beside a reply's author — the reason to read it before the
others in the thread. */
.workingTag {
display: inline-flex;
align-items: center;
gap: 4px;
font-family: var(--pf-mono, monospace);
font-size: 9px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--pfc-led);
}
.workingTag i {
width: 6px;
height: 6px;
background: var(--pfc-led);
}
@media (max-width: 900px) {
.threadLayout {
grid-template-columns: 1fr;
gap: 24px;
}
.threadMain {
padding-right: 0;
border-right: 0;
}
.threadAside {
padding-left: 0;
}
/* A phone gets the list, not the stage — MapClient forces the floor plan
there, so the toggle would offer a view it will not honour. */
.viewToggle {
display: none;
}
.floorPlan {
grid-template-columns: 1fr;
grid-auto-rows: auto;
padding: 12px;
}
.floorZone {
grid-column: span 1 !important;
}
.mapDrawer {
border-top: 1px solid var(--pfc-hair);
padding: 16px;
}
}
/* ── Linkified plain text ──
Keeps author line breaks, since the body was typed as plain text and the
shape is part of what they wrote. */
.linkedText {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.inlineLink {
color: var(--pfc-ink);
text-decoration: underline;
text-underline-offset: 2px;
}
.inlineLink:hover {
color: var(--pfc-led);
}
/* ── Post bodies ──
Markdown, but a closed set of it (see PostBody.tsx). Everything here is a
descendant selector on .postBody rather than a class per element, because
react-markdown emits plain tags and there is nowhere to hang a class.
Sizes stay close to the body text: a thread is somebody talking, and an <h2>
that looks like a landing-page headline makes a build log read as a brochure. */
.postBody {
font-size: 14px;
line-height: 1.65;
overflow-wrap: anywhere;
}
.postBody > *:first-child {
margin-top: 0;
}
.postBody > *:last-child {
margin-bottom: 0;
}
.postBody p {
margin: 0 0 12px;
}
.postBody h1,
.postBody h2,
.postBody h3,
.postBody h4,
.postBody h5,
.postBody h6 {
margin: 20px 0 8px;
font-weight: 600;
line-height: 1.35;
}
.postBody h1,
.postBody h2 {
font-size: 16px;
}
.postBody h3,
.postBody h4,
.postBody h5,
.postBody h6 {
font-size: 14.5px;
}
.postBody ul,
.postBody ol {
margin: 0 0 12px;
padding-left: 22px;
}
.postBody li {
margin: 3px 0;
}
.postBody li::marker {
color: var(--pfc-ink-faint);
}
.postBody blockquote {
margin: 12px 0;
padding: 2px 0 2px 12px;
border-left: 2px solid var(--pfc-edge);
color: var(--pfc-ink-2);
}
.postBody hr {
margin: 18px 0;
border: 0;
border-top: 1px solid var(--pfc-hair);
}
.postBody strong {
font-weight: 600;
color: var(--pfc-ink);
}
.postBody del {
color: var(--pfc-ink-faint);
}
/* Says why the picture is a link and not a picture, without a sentence of
explanation next to every one. */
.imageLinkTag {
font-family: var(--pf-mono, monospace);
font-size: 0.82em;
color: var(--pfc-ink-faint);
text-decoration: none;
}
/* Inline code — a register name or a pin, mid-sentence. Quiet enough to sit in
a paragraph, distinct enough to not be read as prose. */
.inlineCode {
padding: 1px 5px;
background: var(--pfc-well);
border: 1px solid var(--pfc-hair);
font-family: var(--pf-mono, monospace);
font-size: 0.88em;
overflow-wrap: anywhere;
}
/* Measurement tables. Narrow columns and a hard scroll boundary — a wide table
must never be the reason the page scrolls sideways. */
.postBody table {
display: block;
width: max-content;
max-width: 100%;
margin: 0 0 12px;
overflow-x: auto;
border-collapse: collapse;
font-size: 13px;
}
.postBody th,
.postBody td {
padding: 5px 10px;
border: 1px solid var(--pfc-hair);
text-align: left;
}
.postBody th {
background: var(--pfc-well);
font-weight: 600;
}
/* ── Composing ──
Write / Preview, rendered through the same PostBody the thread uses. */
.bodyComposer {
display: flex;
flex-direction: column;
gap: 6px;
}
.composerTabs {
display: flex;
align-items: center;
gap: 4px;
}
.composerTabs button {
appearance: none;
border: 1px solid transparent;
background: none;
padding: 3px 9px;
font: inherit;
font-size: 11.5px;
color: var(--pfc-ink-faint);
cursor: pointer;
}
.composerTabs button[data-active="true"] {
border-color: var(--pfc-edge);
background: var(--pfc-well);
color: var(--pfc-ink);
}
.composerTabs button:disabled {
opacity: 0.45;
cursor: default;
}
.composerHint {
font-family: var(--pf-mono, monospace);
font-size: 10.5px;
color: var(--pfc-ink-faint);
}
/* Same box as the textarea it replaces, so switching tabs doesn't move the
page under the person typing. */
.previewPane {
box-sizing: border-box;
min-height: 180px;
padding: 11px 12px;
border: 1px dashed var(--pfc-edge);
background: var(--pfc-ground);
overflow-y: auto;
}
.previewPane[data-grow="page"] {
max-height: 75vh;
}
.previewPane[data-grow="modal"] {
max-height: 42vh;
}
/* A reply sits in a list of other people's replies, so it matches the comment
textarea's own floor rather than the post editor's. */
.previewPane[data-grow="comment"] {
min-height: 84px;
max-height: 60vh;
}
.postBodyInput[data-grow="comment"] {
max-height: 60vh;
}
/* BodyComposer brings .postBodyInput with it, but inside a comment form the
more specific `.commentForm textarea` still owns the box — which is what we
want (84px floor, not 180). Stated here so the interaction is on purpose. */
.commentForm .bodyComposer .postBodyInput {
min-height: 84px;
}
@media (max-width: 560px) {
/* The hint is the first thing to go — the tabs have to stay tappable. */
.composerHint {
display: none;
}
}
/* ── Code fences ──
Same dark ground as the code panel, so pasted logs read as what they are. */
.codeFence {
margin: 10px 0;
padding: 10px 12px;
background: var(--pfc-well);
color: var(--pfc-ink-2);
font-family: var(--pf-mono, monospace);
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
border: 1px solid var(--pfc-hair);
}
/* ── Pagination ──
(The wall scrolls forever; the board still pages — twenty text rows at a
time is plenty, and a pager is honest furniture for an archive.) */
.paginationBar {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
font-family: var(--pf-mono, monospace);
font-size: 13px;
margin-top: 8px;
}
.pageBtn {
border: 1px solid var(--pfc-edge-strong);
background: transparent;
color: var(--pfc-ink);
font-size: 12px;
font-family: inherit;
font-weight: 500;
padding: 6px 14px;
cursor: pointer;
text-decoration: none;
transition: all 120ms ease;
}
.pageBtn:hover:not([data-disabled="true"]) {
background: var(--pfc-ink);
border-color: var(--pfc-ink);
color: var(--pfc-ground);
}
.pageBtn[data-disabled="true"] {
opacity: 0.4;
pointer-events: none;
}
.pageIndicator {
color: var(--pfc-ink-muted);
display: flex;
align-items: center;
gap: 6px;
}
.pageIndicator strong {
color: var(--pfc-ink);
}
.pageMetaTotal {
font-size: 11px;
opacity: 0.7;
}
/* ── Moderation queue (/community/reports) ──
Admin-only, so it is plain: a list you read top to bottom and act on, not a
dashboard. */
.reportsPage {
max-width: 780px;
padding: 26px 0 60px;
}
.reportsHead {
display: flex;
align-items: baseline;
gap: 16px;
margin-bottom: 20px;
}
.reportsHead h1 {
margin: 0;
font-size: 22px;
font-weight: 600;
}
.reportsTabs {
display: flex;
gap: 12px;
font-family: var(--pf-mono);
font-size: 11px;
letter-spacing: var(--pf-track-mono);
text-transform: uppercase;
}
.reportsTabs a {
color: var(--pfc-ink-faint);
text-decoration: none;
}
.reportsTabs a[data-active="true"] {
color: var(--pfc-ink);
text-decoration: underline;
text-underline-offset: 3px;
}
.reportList {
list-style: none;
margin: 0 0 20px;
padding: 0;
}
.reportRow {
padding: 14px 16px;
margin-bottom: 10px;
border: 1px solid rgba(244, 239, 230, 0.14);
background: var(--pfc-panel);
}
/* A closed report stays legible but stops competing with the open ones. */
.reportRow[data-status="actioned"],
.reportRow[data-status="dismissed"] {
opacity: 0.55;
background: transparent;
}
.reportMeta {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 8px;
font-family: var(--pf-mono);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--pfc-ink-muted);
}
.reportReason {
color: var(--pfc-ink);
font-weight: 500;
text-transform: none;
letter-spacing: 0;
font-family: var(--pf-sans);
font-size: 13px;
}
.reportRepeat {
padding: 1px 6px;
background: var(--pfc-danger);
color: var(--pfc-ground);
}
.reportStatus {
padding: 1px 6px;
border: 1px solid var(--pfc-hair);
}
.reportDate {
margin-left: auto;
}
.reportTarget {
font-size: 14px;
margin-bottom: 6px;
}
.reportKind {
font-family: var(--pf-mono);
font-size: 10px;
letter-spacing: var(--pf-track-mono);
text-transform: uppercase;
color: var(--pfc-ink-faint);
}
.reportQuote {
color: var(--pfc-ink-muted);
font-style: italic;
}
.reportDetail {
margin: 0 0 10px;
padding: 8px 12px;
background: var(--pfc-well);
font-size: 13.5px;
line-height: 1.55;
white-space: pre-wrap;
word-break: break-word;
}
.reportFoot {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--pfc-ink-muted);
}
/* ── Narrow screens ──
Keep this media query in sync with MOBILE_MEDIA_QUERY in lib/hooks/useMediaQuery.ts. */
@media (max-width: 900px) {
.detailLayout {
grid-template-columns: 1fr;
gap: 20px;
}
.matrixFrame {
max-width: 190px;
}
.knobs {
margin-top: 14px;
max-width: 100%;
gap: 9px;
}
.knobLine {
grid-template-columns: 58px minmax(0, 1fr) 48px;
gap: 8px;
font-size: 10.5px;
}
/* A slider is a drag target before it is a control — 44px of thumb. */
.knobLine input[type="range"] {
height: 24px;
}
.actionRow {
max-width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.actionRow .btnAccent {
grid-column: span 2;
}
.actionRow button,
.actionRow a {
min-height: 44px;
justify-content: center;
}
.editorBody {
min-height: 320px;
}
}
@media (max-width: 768px), (hover: none) {
.pageHeader {
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
padding: 14px 16px;
}
/* The section nav moves to the tab bar at the bottom of the screen. */
.sectionNav,
.headerSpacer {
display: none;
}
/* Signed in this row carries five things — CTA, deck chip, Alerts, handle,
sign out — and it used to be an unwrappable 461px against a 343px content
box, which scrolled the whole page sideways on a phone. Wrapping is the
structural fix: no combination of labels can push the page wider than the
screen again. The measures below are what keep it to one line anyway. */
.headerNav {
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px 12px;
min-width: 0;
/* Right-aligned as a group. This used to be `margin-left: auto` on the CTA
itself, which was fine while the row could not wrap and disastrous once
it could: an auto margin swallows the line's free space, so the CTA sat
alone on row one and pushed everything else onto row two. */
margin-left: auto;
}
.makeCta {
font-size: 12px;
padding: 8px 12px;
}
/* "Make ↗". The full sentence is a desktop measure — on a phone it alone was
212px, over half the screen. The title attribute still says the rest. */
.ctaRest {
display: none;
}
/* A handle can be any length; a header row cannot. */
.userChip {
max-width: 96px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The fifth item is what tipped the row onto a second line, and it is the
one the "You" tab already leads to. It moves to your own profile. */
.headerNav .signOutLink {
display: none;
}
.profileSignOut {
display: inline-block;
}
.pageBody {
padding: 0 16px;
}
.marquee {
margin: 0 -16px;
}
/* Clear of the tab bar. */
.pageFooter {
padding-bottom: 90px;
}
.feedWrapper {
padding: 16px 0 40px;
}
.feedGrid {
gap: 8px;
}
.introRow {
font-size: 13px;
}
.feedControls {
gap: 12px;
}
/* Card size is fixed by the mobile view config, and there is no Ctrl+wheel
on a phone — the hint would advertise a gesture that does not exist. */
.zoomHint,
.controlRule {
display: none;
}
.deckGrid {
grid-template-columns: 1fr;
}
.profileGrid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.deckSlots {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.sectionTitle {
font-size: 20px;
}
.metaTitle {
font-size: 22px;
}
}