Files
antisocial/public/app.css
T
thatguygriffandClaude Opus 5 df50fa7e93
CI / Typecheck, test, build (pull_request) Successful in 34s
Keep a post inside the window, and stop letterboxing it
On a desktop a portrait video came with black bars all the way around and sat
partly below the fold, so you had to scroll to find the thing you had opened.

The bars were the figure, not the video. It carried the media's aspect ratio,
so at the full width of the column a 720x1280 reel forced a box 1204px tall
inside a 982px window, and the video -- capped at 78vh -- floated in the middle
of it with 438px of black above and below and 247px either side. The ratio now
goes on the media itself, which sizes to its own proportions instead of being
stretched to the column, and the background is the card's rather than black, so
what space is left reads as page instead of as a border.

The height cap is the window less the chrome around it. Deriving that from the
layout is the obvious approach and does not work: the card is sized by its
contents, so every percentage height inside resolves to auto and clamps
nothing. Measured across the posts this serves, the header, caption and footer
come to 270-350px, so the cap subtracts 350 and the whole post fits. Smaller
than it might be on a short window, but a video can be full-screened and a post
you have to hunt for cannot.

Threads keep the looser cap and scroll, which a column of several posts was
always going to do.

Grid thumbnails were quietly broken and the measuring turned it up: the cell
takes its height from an aspect ratio, which a percentage height will not
resolve against, and `aspect-ratio: auto` asks for the image's own ratio rather
than none, so a portrait thumbnail rendered at twice the height of its cell and
spilled out. They fill their cell absolutely now.

Verified at 1512x982, 1280x800, 1440x700 and 390x844: no letterboxing anywhere,
and the media sits above the fold on every one.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BGkRmLfiWuJHx6tQ12EELY
2026-08-26 17:24:22 -03:00

333 lines
8.8 KiB
CSS

