Files
antisocial/public/app.css
T
thatguygriffandClaude Opus 5 abf8ec317c
CI / Typecheck, test, build (pull_request) Successful in 28s
Let the "Open on" button choose a browser
The StopTheMadness rules are indiscriminate, which is the point, but they
catch the link on the way back out as well: in the browser they are
installed in, "Open on <platform>" redirects straight back here. The one
button meant to reach the app is the one that cannot.

Handing the address to a different browser is the way past it, and the only
way to do that from a page is that browser's own URL scheme. `/` gets a
picker for which one; the choice lives in that browser's localStorage,
because which browsers are installed is a fact about the device and the
phone's answer is not the Mac's.

The scheme table is its own module so a test can pin it -- every browser
spells it differently, and Edge differs between macOS and iOS. Firefox has
no scheme on macOS, so it is only offered on iOS, and an unknown or
schemeless choice keeps the plain link rather than producing a dead one.
The markup still carries the plain https address and the script swaps it
afterwards, so nothing changes without JavaScript. Once a browser is
chosen the button says which, since a scheme for a browser that is not
installed opens nothing and the tap would otherwise be silent.

Closes #7

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KF5YF3iZVKbezwALap8LYd
2026-08-29 19:36:00 -03:00

602 lines
16 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
: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; }
.picker {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid var(--line);
}
/* The control ships hidden and is revealed by the script, and `display:
flex` above would otherwise beat the browser's own rule for [hidden]. */
.picker[hidden] { display: none; }
.picker__label { font-size: 13px; color: var(--ink-dim); }
.picker__select {
font: inherit;
font-size: 14px;
padding: 7px 10px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--card);
color: var(--ink);
}
.picker__note { flex-basis: 100%; margin: 0; font-size: 12px; color: var(--ink-dim); }
/* ---------- 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;
}
}
/* ---------- a post with a headline ---------- */
/* Reddit is the one platform where the post has a title and the body is
optional, so the headline has to carry on its own. */
.text__title {
margin: 0 0 8px;
font-size: 19px;
line-height: 1.3;
font-weight: 650;
}
.text__title:only-child { margin-bottom: 0; }
/* ---------- comments ---------- */
.thread {
margin-top: 12px;
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--radius);
}
.thread__head {
display: flex;
align-items: baseline;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
}
.thread__title { margin: 0; font-size: 15px; font-weight: 650; }
.thread__count { font-size: 12px; color: var(--ink-dim); }
.thread__toggle {
appearance: none;
margin-left: auto;
border: 1px solid var(--line);
background: transparent;
color: var(--ink-dim);
font: inherit;
font-size: 12px;
padding: 4px 12px;
border-radius: 999px;
cursor: pointer;
}
.thread__list { padding: 6px 14px 14px; }
.c { margin-top: 10px; }
/* The whole byline is the hit target for folding, so it wants to look like
one thing rather than a row of separate labels. */
.c__head {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 8px;
font-size: 12px;
color: var(--ink-dim);
cursor: pointer;
list-style: none;
padding: 2px 0;
user-select: none;
}
.c__head::-webkit-details-marker { display: none; }
.c__head::before {
content: "";
display: inline-grid;
place-items: center;
width: 15px;
height: 15px;
border: 1px solid var(--line);
border-radius: 4px;
font-size: 11px;
line-height: 1;
color: var(--ink-dim);
flex: none;
}
.c:not([open]) > .c__head::before { content: "+"; }
.c__author { font-weight: 600; color: var(--ink); }
.c__hidden { font-variant-numeric: tabular-nums; }
.c[open] > .c__head .c__hidden { display: none; }
.c__mark {
padding: 1px 5px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.03em;
color: var(--accent-ink);
background: var(--ink-dim);
}
.c__mark--op { background: #ff4500; }
.c__mark--mod, .c__mark--admin { background: #1a7f4b; }
.c__body { font-size: 15px; overflow-wrap: anywhere; }
.c__body > :first-child { margin-top: 4px; }
.c__body > :last-child { margin-bottom: 0; }
.c__body p { margin: 0 0 8px; }
.c__body a { color: inherit; text-decoration-color: var(--ink-dim); text-underline-offset: 2px; }
.c__body ul, .c__body ol { margin: 0 0 8px; padding-left: 20px; }
.c__body blockquote {
margin: 0 0 8px;
padding-left: 10px;
border-left: 3px solid var(--line);
color: var(--ink-dim);
}
.c__body pre {
margin: 0 0 8px;
padding: 8px 10px;
border-radius: 8px;
background: color-mix(in srgb, var(--ink) 6%, transparent);
overflow-x: auto;
}
.c__body code { font-size: 13px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.c__body pre code { font-size: 12px; }
/* The rule down the left is what tells you where a subtree ends, which is
the thing a wall of replies otherwise makes you count indents for. */
.c__replies {
margin: 6px 0 0 7px;
padding-left: 11px;
border-left: 1px solid var(--line);
}
.c__more { margin: 8px 0 0; font-size: 12px; color: var(--ink-dim); }
.thread__more {
margin: 0;
padding: 12px 14px;
border-top: 1px solid var(--line);
font-size: 12px;
color: var(--ink-dim);
}
@media (max-width: 520px) {
.c__replies { margin-left: 4px; padding-left: 7px; }
}
/* ---------- a quoted post ---------- */
/*
* A post inside a post. Bordered and inset rather than merely indented,
* because the whole job of this block is to be unmistakably someone else's
* — an indent alone reads as a continuation of the words above it.
*/
.quote {
margin: 0 14px 14px;
border: 1px solid var(--line);
border-radius: 10px;
overflow: hidden;
}
.text + .quote, .media + .quote { margin-top: 14px; }
.quote__who {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 2px 6px;
padding: 10px 12px 0;
font-size: 13px;
min-width: 0;
}
.quote__avatar {
width: 20px;
height: 20px;
border-radius: 50%;
flex: none;
align-self: center;
}
.quote__name { font-weight: 600; }
.quote__handle, .quote__who time { color: var(--ink-dim); font-size: 12px; }
.quote__text { padding: 6px 12px 10px; font-size: 15px; overflow-wrap: anywhere; }
.quote__text a { color: inherit; text-decoration-color: var(--ink-dim); text-underline-offset: 2px; }
/* Its own media sits flush in the block, the way the post's own does in the
card, so the two read as the same kind of thing at different sizes. */
.quote .media { border-bottom: 0; }
.quote__text + .media, .quote__who + .media { border-top: 1px solid var(--line); }
.quote__open {
display: block;
padding: 9px 12px;
border-top: 1px solid var(--line);
font-size: 12px;
color: var(--ink-dim);
text-decoration: none;
}
/*
* Deliberately smaller than the post's own media, and after the window-fitting
* rule above so it wins on source order at equal specificity. A quote is
* context for the post, and context that fills the screen has stopped being
* context.
*/
.post .quote .item img,
.post .quote .item video {
max-height: 40vh;
max-height: 40dvh;
min-height: 0;
}
/*
* A post that quotes another has two things to show, so its own media gives
* up the window-filling cap above. Left at full height the quote starts below
* the fold, which is the same as not having it — the whole reason it is here
* is that neither half reads without the other.
*/
.segment[data-quoted] .item img,
.segment[data-quoted] .item video {
max-height: 45vh;
max-height: 45dvh;
min-height: 0;
}
/* A picture someone put in a comment. Capped hard: it is a remark inside a
conversation, not the thing the page is about. */
.c__img {
display: block;
max-width: min(100%, 420px);
max-height: 40vh;
max-height: 40dvh;
width: auto;
height: auto;
margin: 8px 0;
border: 1px solid var(--line);
border-radius: 8px;
background: color-mix(in srgb, var(--ink) 4%, transparent);
}
/* A lone image is the whole comment more often than not, so it should not
carry a paragraph's worth of space above it as well as its own. */
.c__body > p:first-child > .c__img:first-child { margin-top: 2px; }