Something went wrong. Try again.
Browser extension: detect and subscribe to standard.site publications on ATProto
Something went wrong. Try again.
21 kB · 713 lines
CSS
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714/* Brand palette: paper, ink, and the red pen (see brand/mark.svg). Raised surfaces (menus, inputs, buttons) sit on --card against the paper --bg. */:root { color-scheme: light dark; --fg: #26282b; --bg: #f4f3ee; --card: #fdfcf9; --muted: #6d6a62; --border: #dedcd3; --red: #d9482b; --gold: #e8b52a; --good: #1a7f37; --good-fg: #ffffff; --warn: #9a6700; --bad: #cf222e;}@media (prefers-color-scheme: dark) { :root { --fg: #e9e7e0; --bg: #1c1d20; --card: #26272b; --muted: #96938a; --border: #3a3b40; --red: #e2593a; --gold: #d9a826; --good: #4cc38a; --good-fg: #14321f; --warn: #d4a72c; --bad: #ff7b72; }}
/* Ink, thinned until it is a shape rather than a word: the grey of every loading placeholder. One definition for both palettes — it is mixed from --fg, which each of them has already set. */:root { --skeleton: color-mix(in srgb, var(--fg) 12%, transparent); }
* { box-sizing: border-box; }body { /* Wide enough for the actions row's widest state — ✓ Subscribed, the iconed "Open in Standard Reader" split, and the refresh icon — which needs ~390px on Linux fonts, plus slack for wider platform fonts. */ width: 400px; margin: 0; font: 13px/1.45 system-ui, sans-serif; color: var(--fg); background: var(--bg);}
#header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border);}/* The sticker leans its usual 2 degrees. */#header > img { transform: rotate(2deg);}/* Wordmark link: the name always carries its spellcheck squiggle. */#header .title { font-weight: 600; color: inherit; text-decoration: underline; text-decoration-style: wavy; text-decoration-color: var(--red); text-decoration-thickness: 1px; text-underline-offset: 3px;}#header .title:hover { text-decoration-thickness: 1.5px;}/* Build identity, next to the wordmark: quiet enough to ignore, there to be read back in a bug report. Filled from the manifest in popup.ts, so it is empty (and takes no gap) in a page opened outside the extension. */#header .version { font-size: 11px; color: var(--muted);}#header .version:empty { display: none;}/* All account state lives here: sign in when signed out, avatar + handle opening the sign out / switch menu when signed in. */.account-area { margin-left: auto; position: relative; display: flex;}/* Author-origin `display` beats the UA's `[hidden] { display: none }` (same trap as .pub-icon-fallback), so signed-out must be restated here or the button lingers as a caret that still opens the sign-out menu. */.account-btn[hidden] { display: none; }.account-btn { display: flex; align-items: center; gap: 6px; max-width: 180px; padding: 2px 6px; border: 0; border-radius: 6px; background: none; font: inherit; font-size: 12px; color: var(--muted); cursor: pointer;}.account-btn:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 8%, transparent);}.account-avatar { width: 18px; height: 18px; flex: none; border-radius: 50%; object-fit: cover;}.account-handle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}.account-btn .caret { font-size: 9px; }.account-area .menu { min-width: 150px; }/* Dropdown that hosts the sign-in form when signed out. Nearly the popup's full width: the typeahead suggestion list is only as wide as the input, so a narrow panel truncates every suggested handle. */.signin-panel { width: 364px; padding: 8px; }.signin-panel form { display: flex; gap: 6px; }.signin-panel input { width: 100%; min-width: 0; font: inherit; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--card); color: var(--fg);}.signin-panel .note:empty { display: none; }.signin-lede { margin: 0 0 8px; }/* The note text is muted, so a persistent underline is what marks the link. */.signin-lede a { color: inherit; text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--muted) 55%, transparent); text-underline-offset: 2px;}.signin-lede a:hover { color: var(--fg); text-decoration-color: currentColor;}/* Handle autocomplete dropdown. */actor-typeahead { flex: 1; --color-background: var(--card); --color-border: var(--border); --color-hover: color-mix(in srgb, var(--fg) 10%, transparent); --color-avatar-fallback: var(--border); --radius: 6px;}
#main { padding: 12px; }/* Status pills stack, most severe first; bare text (e.g. "No active tab.") renders as a plain muted line. */.status { display: flex; flex-direction: column; gap: 8px; color: var(--muted);}.pill { display: flex; gap: 8px; align-items: flex-start; }/* Warnings, alerts and errors get a tinted banner; info stays a plain muted line. Red is for the serious ones: something the extension could not do (error) and something serious it did (alert, e.g. a block you made). */.pill.warn,.pill.alert,.pill.error { padding: 6px 8px; border: 1px solid; border-radius: 6px; font-size: 12px;}.pill.warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); background: color-mix(in srgb, var(--warn) 10%, transparent);}.pill.alert,.pill.error { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, transparent); background: color-mix(in srgb, var(--bad) 10%, transparent);}/* Shape-coded tone mark (triangle/octagon/circle); severity survives without hue, which the two red tones need most. */.pill-icon { flex: none; display: flex; }.pill-icon svg { width: 14px; height: 14px; margin-top: 1.5px; }/* Links keep the pill's tone; underline is what marks them as links. */.status a { color: inherit; text-decoration: underline; text-underline-offset: 2px;}.status:not([hidden]) + #pub:not([hidden]) { margin-top: 10px; }
/* The owner card: the account, above the publication it publishes. A card rather than a line, because it carries the same three things a profile does — avatar, name, bio — and it is a link to that profile. *//* The same trap as .pub-icon-fallback and .account-btn: an author-origin `display` beats the UA's `[hidden] { display: none }`, so a card with no profile in it yet drew an empty bordered box — which is what the placeholder below stands in the middle of, and what it must fall back to nothing from. */.owner[hidden] { display: none; }.owner { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 10px; padding: 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--card); color: inherit; text-decoration: none;}.owner:hover { border-color: color-mix(in srgb, var(--fg) 25%, transparent); }.owner-avatar { width: 28px; height: 28px; flex: none; border-radius: 50%; object-fit: cover; background: color-mix(in srgb, var(--fg) 8%, transparent);}.owner-avatar-fallback[hidden] { display: none; }.owner-avatar-fallback { display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: var(--muted); user-select: none;}.owner-meta { min-width: 0; display: flex; flex-direction: column; }/* The name, and whatever the reader's own relationship to the account adds to it. The name takes the space that is left, so a long one ellipsizes rather than pushing the badge off the card. */.owner-line { display: flex; align-items: center; gap: 6px; min-width: 0; }.owner-name { font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}/* Quieter than a label pill: this is the reader's own state, not a claim anyone made about the account. */.owner-follows { flex: none; padding: 0 6px; border: 1px solid var(--border); border-radius: 999px; background: color-mix(in srgb, var(--fg) 6%, transparent); color: var(--muted); font-size: 10px; font-weight: 500; line-height: 15px;}.owner:hover .owner-name { text-decoration: underline; text-underline-offset: 2px; }.owner-handle { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}.owner-desc { margin-top: 4px; color: var(--muted); font-size: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;}
.pub-head { display: flex; align-items: center; gap: 10px; }.pub-icon { width: 36px; height: 36px; flex: none; border-radius: 6px; object-fit: cover; border: 1px solid var(--border);}/* Neutral monogram tile shown when the publication has no icon (or it fails to load). *//* An author-origin `display` beats the UA's `[hidden] { display: none }`, so the hidden state must be restated here or the tile renders next to a loaded icon. */.pub-icon-fallback[hidden] { display: none; }.pub-icon-fallback { display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 600; color: var(--muted); background: color-mix(in srgb, var(--fg) 8%, transparent); user-select: none;}.pub-meta { min-width: 0; }.pub-name { font-weight: 600; font-size: 14px; }/* Same treatment as the header wordmark: plain at rest, underline on hover. */.pub-name a { color: inherit; text-decoration: none;}.pub-name a:hover { text-decoration: underline; text-underline-offset: 2px;}.pub-desc { margin: 8px 0 0; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;}/* Who else subscribes: overlapping faces of the people you follow, then the count. Sits between the description and the article line, so it reads as part of the publication rather than as part of the actions. *//* Same trap: hidden has to be restated, or the row keeps its top margin. */.subscribers[hidden] { display: none; }.subscribers { margin-top: 8px; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted);}.faces { display: flex; }.faces:empty { display: none; }/* Overlapped, and each face draws a ring in the page background so the stack stays legible against whatever the avatars themselves are. */.face { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; background: color-mix(in srgb, var(--fg) 8%, transparent); box-shadow: 0 0 0 1.5px var(--bg);}.face + .face { margin-left: -6px; }/* Stand-in for an account with no avatar, or whose avatar did not load. */.face-fallback { display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; color: var(--muted); user-select: none;}.subscriber-count { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Placeholders, for a card whose lookup is still out after 150ms (the rule lives in src/popup/cards/loading.ts; this is only how it looks).
Every box below is an element the real content fills, given a size here so the answer lands inside it rather than pushing it open — a placeholder that collapsed to nothing would move more than the pop-in it replaces. Sizes are the middle of what these fields actually hold: a name, a handle, one line of a bio that may turn out to be two. */.loading { pointer-events: none; }.loading .owner-avatar,.loading .face,.loading .owner-name,.loading .owner-handle,.loading .owner-desc,.loading .subscriber-count { background: var(--skeleton); animation: placeholder-pulse 1.6s ease-in-out infinite;}/* Each bar sits in the line box its text would have taken — 13px/1.45 rounds to 19, 12px/1.45 to 17 — so a card whose bio turns out to be one line does not move at all when it arrives, and one with two lines grows by one. */.loading .owner-line { min-height: 19px; }.loading .owner-name { width: 128px; height: 12px; border-radius: 3px; }.loading .owner-handle { width: 92px; height: 10px; margin: 3.5px 0; border-radius: 3px; }.loading .owner-desc { height: 10px; margin: 7.5px 0 3.5px; border-radius: 3px; }.loading .subscriber-count { width: 116px; height: 10px; margin: 3.5px 0; border-radius: 3px; }/* Barely there: the point is that something is coming, not that something is happening. */@keyframes placeholder-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; }}@media (prefers-reduced-motion: reduce) { /* The pulse is the only thing in this popup that moves on its own, so stopping every animation is the whole guard. The grey boxes stand still and say the same thing. */ *, *::before, *::after { animation: none !important; }}/* Moderation labels, themed after Bluesky's: a chip per label, an alert tone for the values its own interpretation calls alerts, and a cover in front of anything a label says to put behind a click. */.labels { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px;}.label-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); background: color-mix(in srgb, var(--fg) 6%, transparent); color: var(--muted); font-size: 11px; line-height: 16px;}.label-pill.alert { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 45%, transparent); background: color-mix(in srgb, var(--bad) 10%, transparent);}.label-pill .pill-icon svg { width: 12px; height: 12px; margin-top: 0; }
.label-cover { padding: 10px; border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent); border-radius: 6px; background: color-mix(in srgb, var(--warn) 8%, transparent);}/* A cover put up by an alert label is red, matching that label's own pill. */.label-cover.alert { border-color: color-mix(in srgb, var(--bad) 45%, transparent); background: color-mix(in srgb, var(--bad) 8%, transparent);}.label-cover-head { display: flex; align-items: center; gap: 6px; font-weight: 600; color: var(--warn);}.label-cover.alert .label-cover-head { color: var(--bad); }.label-cover .note { margin: 6px 0 0; }.label-cover-actions { margin-top: 8px; display: flex; align-items: center; justify-content: space-between; gap: 8px;}.label-cover-actions .note { margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.doc-line { margin-top: 8px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px;}
.actions { display: flex; gap: 8px; margin-top: 12px; align-items: stretch;}.btn { font: inherit; white-space: nowrap; padding: 6px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--card); color: var(--fg); cursor: pointer;}.btn:hover:not([aria-disabled='true']) { border-color: var(--muted); }/* aria-disabled looks disabled but keeps its click: the signed-out Subscribe answers a click with the sign-in pill instead of ignoring it. */.btn:disabled,.btn[aria-disabled='true'] { opacity: 0.5; cursor: default; }.btn.sm { padding: 3px 10px; font-size: 12px; }/* Subscribe is a green sticker: ink border, solid misprint shadow, almost straight. Hover presses it into its own shadow. */.btn.primary { background: var(--good); color: var(--good-fg); border: 1.5px solid #26282b; transform: rotate(-0.5deg); box-shadow: 2px 2px 0 var(--border);}.btn.primary:hover:not(:disabled):not([aria-disabled='true']) { transform: rotate(-0.5deg) translate(1px, 1px); box-shadow: 1px 1px 0 var(--border);}/* Subscribe armed for its second click on a blocked publisher: same shape as .primary so nothing jumps, red so the click that goes through looks different from the one that armed it. */.btn.confirm { background: var(--bad); color: var(--good-fg); border: 1.5px solid #26282b; transform: rotate(-0.5deg); box-shadow: 2px 2px 0 var(--border);}.btn.confirm:hover:not(:disabled) { transform: rotate(-0.5deg) translate(1px, 1px); box-shadow: 1px 1px 0 var(--border);}.btn.subscribed { background: var(--card); color: var(--good); border-color: var(--good);}/* Clicking Subscribed unsubscribes, so hovering shows the red pen. */.btn.subscribed:hover { color: var(--bad); border-color: var(--bad); background: color-mix(in srgb, var(--bad) 8%, transparent);}
/* Vendored reader favicon, shown before the reader's name in the open button and the menu. Baseline-free flex centering in both hosts. */.reader-icon { width: 14px; height: 14px; flex: none; object-fit: contain;}.split .btn:first-child .reader-icon { margin-right: 6px; }
.split { position: relative; display: flex; margin-left: auto; }/* Grey-on-grey utility next to the split: visible on approach, quiet at rest. */.icon-btn { display: inline-flex; align-items: center; align-self: center; border: 0; background: none; color: var(--muted); opacity: 0.7; padding: 2px; cursor: pointer;}.icon-btn:hover { opacity: 1; }.split .btn:first-child { display: flex; align-items: center; border-top-right-radius: 0; border-bottom-right-radius: 0;}.split .arrow { border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; padding: 6px 8px;}/* Menus drop a solid misprint shadow, not a blur. */.menu { position: absolute; right: 0; top: calc(100% + 4px); min-width: 190px; /* Size to the widest item; icons pushed the reader rows past min-width. */ width: max-content; max-width: 376px; background: var(--card); border: 1px solid var(--border); border-radius: 8px; box-shadow: 3px 3px 0 color-mix(in srgb, var(--fg) 14%, transparent); z-index: 10; padding: 4px;}.menu .item { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 8px; border: 0; background: none; color: var(--fg); font: inherit; border-radius: 5px; cursor: pointer; text-align: left;}.menu .item:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }.menu .item .star { margin-left: auto; color: var(--muted); font-size: 12px;}.menu .item .star.active { color: var(--fg); }
.note { margin-top: 8px; color: var(--muted); font-size: 12px; min-height: 1em; }
/* Feedback: the report form, opened from the footer, hidden until then. */#feedback { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);}#feedback-form { display: flex; flex-direction: column; gap: 8px; }#feedback input[type='text'],#feedback textarea { width: 100%; font: inherit; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--card); color: var(--fg);}#feedback textarea { resize: vertical; }/* The board's own categories; the row is absent when the board lists none, so the author-origin `display` has to restate the hidden state. */.feedback-tags[hidden] { display: none; }.feedback-tags { display: flex; flex-wrap: wrap; gap: 4px 12px; }.feedback-tag { display: flex; align-items: center; gap: 4px; font-size: 12px; color: var(--muted); cursor: pointer;}/* Same as the sign-in lede: muted text, so the underline marks the link. */.feedback-lede { margin: 0; }.feedback-lede a { color: inherit; text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--muted) 55%, transparent); text-underline-offset: 2px;}.feedback-lede a:hover { color: var(--fg); text-decoration-color: currentColor;}.feedback-actions { display: flex; gap: 8px; }#feedback-note { margin-top: 0; }#feedback-note a { color: inherit; text-decoration: underline; text-underline-offset: 2px;}
/* Footer: the way into the feedback form and the updates blog, on every popup open. */#footer { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-top: 1px solid var(--border);}/* A quiet text button — the footer is not an action bar. */.link-btn { padding: 0; border: 0; background: none; font: inherit; font-size: 12px; color: var(--muted); cursor: pointer; text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--muted) 55%, transparent); text-underline-offset: 2px;}.link-btn:hover { color: var(--fg); text-decoration-color: currentColor; }
/* The dev build's debug menu, pushed to the far end of the footer so it never shifts where the two real footer links sit. Absent from a release build: author-origin `display` has to restate the hidden state, as everywhere else in this file. */.debug-area[hidden] { display: none; }.debug-area { margin-left: auto; position: relative; display: flex;}/* What the last debug action did, said in the menu that started it: the two that fetch have no other place to report, and the popup's status pills are redrawn from page state rather than written to. */.menu-note[hidden] { display: none; }.menu-note { margin: 4px 8px 2px; color: var(--muted); font-size: 12px;}
/* Nothing sits below the footer for a menu to drop into, so this one rises. */.menu.up { top: auto; bottom: calc(100% + 4px);}