:root {
--bg: #f6f6f7;
--card: #ffffff;
--ink: #16181c;
--ink-dim: #5b6470;
--line: #e2e4e8;
--accent-ink: #ffffff;
--radius: 14px;
color-scheme: light dark;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d0e11;
--card: #16181d;
--ink: #e9ebee;
--ink-dim: #949cab;
--line: #262a31;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: max(12px, env(safe-area-inset-top)) 12px calc(24px + env(safe-area-inset-bottom));
background: var(--bg);
color: var(--ink);
font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
-webkit-text-size-adjust: 100%;
}
main { max-width: 680px; margin: 0 auto; }
.post {
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
}
/* ---------- header ---------- */
.post__head {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
}
.badge { display: inline-flex; align-items: center; gap: 8px; flex: none; }
.badge__mark {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border-radius: 8px;
background: var(--accent);
color: var(--accent-ink);
font-size: 13px;
font-weight: 700;
line-height: 1;
}
.badge__label { font-size: 13px; font-weight: 600; letter-spacing: 0.01em; }
.who { display: flex; align-items: center; gap: 8px; min-width: 0; margin-left: auto; }
.who__avatar { width: 28px; height: 28px; border-radius: 50%; flex: none; }
.who__names { display: flex; flex-direction: column; min-width: 0; text-align: right; }
.who__name { font-size: 13px; font-weight: 600; }
.who__name, .who__handle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.who__handle { font-size: 12px; color: var(--ink-dim); }
/* ---------- text ---------- */
.text {
padding: 14px;
font-size: 16px;
white-space: normal;
overflow-wrap: anywhere;
}
.text a { color: inherit; text-decoration-color: var(--ink-dim); text-underline-offset: 2px; }
/* ---------- media ---------- */
/* Card-coloured, not black: whatever space the media does not fill should
read as part of the page rather than as a border around it. */
.media { border-block: 1px solid var(--line); background: var(--card); }
.post__head + .media, .text + .media { border-top: 1px solid var(--line); }
.media__bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 12px;
background: var(--card);
border-bottom: 1px solid var(--line);
}
.media__count { font-size: 12px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.media__views { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.media__view {
appearance: none;
border: 0;
background: transparent;
color: var(--ink-dim);
font: inherit;
font-size: 12px;
padding: 5px 12px;
cursor: pointer;
}
.media__view[aria-pressed="true"] { background: var(--ink); color: var(--card); }
/* Swipe is the default, and needs no JavaScript to work. */
.media__rail {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none;
}
.media__rail::-webkit-scrollbar { display: none; }
.item {
flex: 0 0 100%;
scroll-snap-align: center;
scroll-snap-stop: always;
margin: 0;
display: grid;
place-items: center;
position: relative;
min-width: 0;
min-height: 0;
}
/* Sized by its own proportions rather than stretched to the column: a
portrait video filling the width is mostly empty space either side of it.
The cap keeps any single item within the window. */
.item img, .item video {
display: block;
width: auto;
height: auto;
max-width: 100%;
max-height: 70vh;
max-height: 70dvh;
object-fit: contain;
background: transparent;
}
.item__note {
position: absolute;
inset-inline: 0;
bottom: 0;
padding: 4px 8px;
font-size: 11px;
color: #cfd4dc;
background: rgba(0, 0, 0, 0.55);
}
.media[data-view="grid"] .media__rail {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 2px;
overflow: visible;
}
.media[data-view="grid"] .item {
aspect-ratio: 1 / 1;
cursor: zoom-in;
overflow: hidden;
}
/*
* Filled absolutely rather than with height: 100%. The cell's height comes
* from its aspect-ratio, which a percentage height will not resolve against,
* and `aspect-ratio: auto` means "use the image's own" rather than "ignore
* ratios" -- between them a portrait thumbnail ends up twice the height of
* its cell.
*/
.media[data-view="grid"] .item img,
.media[data-view="grid"] .item video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
max-width: none;
max-height: none;
min-height: 0;
object-fit: cover;
aspect-ratio: auto !important;
}
.media[data-view="grid"] .item__note { display: none; }
/* ---------- footer ---------- */
.post__foot { padding: 14px; display: flex; flex-direction: column; gap: 12px; }
.post__foot time { font-size: 12px; color: var(--ink-dim); }
.original { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
.copy, .original__open {
appearance: none;
font: inherit;
font-size: 14px;
padding: 8px 14px;
border-radius: 999px;
cursor: pointer;
text-decoration: none;
border: 1px solid var(--line);
}
.copy { background: var(--ink); color: var(--card); border-color: var(--ink); }
.copy[data-copied] { background: #1a7f4b; border-color: #1a7f4b; }
.original__open { background: transparent; color: var(--ink); }
.original__url { flex-basis: 100%; margin: 0; }
.original__url code {
font-size: 12px;
color: var(--ink-dim);
overflow-wrap: anywhere;
user-select: all;
}
/* ---------- error and index ---------- */
.error__heading { font-size: 18px; margin: 14px; }
.error__detail { margin: 0 14px 14px; color: var(--ink-dim); font-size: 14px; }
.post--error .post__head:empty { display: none; }
.post--index { padding: 18px; }
.index__title { margin: 0; font-size: 20px; }
.index__lede { color: var(--ink-dim); font-size: 14px; }
.index__table { width: 100%; border-collapse: collapse; font-size: 13px; }
.index__table th { text-align: left; color: var(--ink-dim); font-weight: 600; padding: 6px 8px 6px 0; }
.index__table td { padding: 8px 8px 8px 0; border-top: 1px solid var(--line); vertical-align: middle; }
.index__table code { overflow-wrap: anywhere; }
/* ---------- verification puzzle ---------- */
.challenge__lede { margin: 14px; font-size: 14px; color: var(--ink-dim); }
.challenge__stage {
position: relative;
display: grid;
place-items: center;
padding: 8px;
background: #000;
border-block: 1px solid var(--line);
min-height: 160px;
}
.challenge__stage img {
max-width: 100%;
/* The puzzle is a drag target, so the browser must not claim the gesture
for scrolling or image-dragging. */
touch-action: none;
user-select: none;
-webkit-user-drag: none;
cursor: grab;
}
.challenge__stage img:active { cursor: grabbing; }
.challenge__status {
position: absolute;
bottom: 8px;
margin: 0;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px;
color: #e9ebee;
background: rgba(0, 0, 0, 0.65);
}
/* ---------- threads ---------- */
.segment + .segment { border-top: 1px solid var(--line); }
/* A chain reads as one column of posts, so the media in each needs its own
top edge only when text sits above it. */
.segment .text + .media { border-top: 1px solid var(--line); }
.post[data-segments] .segment { position: relative; }
.segment__mark {
margin: 0;
padding: 8px 14px 0;
font-size: 11px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--ink-dim);
}
.post[data-segments] .segment[data-anchor="true"] {
/* The link may point at the middle of a thread, so say which one it was
without hiding the rest. */
background: color-mix(in srgb, var(--accent, #888) 7%, transparent);
}
/* ---------- fitting the window ---------- */
/*
* On a large screen a single post should be readable without scrolling to
* find it -- a video especially, which you can full-screen if you want it
* bigger.
*
* The cap is the window less the chrome around the media: header, caption and
* footer come to roughly 270-350px on the posts this serves. Deriving it
* instead of subtracting a constant is the obvious thing to try and does not
* work: the card is sized by its contents, so a percentage height inside it
* resolves to auto and clamps nothing.
*
* A thread is a column of several posts and cannot fit whatever we do, so it
* keeps the looser cap above and scrolls.
*/
@media (min-width: 700px) and (min-height: 560px) {
.post:not([data-segments]) .item img,
.post:not([data-segments]) .item video {
max-height: calc(100vh - 350px);
max-height: calc(100dvh - 350px);
/* Below that the media would be too small to be worth showing; better to
let a very long caption scroll. */
min-height: 220px;
}
}