v4: the six styles are reworked, each around one idea of its own, and the tab row stops painting the page — a pass over every theme, then the faults it showed in what they share. Brutalist draws every box in ink with a solid offset shadow and sets the palette's colors as flat blocks (a teal tag, an apricot quote, an inked label); its dark scheme keeps that ink darker than the page, on Twilight Indigo, where eggshell lines glared across every frame. GeoCities goes to the desktop of its day: the header is the taskbar, the site's name its Start button, the controls grey bevels, and a marquee crawls under the header (Marquee.tsx, drawn in that style alone), with Comic Neue where the system has no Comic Sans. Melange leaves its browns and Fraunces for a sorbet blend in Bricolage Grotesque, run along the header, through the site's name, where it drifts, and between a journal's blocks; its two schemes are one print and its negative, trading ink and paper while the pink and the plum written on it stay, and they share one fine grain where the light one had a coarse tile that greyed the page. Kanagawa opens the comments and the related dumps on a status line and reads on Lotus's brightest paper; its page is flat. Times draws its heavy rules in a mid grey at night, where ink is near white, and sets a journal cell with no picture as text. In what the styles share: a row of tabs (TabBar) hid the tabs scrolled under its scope switcher, and faded its ends, by painting the page's color over them, which showed as a patch on any page that is a gradient or a picture. The switcher now sits before the row that scrolls, which clips the tabs at its edge, and the fades are a mask (--tab-fade-start and --tab-fade-end); useRoomBeside measures the row through that one more level. A page's title keeps a word whole unless it alone is wider than the line: Firefox sizes a title set with negative letter-spacing a hair narrower than it lays it out, which dropped its last letter to a second line in any style that tightened it, so the rule is the base's and the two styles' copies go. A card holding one paragraph, such as the instances a new account hasn't signed in to on /settings, is spaced by its padding and not by the paragraph's margins as well. The site's name answers the pointer as a card's title does in each style, where two of them left it still. The emails follow the styles' tokens, Melange's blend among them.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 55s
Build and Deploy Static Documentation Site / build-and-push (push) Successful in 26s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-12 19:30:57 +00:00
parent eb4e64683c
commit 62e51f1622
20 changed files with 1390 additions and 513 deletions

View File

@@ -191,7 +191,7 @@ const RECIPES: Record<StyleName, Recipe> = {
},
},
melange: {
webFontAxes: "opsz,wght@9..144,400;9..144,600;9..144,700",
webFontAxes: "wght@400;600",
scale: 1,
avatarRadius: 16,
title: { size: 32, weight: 600 },

View File

@@ -32,13 +32,14 @@ Deno.test("readTokens: a forced scheme's block overrides the theme's", () => {
Deno.test("readTokens: var() follows tokens, color-mix() is computed", () => {
const melange = readTokens("melange", "light");
// The spice starts with var(--color-accent) and drops its stops' positions.
// The spice holds var()s, the accent among them, and drops its stops'
// positions.
assertEquals(melange.stops("--mel-spice"), [
"#a06d00",
"#bc5c00",
"#c77b8b",
"#be79bb",
"#6e9b72",
"#ea6a45",
"#e2568f",
"#8262d8",
"#3a8fd4",
"#27a583",
]);
// 18% of the muted text over the card, as a badge shows on the site.
const smooth = readTokens("smooth", "light");

13
deno.lock generated
View File

@@ -30,12 +30,13 @@
"jsr:@std/testing@^1.0.21": "1.0.21",
"jsr:@tajpouria/cors@^1.2.1": "1.2.1",
"npm:@eslint/js@^10.0.1": "10.0.1_eslint@10.11.0_jiti@2.7.0",
"npm:@fontsource-variable/fraunces@5.3.0": "5.3.0",
"npm:@fontsource-variable/bricolage-grotesque@5.3.0": "5.3.0",
"npm:@fontsource-variable/jetbrains-mono@5.3.0": "5.3.0",
"npm:@fontsource-variable/libre-franklin@5.3.0": "5.3.0",
"npm:@fontsource-variable/newsreader@5.3.0": "5.3.0",
"npm:@fontsource-variable/saira@5.3.0": "5.3.0",
"npm:@fontsource-variable/space-grotesk@5.3.0": "5.3.0",
"npm:@fontsource/comic-neue@5.3.0": "5.3.0",
"npm:@fontsource/instrument-serif@5.3.0": "5.3.0",
"npm:@fontsource/manufacturing-consent@5.3.0": "5.3.0",
"npm:@lingui/cli@*": "6.8.0",
@@ -471,8 +472,8 @@
"@exodus/bytes@1.16.0": {
"integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA=="
},
"@fontsource-variable/fraunces@5.3.0": {
"integrity": "sha512-9BYGySn4AHEJdgp9Z28tQ3X+laJMEOITXkQarZXeloWQZDq5oOvXJ3kDA8c7MGIfpogIaZfjrQBqmda8POOCKA=="
"@fontsource-variable/bricolage-grotesque@5.3.0": {
"integrity": "sha512-TLi9Q4hJjS2UvoTMRSS2nHu6c4R56lAw60NR9QYtVRCHn0XtsFpiEhNffZ8Glsoxu6wEEwLKBP8lb94J52PNBA=="
},
"@fontsource-variable/jetbrains-mono@5.3.0": {
"integrity": "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw=="
@@ -489,6 +490,9 @@
"@fontsource-variable/space-grotesk@5.3.0": {
"integrity": "sha512-2IxmvfB08i9vnGB3Ym/AXvhRE+8XOjWMXIyDum03c+tPwH0FUoMNQfGpU8NXPxjbws0Vvss3AH0Zqt4oJBBAdw=="
},
"@fontsource/comic-neue@5.3.0": {
"integrity": "sha512-uZsyUTExw8tCySNhL65dN6rEUhASMPGintSS5Kw/ZkrQTK1vcAgramzOLT/mZKtZ+EXr03MHj0s5osVVJITCow=="
},
"@fontsource/instrument-serif@5.3.0": {
"integrity": "sha512-mDiaIg0u67sYV59fie92Wz4sM8UiVlbL7fLxnFPCKkX15DMASEnQTREbaP5S5/3DCcsoAOcQ0sV9E4AeY4QqYQ=="
},
@@ -3081,12 +3085,13 @@
"packageJson": {
"dependencies": [
"npm:@eslint/js@^10.0.1",
"npm:@fontsource-variable/fraunces@5.3.0",
"npm:@fontsource-variable/bricolage-grotesque@5.3.0",
"npm:@fontsource-variable/jetbrains-mono@5.3.0",
"npm:@fontsource-variable/libre-franklin@5.3.0",
"npm:@fontsource-variable/newsreader@5.3.0",
"npm:@fontsource-variable/saira@5.3.0",
"npm:@fontsource-variable/space-grotesk@5.3.0",
"npm:@fontsource/comic-neue@5.3.0",
"npm:@fontsource/instrument-serif@5.3.0",
"npm:@fontsource/manufacturing-consent@5.3.0",
"npm:@lingui/cli@6.8.0",

File diff suppressed because one or more lines are too long

View File

@@ -19,12 +19,12 @@ msgstr "[deleted]"
#. placeholder {0}: dump.commentCount
#: src/components/dump/DumpCard.tsx:135
#: src/components/dump/JournalCard.tsx:136
#: src/components/dump/JournalCard.tsx:137
msgid "{0, plural, one {# comment} other {# comments}}"
msgstr "{0, plural, one {# comment} other {# comments}}"
#. placeholder {0}: playlist.dumpCount
#: src/components/playlist/PlaylistCard.tsx:131
#: src/components/playlist/PlaylistCard.tsx:141
msgid "{0, plural, one {# dump} other {# dumps}}"
msgstr "{0, plural, one {# dump} other {# dumps}}"
@@ -396,6 +396,10 @@ msgstr "Back to login"
msgid "Back to the login page"
msgstr "Back to the login page"
#: src/components/layout/Marquee.tsx:27
msgid "Best viewed in Netscape Navigator at 800×600"
msgstr "Best viewed in Netscape Navigator at 800×600"
#: src/components/federation/PeerManager.tsx:184
#: src/components/federation/PeerManager.tsx:190
msgid "Block"
@@ -450,7 +454,7 @@ msgstr "Cancel removal"
msgid "Cancel reply"
msgstr "Cancel reply"
#: src/components/layout/AppHeader.tsx:181
#: src/components/layout/AppHeader.tsx:182
msgid "Cancel search"
msgstr "Cancel search"
@@ -521,7 +525,7 @@ msgstr "Close anyway"
msgid "Close emoji picker"
msgstr "Close emoji picker"
#: src/components/player/GlobalPlayer.tsx:262
#: src/components/player/GlobalPlayer.tsx:264
msgid "Close player"
msgstr "Close player"
@@ -529,7 +533,7 @@ msgstr "Close player"
msgid "Close without posting? The attached file won't be kept — everything you typed will."
msgstr "Close without posting? The attached file won't be kept — everything you typed will."
#: src/components/player/GlobalPlayer.tsx:254
#: src/components/player/GlobalPlayer.tsx:256
msgid "Collapse player"
msgstr "Collapse player"
@@ -628,7 +632,7 @@ msgstr "Create"
msgid "Create & Add"
msgstr "Create & Add"
#: src/components/playlist/PlaylistCard.tsx:142
#: src/components/playlist/PlaylistCard.tsx:152
msgid "created {ago}"
msgstr "created {ago}"
@@ -679,7 +683,7 @@ msgstr "Delete dump"
msgid "Delete failed ({0})"
msgstr "Delete failed ({0})"
#: src/components/playlist/PlaylistCard.tsx:162
#: src/components/playlist/PlaylistCard.tsx:172
#: src/components/playlist/PlaylistEditForm.tsx:213
#: src/components/profile/ProfilePlaylistsTab.tsx:224
#: src/pages/PlaylistDetail.tsx:172
@@ -737,7 +741,7 @@ msgid "Done"
msgstr "Done"
#. placeholder {0}: dump.fileName
#: src/components/dump/FilePreview.tsx:226
#: src/components/dump/FilePreview.tsx:233
msgid "Download {0}"
msgstr "Download {0}"
@@ -753,7 +757,7 @@ msgstr "Drop a file here, or paste one"
msgid "Drop a replacement here"
msgstr "Drop a replacement here"
#: src/components/layout/AppHeader.tsx:200
#: src/components/layout/AppHeader.tsx:201
msgid "Dump"
msgstr "Dump"
@@ -788,7 +792,7 @@ msgstr "Earlier"
#: src/components/chat/ChatMessageItem.tsx:108
#: src/components/chat/PendingItem.tsx:93
#: src/components/comment/CommentNode.tsx:256
#: src/components/playlist/PlaylistHeader.tsx:60
#: src/components/playlist/PlaylistHeader.tsx:64
#: src/pages/Dump.tsx:623
msgid "Edit"
msgstr "Edit"
@@ -820,7 +824,7 @@ msgstr "edited"
#. placeholder {0}: relativeTime(message.updatedAt)
#: src/components/chat/ChatMessageItem.tsx:132
#: src/components/comment/CommentNode.tsx:205
#: src/components/playlist/PlaylistHeader.tsx:95
#: src/components/playlist/PlaylistHeader.tsx:99
#: src/pages/Dump.tsx:576
msgid "edited {0}"
msgstr "edited {0}"
@@ -830,7 +834,7 @@ msgstr "edited {0}"
#. placeholder {0}: message.updatedAt.toLocaleString()
#: src/components/chat/ChatMessageItem.tsx:128
#: src/components/comment/CommentNode.tsx:203
#: src/components/playlist/PlaylistHeader.tsx:93
#: src/components/playlist/PlaylistHeader.tsx:97
#: src/pages/Dump.tsx:574
msgid "Edited {0}"
msgstr "Edited {0}"
@@ -856,7 +860,7 @@ msgstr "Email address"
msgid "Enter a query to search."
msgstr "Enter a query to search."
#: src/components/player/GlobalPlayer.tsx:254
#: src/components/player/GlobalPlayer.tsx:256
msgid "Expand player"
msgstr "Expand player"
@@ -1114,7 +1118,7 @@ msgstr "Link or file"
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
#: src/components/layout/AppHeader.tsx:233
#: src/components/layout/AppHeader.tsx:236
msgid "Live updates unavailable."
msgstr "Live updates unavailable."
@@ -1159,7 +1163,7 @@ msgstr "Loading profile…"
msgid "Loading…"
msgstr "Loading…"
#: src/components/layout/AppHeader.tsx:207
#: src/components/layout/AppHeader.tsx:208
#: src/pages/FederationCallback.tsx:32
#: src/pages/UserLogin.tsx:41
#: src/pages/UserLogin.tsx:66
@@ -1273,7 +1277,7 @@ msgstr "New public playlists"
msgid "Next →"
msgstr "Next →"
#: src/components/player/GlobalPlayer.tsx:244
#: src/components/player/GlobalPlayer.tsx:246
msgid "Next track"
msgstr "Next track"
@@ -1388,11 +1392,11 @@ msgid "email.welcome.button"
msgstr "Open {site}"
#. placeholder {0}: richContent.siteName
#: src/components/dump/RichContentCard.tsx:122
#: src/components/dump/RichContentCard.tsx:124
msgid "Open on {0}"
msgstr "Open on {0}"
#: src/components/dump/RichContentCard.tsx:123
#: src/components/dump/RichContentCard.tsx:125
msgid "Open source"
msgstr "Open source"
@@ -1453,9 +1457,9 @@ msgstr "Passwords do not match"
msgid "Paste a link…"
msgstr "Paste a link…"
#: src/components/dump/FilePreview.tsx:70
#: src/components/dump/FilePreview.tsx:184
#: src/components/dump/RichContentCard.tsx:103
#: src/components/dump/FilePreview.tsx:72
#: src/components/dump/FilePreview.tsx:191
#: src/components/dump/RichContentCard.tsx:105
#: src/components/player/MediaPlayer.tsx:276
msgid "Pause"
msgstr "Pause"
@@ -1464,10 +1468,10 @@ msgstr "Pause"
msgid "Picked up where you left off."
msgstr "Picked up where you left off."
#: src/components/dump/FilePreview.tsx:70
#: src/components/dump/FilePreview.tsx:184
#: src/components/dump/RichContentCard.tsx:103
#: src/components/dump/RichContentCard.tsx:150
#: src/components/dump/FilePreview.tsx:72
#: src/components/dump/FilePreview.tsx:191
#: src/components/dump/RichContentCard.tsx:105
#: src/components/dump/RichContentCard.tsx:153
#: src/components/player/MediaPlayer.tsx:276
msgid "Play"
msgstr "Play"
@@ -1477,7 +1481,7 @@ msgid "Playlist categories"
msgstr "Playlist categories"
#: src/components/dump/create/StepIndicator.tsx:21
#: src/components/layout/AppHeader.tsx:173
#: src/components/layout/AppHeader.tsx:174
#: src/components/layout/UserMenu.tsx:93
#: src/components/profile/ProfileFollowTab.tsx:147
#: src/components/profile/ProfilePlaylistsTab.tsx:162
@@ -1502,14 +1506,14 @@ msgstr "Post reply"
msgid "Posting…"
msgstr "Posting…"
#: src/components/player/GlobalPlayer.tsx:228
#: src/components/player/GlobalPlayer.tsx:230
msgid "Previous track"
msgstr "Previous track"
#: src/components/dump/DumpCard.tsx:144
#: src/components/dump/JournalCard.tsx:145
#: src/components/playlist/PlaylistCard.tsx:117
#: src/components/playlist/PlaylistHeader.tsx:77
#: src/components/dump/JournalCard.tsx:146
#: src/components/playlist/PlaylistCard.tsx:127
#: src/components/playlist/PlaylistHeader.tsx:81
#: src/components/playlist/PlaylistMembershipPanel.tsx:81
#: src/pages/Dump.tsx:582
msgid "private"
@@ -1519,8 +1523,8 @@ msgstr "private"
msgid "Private"
msgstr "Private"
#: src/components/playlist/PlaylistCard.tsx:116
#: src/components/playlist/PlaylistHeader.tsx:77
#: src/components/playlist/PlaylistCard.tsx:126
#: src/components/playlist/PlaylistHeader.tsx:81
msgid "public"
msgstr "public"
@@ -1532,7 +1536,7 @@ msgstr "Public"
msgid "Refresh metadata"
msgstr "Refresh metadata"
#: src/components/player/GlobalPlayer.tsx:324
#: src/components/player/GlobalPlayer.tsx:326
msgid "Refreshing stream…"
msgstr "Refreshing stream…"
@@ -1671,7 +1675,7 @@ msgstr "Save"
msgid "Saving…"
msgstr "Saving…"
#: src/components/layout/AppHeader.tsx:181
#: src/components/layout/AppHeader.tsx:182
#: src/components/layout/SearchBar.tsx:83
#: src/pages/Search.tsx:106
msgid "Search"
@@ -1719,7 +1723,7 @@ msgstr "Send reset link"
msgid "Sending…"
msgstr "Sending…"
#: src/components/layout/AppHeader.tsx:197
#: src/components/layout/AppHeader.tsx:198
msgid "Server unreachable"
msgstr "Server unreachable"
@@ -1860,6 +1864,10 @@ msgstr "This month"
msgid "This page does not exist."
msgstr "This page does not exist."
#: src/components/layout/Marquee.tsx:28
msgid "This page is under construction"
msgstr "This page is under construction"
#: src/pages/FederationCallback.tsx:40
msgid "This sign-in link is incomplete."
msgstr "This sign-in link is incomplete."
@@ -1911,7 +1919,7 @@ msgid "digest.topWeek"
msgstr "Top this week"
#. placeholder {0}: i + 1
#: src/components/player/GlobalPlayer.tsx:338
#: src/components/player/GlobalPlayer.tsx:340
msgid "Track {0}"
msgstr "Track {0}"
@@ -1985,7 +1993,7 @@ msgstr "Unsubscribed"
msgid "Updated"
msgstr "Updated"
#: src/components/playlist/PlaylistCard.tsx:141
#: src/components/playlist/PlaylistCard.tsx:151
msgid "updated {ago}"
msgstr "updated {ago}"
@@ -2091,6 +2099,10 @@ msgstr "Volume"
msgid "digest.weekly"
msgstr "Weekly digest"
#: src/components/layout/Marquee.tsx:26
msgid "Welcome to {SITE_NAME}!"
msgstr "Welcome to {SITE_NAME}!"
#. js-lingui-explicit-id
#: api/mail/welcome.ts:69
msgid "email.welcome.subject"

File diff suppressed because one or more lines are too long

View File

@@ -19,12 +19,12 @@ msgstr "[supprimé]"
#. placeholder {0}: dump.commentCount
#: src/components/dump/DumpCard.tsx:135
#: src/components/dump/JournalCard.tsx:136
#: src/components/dump/JournalCard.tsx:137
msgid "{0, plural, one {# comment} other {# comments}}"
msgstr "{0, plural, one {# commentaire} other {# commentaires}}"
#. placeholder {0}: playlist.dumpCount
#: src/components/playlist/PlaylistCard.tsx:131
#: src/components/playlist/PlaylistCard.tsx:141
msgid "{0, plural, one {# dump} other {# dumps}}"
msgstr "{0, plural, one {# reco} other {# recos}}"
@@ -396,6 +396,10 @@ msgstr "Retour à la connexion"
msgid "Back to the login page"
msgstr "Retour à la page de connexion"
#: src/components/layout/Marquee.tsx:27
msgid "Best viewed in Netscape Navigator at 800×600"
msgstr "Optimisé pour Netscape Navigator en 800×600"
#: src/components/federation/PeerManager.tsx:184
#: src/components/federation/PeerManager.tsx:190
msgid "Block"
@@ -450,7 +454,7 @@ msgstr "Annuler la suppression"
msgid "Cancel reply"
msgstr "Annuler la réponse"
#: src/components/layout/AppHeader.tsx:181
#: src/components/layout/AppHeader.tsx:182
msgid "Cancel search"
msgstr "Annuler la recherche"
@@ -521,7 +525,7 @@ msgstr "Fermer quand même"
msgid "Close emoji picker"
msgstr "Fermer le sélecteur d'emoji"
#: src/components/player/GlobalPlayer.tsx:262
#: src/components/player/GlobalPlayer.tsx:264
msgid "Close player"
msgstr "Fermer le lecteur"
@@ -529,7 +533,7 @@ msgstr "Fermer le lecteur"
msgid "Close without posting? The attached file won't be kept — everything you typed will."
msgstr "Fermer sans publier ? Le fichier joint sera perdu — ce que vous avez écrit sera conservé."
#: src/components/player/GlobalPlayer.tsx:254
#: src/components/player/GlobalPlayer.tsx:256
msgid "Collapse player"
msgstr "Réduire le lecteur"
@@ -628,7 +632,7 @@ msgstr "Créer"
msgid "Create & Add"
msgstr "Créer et ajouter"
#: src/components/playlist/PlaylistCard.tsx:142
#: src/components/playlist/PlaylistCard.tsx:152
msgid "created {ago}"
msgstr "créée {ago}"
@@ -679,7 +683,7 @@ msgstr "Supprimer la reco"
msgid "Delete failed ({0})"
msgstr "Échec de la suppression ({0})"
#: src/components/playlist/PlaylistCard.tsx:162
#: src/components/playlist/PlaylistCard.tsx:172
#: src/components/playlist/PlaylistEditForm.tsx:213
#: src/components/profile/ProfilePlaylistsTab.tsx:224
#: src/pages/PlaylistDetail.tsx:172
@@ -737,7 +741,7 @@ msgid "Done"
msgstr "Terminé"
#. placeholder {0}: dump.fileName
#: src/components/dump/FilePreview.tsx:226
#: src/components/dump/FilePreview.tsx:233
msgid "Download {0}"
msgstr "Télécharger {0}"
@@ -753,7 +757,7 @@ msgstr "Déposez un fichier ici, ou collez-en un"
msgid "Drop a replacement here"
msgstr "Déposez un fichier de remplacement ici"
#: src/components/layout/AppHeader.tsx:200
#: src/components/layout/AppHeader.tsx:201
msgid "Dump"
msgstr "Reco"
@@ -788,7 +792,7 @@ msgstr "Plus tôt"
#: src/components/chat/ChatMessageItem.tsx:108
#: src/components/chat/PendingItem.tsx:93
#: src/components/comment/CommentNode.tsx:256
#: src/components/playlist/PlaylistHeader.tsx:60
#: src/components/playlist/PlaylistHeader.tsx:64
#: src/pages/Dump.tsx:623
msgid "Edit"
msgstr "Modifier"
@@ -820,7 +824,7 @@ msgstr "modifié"
#. placeholder {0}: relativeTime(message.updatedAt)
#: src/components/chat/ChatMessageItem.tsx:132
#: src/components/comment/CommentNode.tsx:205
#: src/components/playlist/PlaylistHeader.tsx:95
#: src/components/playlist/PlaylistHeader.tsx:99
#: src/pages/Dump.tsx:576
msgid "edited {0}"
msgstr "modifié {0}"
@@ -830,7 +834,7 @@ msgstr "modifié {0}"
#. placeholder {0}: message.updatedAt.toLocaleString()
#: src/components/chat/ChatMessageItem.tsx:128
#: src/components/comment/CommentNode.tsx:203
#: src/components/playlist/PlaylistHeader.tsx:93
#: src/components/playlist/PlaylistHeader.tsx:97
#: src/pages/Dump.tsx:574
msgid "Edited {0}"
msgstr "Modifié le {0}"
@@ -856,7 +860,7 @@ msgstr "Adresse e-mail"
msgid "Enter a query to search."
msgstr "Saisissez une recherche."
#: src/components/player/GlobalPlayer.tsx:254
#: src/components/player/GlobalPlayer.tsx:256
msgid "Expand player"
msgstr "Agrandir le lecteur"
@@ -1114,7 +1118,7 @@ msgstr "Lien ou fichier"
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
#: src/components/layout/AppHeader.tsx:233
#: src/components/layout/AppHeader.tsx:236
msgid "Live updates unavailable."
msgstr "Mises à jour en direct indisponibles."
@@ -1159,7 +1163,7 @@ msgstr "Chargement du profil…"
msgid "Loading…"
msgstr "Chargement…"
#: src/components/layout/AppHeader.tsx:207
#: src/components/layout/AppHeader.tsx:208
#: src/pages/FederationCallback.tsx:32
#: src/pages/UserLogin.tsx:41
#: src/pages/UserLogin.tsx:66
@@ -1273,7 +1277,7 @@ msgstr "Nouvelles collections publiques"
msgid "Next →"
msgstr "Suivant →"
#: src/components/player/GlobalPlayer.tsx:244
#: src/components/player/GlobalPlayer.tsx:246
msgid "Next track"
msgstr "Piste suivante"
@@ -1388,11 +1392,11 @@ msgid "email.welcome.button"
msgstr "Ouvrir {site}"
#. placeholder {0}: richContent.siteName
#: src/components/dump/RichContentCard.tsx:122
#: src/components/dump/RichContentCard.tsx:124
msgid "Open on {0}"
msgstr "Ouvrir sur {0}"
#: src/components/dump/RichContentCard.tsx:123
#: src/components/dump/RichContentCard.tsx:125
msgid "Open source"
msgstr "Ouvrir la source"
@@ -1453,9 +1457,9 @@ msgstr "Les mots de passe ne correspondent pas"
msgid "Paste a link…"
msgstr "Collez un lien…"
#: src/components/dump/FilePreview.tsx:70
#: src/components/dump/FilePreview.tsx:184
#: src/components/dump/RichContentCard.tsx:103
#: src/components/dump/FilePreview.tsx:72
#: src/components/dump/FilePreview.tsx:191
#: src/components/dump/RichContentCard.tsx:105
#: src/components/player/MediaPlayer.tsx:276
msgid "Pause"
msgstr "Pause"
@@ -1464,10 +1468,10 @@ msgstr "Pause"
msgid "Picked up where you left off."
msgstr "Reprise de votre brouillon."
#: src/components/dump/FilePreview.tsx:70
#: src/components/dump/FilePreview.tsx:184
#: src/components/dump/RichContentCard.tsx:103
#: src/components/dump/RichContentCard.tsx:150
#: src/components/dump/FilePreview.tsx:72
#: src/components/dump/FilePreview.tsx:191
#: src/components/dump/RichContentCard.tsx:105
#: src/components/dump/RichContentCard.tsx:153
#: src/components/player/MediaPlayer.tsx:276
msgid "Play"
msgstr "Lecture"
@@ -1477,7 +1481,7 @@ msgid "Playlist categories"
msgstr "Catégories de collections"
#: src/components/dump/create/StepIndicator.tsx:21
#: src/components/layout/AppHeader.tsx:173
#: src/components/layout/AppHeader.tsx:174
#: src/components/layout/UserMenu.tsx:93
#: src/components/profile/ProfileFollowTab.tsx:147
#: src/components/profile/ProfilePlaylistsTab.tsx:162
@@ -1502,14 +1506,14 @@ msgstr "Publier la réponse"
msgid "Posting…"
msgstr "Publication…"
#: src/components/player/GlobalPlayer.tsx:228
#: src/components/player/GlobalPlayer.tsx:230
msgid "Previous track"
msgstr "Piste précédente"
#: src/components/dump/DumpCard.tsx:144
#: src/components/dump/JournalCard.tsx:145
#: src/components/playlist/PlaylistCard.tsx:117
#: src/components/playlist/PlaylistHeader.tsx:77
#: src/components/dump/JournalCard.tsx:146
#: src/components/playlist/PlaylistCard.tsx:127
#: src/components/playlist/PlaylistHeader.tsx:81
#: src/components/playlist/PlaylistMembershipPanel.tsx:81
#: src/pages/Dump.tsx:582
msgid "private"
@@ -1519,8 +1523,8 @@ msgstr "privé"
msgid "Private"
msgstr "Privé"
#: src/components/playlist/PlaylistCard.tsx:116
#: src/components/playlist/PlaylistHeader.tsx:77
#: src/components/playlist/PlaylistCard.tsx:126
#: src/components/playlist/PlaylistHeader.tsx:81
msgid "public"
msgstr "public"
@@ -1532,7 +1536,7 @@ msgstr "Public"
msgid "Refresh metadata"
msgstr "Actualiser les métadonnées"
#: src/components/player/GlobalPlayer.tsx:324
#: src/components/player/GlobalPlayer.tsx:326
msgid "Refreshing stream…"
msgstr "Actualisation du flux…"
@@ -1671,7 +1675,7 @@ msgstr "Enregistrer"
msgid "Saving…"
msgstr "Enregistrement…"
#: src/components/layout/AppHeader.tsx:181
#: src/components/layout/AppHeader.tsx:182
#: src/components/layout/SearchBar.tsx:83
#: src/pages/Search.tsx:106
msgid "Search"
@@ -1719,7 +1723,7 @@ msgstr "Envoyer le lien de réinitialisation"
msgid "Sending…"
msgstr "Envoi…"
#: src/components/layout/AppHeader.tsx:197
#: src/components/layout/AppHeader.tsx:198
msgid "Server unreachable"
msgstr "Serveur inaccessible"
@@ -1860,6 +1864,10 @@ msgstr "Ce mois-ci"
msgid "This page does not exist."
msgstr "Rien à voir, circulez."
#: src/components/layout/Marquee.tsx:28
msgid "This page is under construction"
msgstr "Cette page est en construction"
#: src/pages/FederationCallback.tsx:40
msgid "This sign-in link is incomplete."
msgstr "Ce lien de connexion est incomplet."
@@ -1911,7 +1919,7 @@ msgid "digest.topWeek"
msgstr "Les plus validées de la semaine"
#. placeholder {0}: i + 1
#: src/components/player/GlobalPlayer.tsx:338
#: src/components/player/GlobalPlayer.tsx:340
msgid "Track {0}"
msgstr "Piste {0}"
@@ -1985,7 +1993,7 @@ msgstr "Désabonné"
msgid "Updated"
msgstr "Mises à jour"
#: src/components/playlist/PlaylistCard.tsx:141
#: src/components/playlist/PlaylistCard.tsx:151
msgid "updated {ago}"
msgstr "mise à jour {ago}"
@@ -2091,6 +2099,10 @@ msgstr "Volume"
msgid "digest.weekly"
msgstr "Résumé hebdomadaire"
#: src/components/layout/Marquee.tsx:26
msgid "Welcome to {SITE_NAME}!"
msgstr "Bienvenue sur {SITE_NAME} !"
#. js-lingui-explicit-id
#: api/mail/welcome.ts:69
msgid "email.welcome.subject"

View File

@@ -10,12 +10,13 @@
"preview": "vite preview"
},
"dependencies": {
"@fontsource-variable/fraunces": "5.3.0",
"@fontsource-variable/bricolage-grotesque": "5.3.0",
"@fontsource-variable/jetbrains-mono": "5.3.0",
"@fontsource-variable/saira": "5.3.0",
"@fontsource-variable/space-grotesk": "5.3.0",
"@fontsource-variable/libre-franklin": "5.3.0",
"@fontsource-variable/newsreader": "5.3.0",
"@fontsource/comic-neue": "5.3.0",
"@fontsource/instrument-serif": "5.3.0",
"@fontsource/manufacturing-consent": "5.3.0",
"@lingui/cli": "6.8.0",

View File

@@ -2731,7 +2731,11 @@ body.has-player .chat-fab {
display: flex;
align-items: center;
gap: 0.5rem;
overflow-wrap: anywhere;
/* A word stays whole unless it alone is wider than the line. Breaking
anywhere lets the text be squeezed under the width of a word, and Firefox
sizes a title set with negative letter-spacing a hair narrower than it
lays it out, which dropped the last letter to a line of its own. */
overflow-wrap: break-word;
}
.page-title-icon {
@@ -2769,6 +2773,12 @@ body.has-player .chat-fab {
padding: 1rem;
}
/* A card that holds one line of text, such as a list with nothing in it yet:
the card's padding spaces it, not the paragraph's margins as well. */
.page-card > p:only-child {
margin: 0;
}
/* A list of dump or playlist cards in a group fills the column, its edges in
line with the label's rule. */
.page-group .dump-feed {
@@ -2855,6 +2865,13 @@ body.has-player .chat-fab {
flex-shrink: 0;
color: var(--color-text);
text-decoration: none;
/* It answers the pointer as a card's title does (.dump-card-title): the
accent, eased in. A theme that colors the name restates the hover. */
transition: color var(--dur-base);
}
.app-header-brand:hover {
color: var(--color-accent);
}
.app-header-center {
@@ -2929,7 +2946,7 @@ body.has-player .chat-fab {
/* When the search bar is expanded, immediately clear the rest of the center —
the expanded input needs the full column width */
.app-header-center:has(.search-bar--expanded) .index-presence,
.app-header-center:has(.search-bar--expanded) .feed-sort {
.app-header-center:has(.search-bar--expanded) .feed-sort-scroller {
display: none;
}
@@ -2942,10 +2959,6 @@ body.has-player .chat-fab {
min-width: 0;
}
.app-header-brand:hover {
color: var(--color-accent);
}
.app-header-nav {
display: flex;
align-items: center;
@@ -3792,90 +3805,54 @@ body.has-player .chat-fab {
/* ── Feed sort scroller + buttons ── */
/* Wrapper: constrains width, clips scroll overflow, and carries the gradient
hint. --tab-row-bg is what the row sits on, which the fade and the pinned
leading content paint so the tabs vanish under them. */
/* Wrapper: the leading content (the scope switcher), then the row of tabs,
which scrolls on its own beside it. Nothing here paints the ground the row
sits on: a theme's page may be a gradient or a picture, which a patch of
flat color would show against. */
.feed-sort-scroller {
position: relative;
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
overflow: hidden;
--tab-row-bg: var(--color-bg);
}
.app-header .feed-sort-scroller {
--tab-row-bg: var(--header-bg);
}
/* Right-edge fade, over the scroll container whatever its scroll position.
It shows only while tabs are hidden past that edge (TabBar's
--more), so it never dims the last tab once it is scrolled to or fits. */
.feed-sort-scroller::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 2rem;
background: linear-gradient(to right, transparent, var(--tab-row-bg));
pointer-events: none;
opacity: 0;
transition: opacity var(--dur-base);
}
.feed-sort-scroller--more::after {
opacity: 1;
}
/* Its twin on the left edge once the row has scrolled, unless leading content
is pinned there, which draws its own (.feed-sort-leading::after). */
.feed-sort-scroller::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 1;
width: 2rem;
background: linear-gradient(to left, transparent, var(--tab-row-bg));
pointer-events: none;
opacity: 0;
transition: opacity var(--dur-base);
}
.feed-sort-scroller--more-left:not(:has(.feed-sort-leading))::before {
opacity: 1;
}
/* The leading content (the scope switcher) stays at the row's start while the
tabs scroll under it. Its padding hides them right after its divider, and
the matching negative margin keeps the gap to the first tab unchanged. */
.feed-sort-leading {
position: sticky;
left: 0;
z-index: 1;
display: flex;
flex-shrink: 0;
padding-right: 0.4rem;
margin-right: -0.4rem;
background: var(--tab-row-bg);
}
/* Once tabs have scrolled under it, a short fade softens their cut edge. */
.feed-sort-leading::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 100%;
width: 0.75rem;
background: linear-gradient(to left, transparent, var(--tab-row-bg));
pointer-events: none;
opacity: 0;
transition: opacity var(--dur-base);
/* The row fades out at an edge while tabs are hidden past it (TabBar's
--more and --more-left), so a fade never dims a tab that is scrolled to or
fits. The fade is a mask, the tabs thinning out over whatever is behind
them. The two lengths are registered so that they can ease in. */
@property --tab-fade-start {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
.feed-sort-scroller--more-left .feed-sort-leading::after {
opacity: 1;
@property --tab-fade-end {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
.feed-sort-scroller {
transition: --tab-fade-start var(--dur-base), --tab-fade-end var(--dur-base);
}
.feed-sort-scroller--more {
--tab-fade-end: 2rem;
}
.feed-sort-scroller--more-left {
--tab-fade-start: 2rem;
}
/* Beside leading content the tabs are cut at its divider, and a shorter fade
softens the cut. */
.feed-sort-scroller--more-left:has(.feed-sort-leading) {
--tab-fade-start: 0.75rem;
}
/* In the index below-header the scroller should fill the available row width */
@@ -3889,6 +3866,13 @@ body.has-player .chat-fab {
gap: 0.4rem;
overflow-x: auto;
scrollbar-width: none;
mask-image: linear-gradient(
to right,
transparent,
#000 var(--tab-fade-start),
#000 calc(100% - var(--tab-fade-end)),
transparent
);
/* Symmetric, so the tabs stay centred against their row (the header's
brand and buttons), with room on both sides for the focus ring that
overflow-x would otherwise clip. */
@@ -7543,7 +7527,7 @@ body.has-player .chat-fab {
}
/* Vertical rule separating the scope selector from the sort tabs. */
.feed-sort .category-switcher::after {
.feed-sort-leading .category-switcher::after {
content: "";
width: 2px;
height: 1.05rem;
@@ -7636,7 +7620,7 @@ body.has-player .chat-fab {
.category-link {
padding: 0.1rem 0.5rem;
border-radius: var(--radius-full);
background: var(--color-header-user-bg);
background: var(--color-badge-bg);
color: var(--color-text-secondary);
text-decoration: none;
font-size: var(--text-xs);

View File

@@ -15,6 +15,7 @@ import { NotificationBell } from "./NotificationBell.tsx";
import { ChatButton } from "./ChatButton.tsx";
import { UserMenu } from "./UserMenu.tsx";
import { SearchBar } from "./SearchBar.tsx";
import { Marquee } from "./Marquee.tsx";
import { IconClose, IconSearch } from "./NavIcons.tsx";
import { SITE_EMOJI, SITE_NAME } from "../../hooks/shared/useDocumentTitle.ts";
import { playlistsPath, searchPath } from "../../../shared/paths.ts";
@@ -227,6 +228,8 @@ export function AppHeader(
</div>
</div>
<Marquee />
{wsStatus === "disconnected" && wsErrorMessage && (
<div className="app-header-status" role="alert">
<strong>

View File

@@ -0,0 +1,40 @@
/**
* Renders the GeoCities style's marquee: a line of welcome that crawls across
* the page under the header, as a `<marquee>` did on a homepage of that era.
* `AppHeader` renders it on every page, and it draws nothing in any other
* style.
*
* @module
*/
import { Trans } from "@lingui/react/macro";
import { useTheme } from "../../hooks/theme/useTheme.ts";
import { SITE_NAME } from "../../hooks/shared/useDocumentTitle.ts";
/**
* Renders the banner's text twice, side by side: geocities.css slides the
* pair left by half its width, so the second copy arrives where the first
* began and the loop has no seam. It is decoration, hidden from assistive
* technology, and stands still for a reader who asked for reduced motion.
*/
export function Marquee(): React.JSX.Element | null {
const { style } = useTheme();
if (style !== "geocities") return null;
const line = (
<span>
✦ <Trans>Welcome to {SITE_NAME}!</Trans> ✦{" "}
<Trans>Best viewed in Netscape Navigator at 800×600</Trans> ✦{" "}
<Trans>This page is under construction</Trans> ✦
</span>
);
return (
<div className="site-marquee" aria-hidden="true">
<div className="site-marquee-track">
{line}
{line}
</div>
</div>
);
}

View File

@@ -27,9 +27,9 @@ export interface TabBarProps<T extends string> {
/** An extra class for the row. */
innerClassName?: string;
/**
* Optional content rendered inside the row, before the tabs (e.g. a scope
* selector). Sits in the same flex line, aligned with them, and stays pinned
* at the row's start while they scroll under it.
* Optional content rendered before the tabs (e.g. a scope selector). Sits
* in the same line, aligned with them, and stays where it is while they
* scroll: it is outside the row that scrolls, which clips them at its edge.
*/
leading?: ReactNode;
}
@@ -43,7 +43,6 @@ export function TabBar<T extends string>(
TabBarProps<T>,
): React.JSX.Element {
const rowRef = useRef<HTMLDivElement>(null);
const leadingRef = useRef<HTMLDivElement>(null);
const [overflowing, setOverflowing] = useState(false);
// Tabs hidden past either edge, which the stylesheet fades only then, so a
// fade never dims a tab that is fully in view.
@@ -69,19 +68,17 @@ export function TabBar<T extends string>(
};
}, [tabs.length]);
// When they don't all fit, keep the active tab in view, between the leading
// content pinned over the row's left edge and the fade over its right edge,
// scrolling the row alone, never the page.
// When they don't all fit, keep the active tab in view, clear of the fade
// over the row's right edge, scrolling the row alone, never the page.
useLayoutEffect(() => {
const row = rowRef.current;
const active = row?.querySelector<HTMLElement>(".feed-sort-btn.active");
if (!row || !active || !overflowing) return;
// The fade is 2rem wide (.feed-sort-scroller::after).
// The fade is 2rem wide (--tab-fade-end on .feed-sort).
const fade =
parseFloat(getComputedStyle(document.documentElement).fontSize) * 2;
const r = row.getBoundingClientRect(), b = active.getBoundingClientRect();
const left = r.left + (leadingRef.current?.offsetWidth ?? 0);
if (b.left < left) row.scrollLeft -= left - b.left;
if (b.left < r.left) row.scrollLeft -= r.left - b.left;
else if (b.right > r.right - fade) {
row.scrollLeft += b.right - r.right + fade;
}
@@ -95,13 +92,11 @@ export function TabBar<T extends string>(
className ? ` ${className}` : ""
}`}
>
{leading && <div className="feed-sort-leading">{leading}</div>}
<div
ref={rowRef}
className={`feed-sort${innerClassName ? ` ${innerClassName}` : ""}`}
>
{leading && (
<div ref={leadingRef} className="feed-sort-leading">{leading}</div>
)}
{tabs.map(({ key, label }) => (
<button
key={key}

View File

@@ -62,6 +62,46 @@
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* geocities: what stands in for Comic Sans where the system has none (Linux,
Android), so it comes after it in the theme's stack and is only downloaded
there. The package has no latin-ext subset. */
@font-face {
font-family: 'Comic Neue';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url(@fontsource/comic-neue/files/comic-neue-latin-400-normal.woff2)
format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Comic Neue';
font-style: italic;
font-display: swap;
font-weight: 400;
src: url(@fontsource/comic-neue/files/comic-neue-latin-400-italic.woff2)
format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Comic Neue';
font-style: normal;
font-display: swap;
font-weight: 700;
src: url(@fontsource/comic-neue/files/comic-neue-latin-700-normal.woff2)
format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Comic Neue';
font-style: italic;
font-display: swap;
font-weight: 700;
src: url(@fontsource/comic-neue/files/comic-neue-latin-700-italic.woff2)
format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* nyt: the headlines' serif, the text's (opsz as well as wght), the
interface's sans, and the nameplate's blackletter */
@font-face {
@@ -229,40 +269,23 @@
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* melange: opsz, SOFT and WONK axes as well as wght */
/* melange: the weight axis alone, which leaves the face at its text optical
size for a headline too. The family has no italic. */
@font-face {
font-family: 'Fraunces Variable';
font-family: 'Bricolage Grotesque Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url(@fontsource-variable/fraunces/files/fraunces-latin-ext-full-normal.woff2)
font-weight: 200 800;
src: url(@fontsource-variable/bricolage-grotesque/files/bricolage-grotesque-latin-ext-wght-normal.woff2)
format('woff2-variations');
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
font-family: 'Fraunces Variable';
font-family: 'Bricolage Grotesque Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url(@fontsource-variable/fraunces/files/fraunces-latin-full-normal.woff2)
format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Fraunces Variable';
font-style: italic;
font-display: swap;
font-weight: 100 900;
src: url(@fontsource-variable/fraunces/files/fraunces-latin-ext-full-italic.woff2)
format('woff2-variations');
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}
@font-face {
font-family: 'Fraunces Variable';
font-style: italic;
font-display: swap;
font-weight: 100 900;
src: url(@fontsource-variable/fraunces/files/fraunces-latin-full-italic.woff2)
font-weight: 200 800;
src: url(@fontsource-variable/bricolage-grotesque/files/bricolage-grotesque-latin-wght-normal.woff2)
format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}

View File

@@ -51,7 +51,8 @@ export function useRoomBeside(
// The tabs are observed too, since their width changes without their
// box's (a web font arriving, a tab added at sign-in), and re-observed as
// they come and go.
// they come and go. They are the children of a child, the row that
// scrolls them, when a scope switcher comes before it.
const resize = new ResizeObserver(measure);
const mutation = new MutationObserver(observe);
function observe() {
@@ -60,8 +61,13 @@ export function useRoomBeside(
resize.observe(container!);
const kept = container!.querySelector<HTMLElement>(beside!);
if (kept) {
for (const child of kept.children) resize.observe(child);
mutation.observe(kept, { childList: true });
for (const child of kept.children) {
resize.observe(child);
if (scrolls(child)) {
for (const inner of child.children) resize.observe(inner);
}
}
mutation.observe(kept, { childList: true, subtree: true });
}
measure();
}
@@ -75,17 +81,26 @@ export function useRoomBeside(
return room;
}
/** Whether `el` scrolls its content sideways, as the row of tabs does. */
function scrolls(el: Element): boolean {
const overflow = getComputedStyle(el).overflowX;
return overflow === "auto" || overflow === "scroll";
}
/**
* Returns how wide the content of `el` runs, from its first child to its last,
* plus its padding. Its box won't do, since a flex row may squeeze it (the tabs
* then scroll) or stretch it, and neither will its scrollWidth, which is at
* least the box.
* least the box. A last child that scrolls counts the same way, at its own
* content's width: it is the one squeezed when a scope switcher sits before
* the tabs, outside the row that scrolls them.
*/
function contentWidth(el: HTMLElement): number {
function contentWidth(el: Element): number {
const first = el.firstElementChild, last = el.lastElementChild;
if (!first || !last) return 0;
const cs = getComputedStyle(el);
return last.getBoundingClientRect().right -
const end = last.getBoundingClientRect();
return (scrolls(last) ? end.left + contentWidth(last) : end.right) -
first.getBoundingClientRect().left + parseFloat(cs.paddingLeft) +
parseFloat(cs.paddingRight);
}

View File

@@ -190,7 +190,7 @@ export function Index(
<div className="header-center-slot">
<PresenceRow
fitWithin=".app-header-center"
fitBeside=".feed-sort"
fitBeside=".feed-sort-scroller"
/>
<FeedTabBar
categorySlug={categorySlug}
@@ -209,7 +209,10 @@ export function Index(
tab={tab}
followView={followView}
/>
<PresenceRow fitWithin=".index-below-header" fitBeside=".feed-sort" />
<PresenceRow
fitWithin=".index-below-header"
fitBeside=".feed-sort-scroller"
/>
</div>
{(tab === "hot" || tab === "new") && (

View File

@@ -1,33 +1,66 @@
/* ══════════════════════════════════════════════════════════════════════
Brutalist theme — pastel palette, hard edges, bold type
Brutalist theme — a pastel palette drawn the hard way: every box is ruled
in ink and casts a solid offset shadow of that ink, type is big and tight,
and the palette's colours come as flat blocks (a teal tag, an apricot
pull-quote, an inked label) rather than as tints.
Palette: Eggshell · Burnt Peach · Twilight Indigo · Muted Teal · Apricot Cream
Light scheme → indigo ink on eggshell.
Dark scheme → the same drawing on Twilight Indigo paper: the lines and
the shadows stay dark, a deeper ink than the page, and only
the type turns to eggshell. Reversed into eggshell lines,
every frame on a page glared.
══════════════════════════════════════════════════════════════════════ */
/* ── Palette (light-dark: see tokens.css) ─────────────────────────── */
:root[data-style="brutalist"] {
--color-text: light-dark(#3d405b, #f4f1de);
--color-text-secondary: light-dark(#5a5e80, #c8c5b2);
--color-text-muted: light-dark(#7a7e9a, #8e8c80);
--color-on-accent: light-dark(#f4f1de, #3d405b);
--color-bg: light-dark(#f4f1de, #252838);
--color-surface: light-dark(#f8f5e8, #2e3145);
--color-border: light-dark(#8a8da8, #4a4e6a);
--color-border-subtle: light-dark(rgba(61, 64, 91, 0.25), rgba(74, 78, 106, 0.4));
--color-text-muted: light-dark(#63678a, #b4b1a2);
/* Dark ink on the accent in both schemes: eggshell on Burnt Peach had too
little contrast to read a button by. */
--color-on-accent: light-dark(#1f2136, #252838);
/* The dark page is a shade under Twilight Indigo, which its cards are. */
--color-bg: light-dark(#f4f1de, #33364e);
--color-surface: light-dark(#f8f5e8, #3d405b);
/* The ink every frame and every hard shadow is drawn in. It is darker than
the page in both schemes: on eggshell it is the text's own indigo, on
indigo it is the night under it. What is written on a block of it is
eggshell in either. */
--brut-ink: light-dark(#3d405b, #171829);
--brut-on-ink: #f4f1de;
/* The page's type, under a name of its own: a colored block redefines
--color-text for what is written on it, and a button of the page's
surface set on that block is still written in the page's type. */
--brut-type: var(--color-text);
--color-border: var(--brut-ink);
--color-border-subtle: light-dark(rgba(61, 64, 91, 0.25), rgba(23, 24, 41, 0.45));
--color-accent: light-dark(#e07a5f, #f2cc8f);
--color-accent-hover: light-dark(#c05040, #d4a860);
--color-link: light-dark(#b85a3a, #f2cc8f);
--color-link: light-dark(#a3482a, #f2cc8f);
--color-link-hover: light-dark(#943020, #d4a860);
--color-danger: light-dark(#c04030, #e07a5f);
--color-danger-bg: light-dark(#fae4e0, #3a1e18);
--color-danger-bg: light-dark(#fae4e0, #4f2c2c);
--color-danger-hover: light-dark(#a02820, #c85a40);
--color-success: light-dark(#3a7860, #81b29a);
--color-success-bg: light-dark(#e0f0ea, #1a2e26);
--color-success-bg: light-dark(#e0f0ea, #2b4a45);
--color-success-hover: light-dark(#286048, #5a9878);
--color-overlay: light-dark(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.6));
--color-header-user-bg: light-dark(rgba(224, 122, 95, 0.08), rgba(242, 204, 143, 0.07));
--color-header-user-bg-hover: light-dark(rgba(224, 122, 95, 0.16), rgba(242, 204, 143, 0.14));
--color-option-bg: light-dark(#ede9d4, #222436);
--color-option-border: light-dark(#e0dcc8, #252838);
--color-option-bg: light-dark(#ede9d4, #2d3046);
--color-option-border: light-dark(#e0dcc8, #33364e);
/* The palette's other two colours, used as flat blocks with dark type on
them in either scheme. */
--brut-teal: #81b29a;
--brut-apricot: #f2cc8f;
--brut-on-block: #1f2136;
/* The offset of a box's shadow, and of the one it throws when pointed at. */
--brut-drop: 4px 4px 0;
--brut-drop-lifted: 6px 6px 0;
--card-border: var(--brut-ink);
--card-hover-border: var(--brut-ink);
}
/* ══════════════════════════════════════════════════════════════════════
@@ -53,10 +86,42 @@
font-weight: 500;
}
/* Only section-level headings get the uppercase treatment */
/* Headlines are big and set tight, at the heaviest weight Space Grotesk
has (700). Only section labels are capitals. */
[data-style="brutalist"] h1 {
font-weight: 900;
letter-spacing: 0.03em;
font-weight: 700;
letter-spacing: -0.025em;
}
[data-style="brutalist"] .dump-title {
font-size: clamp(1.9rem, 1.2rem + 2.6vw, 2.75rem);
line-height: 1.05;
}
[data-style="brutalist"] .page-title {
font-size: 2.5rem;
line-height: 1.05;
}
[data-style="brutalist"] .dump-card-title,
[data-style="brutalist"] .playlist-card-title {
font-size: 1.1rem;
font-weight: 700;
letter-spacing: -0.01em;
line-height: 1.25;
}
[data-style="brutalist"] .journal-card-title {
font-weight: 700;
letter-spacing: -0.01em;
}
/* The unread mark is a square, like everything else. */
[data-style="brutalist"] .unread-dot {
width: 9px;
height: 9px;
border-radius: 0;
top: -0.05em;
}
/* Circles App.css draws with a literal 50% (not the radius scale) — square
@@ -95,7 +160,6 @@
[data-style="brutalist"] .modal-close-btn,
[data-style="brutalist"] .playlist-remove-btn,
[data-style="brutalist"] .playlist-card-delete-btn,
[data-style="brutalist"] .vote-btn,
[data-style="brutalist"] .emoji-picker-float [frimousse-emoji],
[data-style="brutalist"] .emoji-picker-close-btn,
[data-style="brutalist"] .text-editor-emoji-btn,
@@ -128,7 +192,6 @@
[data-style="brutalist"] .modal-close-btn:hover,
[data-style="brutalist"] .playlist-remove-btn:hover,
[data-style="brutalist"] .playlist-card-delete-btn:hover,
[data-style="brutalist"] .vote-btn:hover:not([aria-disabled="true"]),
[data-style="brutalist"] .emoji-picker-float [frimousse-emoji]:hover,
[data-style="brutalist"] .emoji-picker-close-btn:hover,
[data-style="brutalist"] .text-editor-emoji-btn:hover:not(:disabled),
@@ -159,26 +222,139 @@
}
/* ── Cards ───────────────────────────────────────────────────────── */
/* A card pointed at lifts off the page, up and left, and the shadow it
leaves behind is the accent's: its frame stays ink. */
[data-style="brutalist"] .dump-card,
[data-style="brutalist"] .playlist-card {
border: 2px solid var(--color-border);
box-shadow: 3px 3px 0 var(--color-border);
[data-style="brutalist"] .playlist-card,
[data-style="brutalist"] .journal-card {
border: 2px solid var(--brut-ink);
box-shadow: var(--brut-drop) var(--brut-ink);
transition:
box-shadow 0.08s ease,
transform 0.08s ease,
border-color 0.1s,
grid-template-rows 0.32s ease,
opacity 0.25s ease,
filter 0.3s ease;
}
[data-style="brutalist"] .dump-card:hover,
[data-style="brutalist"] .playlist-card:hover {
border-color: var(--color-accent);
box-shadow: 3px 3px 0 var(--color-accent);
[data-style="brutalist"] .playlist-card:hover,
[data-style="brutalist"] .journal-card:hover {
border-color: var(--brut-ink);
box-shadow: var(--brut-drop-lifted) var(--color-accent);
transform: translate(-2px, -2px);
}
/* A thumbnail is framed in ink too, where the base draws a hairline matte. */
[data-style="brutalist"] .dump-card-preview,
[data-style="brutalist"] .playlist-card-preview {
border: 2px solid var(--brut-ink);
box-shadow: none;
}
[data-style="brutalist"] .dump-card-inner:hover .dump-card-preview,
[data-style="brutalist"] .playlist-card-inner:hover .playlist-card-preview {
transform: none;
box-shadow: none;
}
/* The related dumps under a dump keep the frame at full weight, smaller. */
[data-style="brutalist"] .related-section .dump-card {
border-width: 2px;
box-shadow: 3px 3px 0 var(--brut-ink);
opacity: 1;
}
[data-style="brutalist"] .related-section .dump-card-title {
font-size: var(--text-base);
}
/* ── Journal — the mosaic's tiles are boxes too ───────────────────── */
/* The gap grows to hold each tile's shadow clear of its neighbour. */
[data-style="brutalist"] .journal-grid {
gap: 1rem;
}
/* A quote is set on a block of Apricot Cream, a tile with neither picture
nor note on one of Muted Teal, both in dark type whatever the scheme. */
[data-style="brutalist"] .journal-card--quote {
background: var(--brut-apricot);
--color-text: var(--brut-on-block);
--color-text-secondary: var(--brut-on-block);
--color-link: var(--brut-on-block);
--color-link-hover: var(--brut-on-block);
color: var(--brut-on-block);
}
[data-style="brutalist"] .journal-card--text {
background: var(--brut-teal);
--color-text: var(--brut-on-block);
--color-text-secondary: var(--brut-on-block);
color: var(--brut-on-block);
}
[data-style="brutalist"] :is(.journal-card--quote, .journal-card--text) .journal-card-title:hover {
color: var(--brut-on-block);
text-decoration: underline;
text-decoration-thickness: 2px;
}
[data-style="brutalist"] .journal-card-quotemark {
color: var(--brut-on-block);
opacity: 0.25;
}
/* A link in the quoted note would take the accent, which in the dark scheme
is the block's own apricot. */
[data-style="brutalist"] .journal-card-quote-body a {
color: inherit;
}
[data-style="brutalist"] :is(.journal-card--quote, .journal-card--text) .vote-btn:not(.vote-btn--active) {
background: var(--color-surface);
color: var(--brut-type);
}
/* ── Role badges — an admin's is an inked label, like a section's; a
moderator's a block of Muted Teal. The base sets them on the accent
and on the success green in the accent's text colour, which here is
dark type on Burnt Peach and dark on dark. ─────────────────────── */
[data-style="brutalist"] .profile-role-chip {
border: 2px solid var(--brut-ink);
background: var(--brut-ink);
color: var(--brut-on-ink);
}
[data-style="brutalist"] .profile-role-chip--moderator {
background: var(--brut-teal);
color: var(--brut-on-block);
}
/* Follow is written in ink: the accent as a text colour is too pale to read
on eggshell. Followed, or pointed at, it fills with the accent. */
[data-style="brutalist"] .follow-btn {
color: var(--color-text);
}
[data-style="brutalist"] .follow-btn:hover:not(:disabled),
[data-style="brutalist"] .follow-btn--following {
background: var(--color-accent);
color: var(--color-on-accent);
}
/* ── Categories — a tag is a teal block, ruled in ink ─────────────── */
[data-style="brutalist"] .category-link {
border: 2px solid var(--brut-ink);
background: var(--brut-teal);
color: var(--brut-on-block);
font-weight: 700;
}
[data-style="brutalist"] .category-link:hover {
background: var(--color-accent);
color: var(--color-on-accent);
}
/* Don't shift cards that are animating out */
[data-style="brutalist"] .dump-card--fading,
[data-style="brutalist"] .dump-card--dismissing {
@@ -206,15 +382,54 @@
--journal-title-hover-shadow: 3px 3px 0 var(--color-text);
}
/* ── Dump post detail header ─────────────────────────────────────── */
[data-style="brutalist"] .dump-post-header {
border: 2px solid var(--color-border);
box-shadow: 3px 3px 0 var(--color-border);
/* ── A dump's page — every panel is the same box ──────────────────── */
[data-style="brutalist"] .dump-post-header,
[data-style="brutalist"] .dump-rich-content,
[data-style="brutalist"] .playlist-detail-header:not(.playlist-detail-header--editing),
[data-style="brutalist"] .form-page-header,
[data-style="brutalist"] .form-panel,
[data-style="brutalist"] .comment-top-form,
[data-style="brutalist"] .auth-card {
border: 2px solid var(--brut-ink);
box-shadow: var(--brut-drop) var(--brut-ink);
}
/* ── App header ──────────────────────────────────────────────────── */
/* Comments nest, and a shadow on each would pile up: the frame alone. */
[data-style="brutalist"] .comment-node-inner {
border: 2px solid var(--brut-ink);
}
[data-style="brutalist"] .dump-comment {
border-left-width: 6px;
opacity: 1;
}
/* ── App header — a heavy rule under it, the site's name in capitals ── */
[data-style="brutalist"] .app-header {
border-bottom: 2px solid var(--color-border);
border-bottom: 3px solid var(--brut-ink);
}
/* Pointed at, the name is stamped like a headline over a photo in the
journal (.journal-card--image .journal-card-title above): a block of the
accent with a hard shadow, here in the ink the header's buttons cast
theirs in. The padding is always there, cancelled by an equal negative
margin, so the block grows around the name without moving it. */
[data-style="brutalist"] .app-header-brand {
text-transform: uppercase;
letter-spacing: 0.06em;
white-space: nowrap;
padding: 0.12em 0.35em;
margin: -0.12em -0.35em;
transition:
color var(--dur-base),
background-color var(--dur-base),
box-shadow var(--dur-base);
}
[data-style="brutalist"] .app-header-brand:hover {
background-color: var(--color-accent);
color: var(--color-on-accent);
box-shadow: 3px 3px 0 var(--brut-ink);
}
[data-style="brutalist"] .app-header-user {
@@ -250,7 +465,7 @@
[data-style="brutalist"] .modal-card,
[data-style="brutalist"] .confirm-modal {
border: 2px solid var(--color-border);
box-shadow: 4px 4px 0 var(--color-border);
box-shadow: 8px 8px 0 var(--color-border);
}
[data-style="brutalist"] .modal-header {
@@ -271,8 +486,40 @@
border-bottom-width: 2px;
}
/* The comments and the related dumps open on the theme's rule, and their
label hangs from it as an inked tab. */
[data-style="brutalist"] .comment-section,
[data-style="brutalist"] .related-section {
padding-top: 0;
}
[data-style="brutalist"] .comment-section-title,
[data-style="brutalist"] .related-section .related-section-title {
width: fit-content;
margin-bottom: 1.25rem;
padding: 0.3rem 0.7rem;
background: var(--brut-ink);
color: var(--brut-on-ink);
}
/* A page group's label is ink, and so is the rule that runs on from it. */
[data-style="brutalist"] .page-group-label {
color: var(--color-text);
}
[data-style="brutalist"] .page-group-label::after {
height: 2px;
background: var(--brut-ink);
}
[data-style="brutalist"] .page-card,
[data-style="brutalist"] .notification-item {
border-color: var(--brut-ink);
}
/* ── Rich content cards ──────────────────────────────────────────── */
[data-style="brutalist"] .rich-content-card {
border-width: 2px;
box-shadow: 3px 3px 0 var(--color-border);
}
@@ -389,3 +636,17 @@
transform: none !important;
box-shadow: none !important;
}
/* ── Vote — a boxed button like the rest, filled once cast ───────── */
[data-style="brutalist"] .vote-btn--active,
[data-style="brutalist"] .vote-btn--active:hover:not([aria-disabled="true"]) {
border-color: var(--brut-ink);
}
/* Over a photo it keeps the theme's solid face rather than the shared
white-on-glass. */
[data-style="brutalist"] .journal-card--image .journal-card-overlay .vote-btn:not(.vote-btn--active) {
background: var(--color-surface);
color: var(--color-text);
border-color: var(--brut-ink);
}

View File

@@ -1,7 +1,10 @@
/* ══════════════════════════════════════════════════════════════════════
GeoCities / NeoCities theme — turn-of-the-millennium personal-homepage
maximalism: tiled starfield wallpaper, Comic Sans, ridged 3D boxes,
clashing neon accents, rainbow dividers, blinking badges.
GeoCities / NeoCities theme — a turn-of-the-millennium personal homepage
as it looked on the desktop of its day: tiled wallpaper, Comic Sans,
ridged table boxes, rainbow rules, WordArt headings, a marquee, and the
grey bevelled controls every form button was then, whatever the page
around it. The header is the taskbar, the site's name its Start button,
and a menu or a dialog is drawn as that desktop drew one.
Dark scheme → NeoCities "neon on black" revival.
Light scheme → classic GeoCities pastel-on-wallpaper.
@@ -27,8 +30,11 @@
--color-border-subtle: light-dark(rgba(208, 0, 208, 0.25), rgba(0, 255, 255, 0.28));
--color-accent: light-dark(#d000d0, #ff00ff);
--color-accent-hover: light-dark(#a000a0, #ff5cff);
/* The three link colours of an unstyled page: unvisited, visited (below)
and the red of one being pressed, which hovering shows here. */
--color-link: light-dark(#0000ee, #00ffff);
--color-link-hover: light-dark(#551a8b, #ffff00);
--color-link-hover: light-dark(#ee0000, #ffff00);
--geo-visited: light-dark(#551a8b, #e39bff);
--color-danger: light-dark(#cc0033, #ff3860);
--color-danger-bg: light-dark(#ffe0e8, #3a0a1a);
--color-danger-hover: light-dark(#990022, #ff6b8a);
@@ -40,6 +46,25 @@
--color-header-user-bg-hover: light-dark(rgba(208, 0, 208, 0.16), rgba(0, 255, 255, 0.2));
--color-option-bg: light-dark(#fff0fb, #14143a);
--color-option-border: light-dark(#d000d0, #00ffff);
/* A control's grey face and the four tones of its bevel. They are the
same in both schemes, as a form button was on a page of any colour. */
--geo-face: #c0c0c0;
--geo-face-ink: #000000;
--geo-face-light: #ffffff;
--geo-face-soft: #dfdfdf;
--geo-face-shade: #808080;
--geo-face-dark: #0a0a0a;
--geo-raised: inset -1px -1px 0 var(--geo-face-shade),
inset 1px 1px 0 var(--geo-face-soft);
--geo-sunken: inset 1px 1px 0 var(--geo-face-shade);
/* A pressed toggle's face: the two-pixel checker a 16-colour display
dithered between white and grey. */
--geo-dither: repeating-conic-gradient(#ffffff 0% 25%, #c0c0c0 0% 50%) 0 0 /
2px 2px;
--geo-titlebar: linear-gradient(90deg, #000080, #1084d0);
--geo-select: #000080;
/* The taskbar is grey too. */
--header-bg: var(--geo-face);
--geo-rainbow: linear-gradient(
90deg,
#ff004d,
@@ -55,6 +80,8 @@
radial-gradient(1.5px 1.5px at 170px 40px, #ff00ff, transparent),
radial-gradient(1px 1px at 50px 190px, #ffee00, transparent);
--geo-pattern-size: 200px 200px;
/* A heading's WordArt fill, kept to hues that read on the page. */
--geo-wordart: linear-gradient(90deg, #ff5cff, #ffee00, #39ff14, #00ffff);
}
/* Non-colour values can't go through light-dark(), so the light variants
@@ -70,9 +97,9 @@
#0077dd,
#8800cc
);
--geo-pattern: radial-gradient(rgba(208, 0, 208, 0.16) 1.5px, transparent 1.5px),
radial-gradient(rgba(0, 179, 255, 0.14) 1.5px, transparent 1.5px);
--geo-pattern-size: 22px 22px, 22px 22px;
--geo-pattern: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cpath fill='%23ffa3e2' d='M20 8l3 9 9 3-9 3-3 9-3-9-9-3 9-3z'/%3E%3Cpath fill='%2399dcff' d='M84 58l2.4 7.6 7.6 2.4-7.6 2.4L84 78l-2.4-7.6-7.6-2.4 7.6-2.4z'/%3E%3Cpath fill='%23d2b6ff' d='M38 82l1.8 5.2 5.2 1.8-5.2 1.8L38 96l-1.8-5.2-5.2-1.8 5.2-1.8z'/%3E%3Ccircle fill='%23ffd866' cx='62' cy='26' r='2.6'/%3E%3Ccircle fill='%23ffa3e2' cx='104' cy='106' r='2.2'/%3E%3Ccircle fill='%2399dcff' cx='8' cy='62' r='1.8'/%3E%3C/svg%3E");
--geo-pattern-size: 120px 120px;
--geo-wordart: linear-gradient(90deg, #e6003f, #d000d0, #0057e0, #008a3a);
}
}
@@ -86,9 +113,9 @@
#0077dd,
#8800cc
);
--geo-pattern: radial-gradient(rgba(208, 0, 208, 0.16) 1.5px, transparent 1.5px),
radial-gradient(rgba(0, 179, 255, 0.14) 1.5px, transparent 1.5px);
--geo-pattern-size: 22px 22px, 22px 22px;
--geo-pattern: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cpath fill='%23ffa3e2' d='M20 8l3 9 9 3-9 3-3 9-3-9-9-3 9-3z'/%3E%3Cpath fill='%2399dcff' d='M84 58l2.4 7.6 7.6 2.4-7.6 2.4L84 78l-2.4-7.6-7.6-2.4 7.6-2.4z'/%3E%3Cpath fill='%23d2b6ff' d='M38 82l1.8 5.2 5.2 1.8-5.2 1.8L38 96l-1.8-5.2-5.2-1.8 5.2-1.8z'/%3E%3Ccircle fill='%23ffd866' cx='62' cy='26' r='2.6'/%3E%3Ccircle fill='%23ffa3e2' cx='104' cy='106' r='2.2'/%3E%3Ccircle fill='%2399dcff' cx='8' cy='62' r='1.8'/%3E%3C/svg%3E");
--geo-pattern-size: 120px 120px;
--geo-wordart: linear-gradient(90deg, #e6003f, #d000d0, #0057e0, #008a3a);
}
/* ══════════════════════════════════════════════════════════════════════
@@ -96,8 +123,10 @@
══════════════════════════════════════════════════════════════════════ */
/* ── Typography — the one true homepage font ──────────────────────── */
/* Comic Sans where the system has it (Windows, macOS), Comic Neue, which
fonts.css bundles, everywhere else. */
[data-style="geocities"] {
--font-body: "Comic Sans MS", "Comic Sans", "Chalkboard SE", "Comic Neue",
--font-body: "Comic Sans MS", "Comic Sans", "Comic Neue", "Chalkboard SE",
"Marker Felt", cursive;
font-family: var(--font-body);
font-weight: 400;
@@ -114,24 +143,62 @@
background-attachment: fixed;
}
/* ── Loud headings with a soft glow ──────────────────────────────── */
[data-style="geocities"] h1,
[data-style="geocities"] .app-header-brand {
/* ── Headings — WordArt: a gradient run through the letters ───────── */
/* The fill replaces the text's colour, which an emoji is painted with too:
a title's icon takes its own colour back, or it would vanish. */
[data-style="geocities"] h1 {
width: fit-content;
max-width: 100%;
font-weight: 700;
letter-spacing: 0.02em;
text-shadow: 0 0 6px color-mix(in srgb, var(--color-accent) 60%, transparent);
background: var(--geo-wordart);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(
2px 2px 0 color-mix(in srgb, var(--color-text) 22%, transparent)
);
}
/* ── Links — always underlined, bold ─────────────────────────────── */
[data-style="geocities"] h1 :is(.page-title-icon, .emoji, img) {
color: var(--color-text);
}
/* ── Links — underlined and bold, purple once followed ────────────── */
[data-style="geocities"] a,
[data-style="geocities"] .md a {
text-decoration: underline;
font-weight: 700;
}
/* …except navigation chrome and avatar links, which aren't prose. */
[data-style="geocities"] .md a:visited {
color: var(--geo-visited);
}
/* A card's title is the link to its page, so it is drawn as one: which dumps
were already opened shows in the list, as it did on a page of links. Over a
photo the title stays white on its scrim. */
[data-style="geocities"] :is(.dump-card-title, .playlist-card-title),
[data-style="geocities"] .journal-card:not(.journal-card--image) .journal-card-title {
color: var(--color-link);
}
[data-style="geocities"] :is(.dump-card-title, .playlist-card-title):visited,
[data-style="geocities"] .journal-card:not(.journal-card--image) .journal-card-title:visited {
color: var(--geo-visited);
}
[data-style="geocities"] .dump-card-inner:hover .dump-card-title,
[data-style="geocities"] .playlist-card-inner:hover .playlist-card-title,
[data-style="geocities"] .journal-card:not(.journal-card--image) .journal-card-title:hover {
color: var(--color-link-hover);
}
/* …except navigation chrome, links drawn as buttons and avatar links, which
aren't prose. */
[data-style="geocities"] a:has(.avatar-img),
[data-style="geocities"] a:has(.avatar-initials),
[data-style="geocities"] a:is(.btn-primary, .btn-secondary, .btn-danger, .btn-border),
[data-style="geocities"] .index-presence-avatar,
[data-style="geocities"] .app-header-brand,
[data-style="geocities"] .app-header-user,
@@ -140,30 +207,42 @@
text-decoration: none;
}
/* ── Blink + marquee-ish keyframes ───────────────────────────────── */
/* ── Blink, and the marquee's crawl ──────────────────────────────── */
@keyframes geo-blink {
50% {
opacity: 0;
}
}
/* ── Buttons — chunky Win95 outset bevel, press = inset ──────────── */
/* `.dump-fab` is exempt from the generic button chrome — it keeps its own
shape, glow and animations — but this squared-off retro theme squares it. */
/* The track is the banner's text twice over, so sliding it left by half its
width ends where it began and the loop has no seam. */
@keyframes geo-marquee {
to {
translate: -50% 0;
}
}
/* ── Controls — the grey bevelled button of the day ──────────────── */
/* `.dump-fab` and `.chat-fab` are exempt from the button chrome — they keep
their own shape, glow and animations — but this squared-off theme squares
them. */
[data-style="geocities"] .dump-fab,
[data-style="geocities"] .chat-fab {
--fab-radius: 0;
}
[data-style="geocities"] button:not(.dump-fab, .chat-fab) {
border: 3px outset var(--color-border);
box-shadow: none;
/* A button with no class of its own (the guest's "Log in") is one of these
too. It weighs nothing, so a class that draws a button differently keeps
its own background and border. */
:where([data-style="geocities"]) button:where(:not(.dump-fab, .chat-fab)) {
background: var(--geo-face);
color: var(--geo-face-ink);
border: 2px solid;
border-color: var(--geo-face-light) var(--geo-face-dark) var(--geo-face-dark)
var(--geo-face-light);
box-shadow: var(--geo-raised);
font-weight: 700;
transition: border-color 0.1s, background 0.12s;
}
[data-style="geocities"] button:active:not(:disabled) {
border-style: inset;
transition: none;
}
[data-style="geocities"] .btn-primary,
@@ -176,17 +255,147 @@
[data-style="geocities"] .invite-btn,
[data-style="geocities"] .btn-add-playlist,
[data-style="geocities"] .load-more-btn,
[data-style="geocities"] .search-bar-btn {
border: 3px outset var(--color-border);
box-shadow: none;
[data-style="geocities"] .search-bar-btn,
[data-style="geocities"] .new-item-toggle,
[data-style="geocities"] .category-chip,
[data-style="geocities"] .app-header-user,
[data-style="geocities"] .nav-search-btn,
[data-style="geocities"] .chat-button,
[data-style="geocities"] .notification-bell,
[data-style="geocities"] .modal-close-btn {
background: var(--geo-face);
color: var(--geo-face-ink);
border: 2px solid;
border-color: var(--geo-face-light) var(--geo-face-dark) var(--geo-face-dark)
var(--geo-face-light);
border-radius: 0;
box-shadow: var(--geo-raised);
font-weight: 700;
opacity: 1;
transform: none;
transition: none;
}
/* The shared .vote-btn declares `background: transparent`, which leaves the
chunky outset bevel see-through against the card. Give the resting state a
solid raised-panel fill; the voted (--active) state keeps its accent
background, so scope this to non-active buttons. */
[data-style="geocities"] .vote-btn:not(.vote-btn--active) {
background: var(--color-surface);
/* A desktop of that day had no hover: the label turning link-blue is the one
concession to a pointer. */
[data-style="geocities"] .btn-secondary:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .btn-danger:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .btn-border:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .follow-btn:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .vote-btn:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .feed-sort-btn:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .invite-btn:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .btn-add-playlist:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .load-more-btn:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .search-bar-btn:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .new-item-toggle:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .category-chip:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .app-header-user:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .nav-search-btn:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .chat-button:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .notification-bell:hover:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .modal-close-btn:hover:not(:disabled, [aria-disabled="true"]) {
background: var(--geo-face);
color: #0000a8;
border-color: var(--geo-face-light) var(--geo-face-dark) var(--geo-face-dark)
var(--geo-face-light);
transform: none;
}
[data-style="geocities"] .btn-danger:hover:not(:disabled) {
color: #a80000;
}
/* Pressed: the bevel turns inside out and the label moves down and right a
pixel with the face. */
[data-style="geocities"] button:not(.dump-fab, .chat-fab, .btn--ghost, .user-menu-trigger, .user-menu-item):active:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .btn-primary:active:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .btn-secondary:active:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .btn-border:active:not(:disabled, [aria-disabled="true"]),
[data-style="geocities"] .app-header-user:active:not(:disabled, [aria-disabled="true"]) {
border-color: var(--geo-face-dark) var(--geo-face-light) var(--geo-face-light)
var(--geo-face-dark);
box-shadow: var(--geo-sunken);
transform: translate(1px, 1px);
}
/* A toggle that is on stays pressed, on the dithered face: the open tab, a
vote cast, a user followed, the chosen option of a segmented control. */
[data-style="geocities"] .feed-sort-btn.active,
[data-style="geocities"] .vote-btn--active,
[data-style="geocities"] .follow-btn--following,
[data-style="geocities"] .visibility-toggle button.active,
[data-style="geocities"] .category-chip-toggle--active,
[data-style="geocities"] .nav-search-btn[aria-expanded="true"] {
background: var(--geo-dither);
color: var(--geo-face-ink);
border: 2px solid;
border-color: var(--geo-face-dark) var(--geo-face-light) var(--geo-face-light)
var(--geo-face-dark);
box-shadow: var(--geo-sunken);
}
[data-style="geocities"] :is(.feed-sort-btn.active, .vote-btn--active, .follow-btn--following):hover:not(:disabled, [aria-disabled="true"]) {
background: var(--geo-dither);
border-color: var(--geo-face-dark) var(--geo-face-light) var(--geo-face-light)
var(--geo-face-dark);
}
/* Greyed out: the label is engraved, a shade over its own white shadow. */
[data-style="geocities"] :is(button, .btn-primary, .btn-secondary, .vote-btn):is(:disabled, [aria-disabled="true"]):not(.btn--ghost, .dump-fab, .chat-fab) {
color: var(--geo-face-shade);
text-shadow: 1px 1px 0 var(--geo-face-light);
opacity: 1;
}
/* The one loud button: the same bevel cut from the accent, its lit and
shaded edges mixed from it. */
[data-style="geocities"] .btn-primary,
[data-style="geocities"] .btn-primary:hover:not(:disabled) {
background: var(--color-accent);
color: var(--color-on-accent);
border-color: color-mix(in srgb, var(--color-accent) 40%, #ffffff)
color-mix(in srgb, var(--color-accent) 35%, #000000)
color-mix(in srgb, var(--color-accent) 35%, #000000)
color-mix(in srgb, var(--color-accent) 40%, #ffffff);
box-shadow: inset -1px -1px 0 color-mix(in srgb, var(--color-accent) 65%,
#000000),
inset 1px 1px 0 color-mix(in srgb, var(--color-accent) 70%, #ffffff);
text-shadow: none;
}
[data-style="geocities"] .btn-primary:hover:not(:disabled) {
background: var(--color-accent-hover);
}
[data-style="geocities"] .btn-primary:disabled {
background: var(--geo-face);
border-color: var(--geo-face-light) var(--geo-face-dark) var(--geo-face-dark)
var(--geo-face-light);
box-shadow: var(--geo-raised);
}
/* The header's dump button presses like the rest, so it neither lifts nor
rocks. */
[data-style="geocities"] .app-header-nav .btn-primary:hover:not(:disabled) {
translate: none;
animation: none;
}
/* Over a photo the shared rule turns a vote into white on glass; here it is
a grey button wherever it sits. */
[data-style="geocities"] .journal-card--image .journal-card-overlay .vote-btn:not(.vote-btn--active),
[data-style="geocities"] .journal-card--image .journal-card-overlay .vote-btn:not(.vote-btn--active):hover:not([aria-disabled="true"]) {
background: var(--geo-face);
color: var(--geo-face-ink);
border-color: var(--geo-face-light) var(--geo-face-dark) var(--geo-face-dark)
var(--geo-face-light);
}
/* Keyboard focus is the dotted rectangle inside the face. */
[data-style="geocities"] :is(button, .btn-primary, .btn-secondary, .btn-border, .app-header-user):focus-visible {
outline: 1px dotted var(--geo-face-ink);
outline-offset: -5px;
}
/* Turn the tint up and give it hard diagonal bands — a flat wash reads as an
@@ -204,7 +413,6 @@
}
/* Ghost / icon-only buttons stay flat */
[data-style="geocities"] .modal-close-btn,
[data-style="geocities"] .playlist-remove-btn,
[data-style="geocities"] .playlist-card-delete-btn,
[data-style="geocities"] .emoji-picker-float [frimousse-emoji],
@@ -212,7 +420,6 @@
[data-style="geocities"] .text-editor-emoji-btn,
[data-style="geocities"] .emoji-picker-float [frimousse-skin-tone-selector],
[data-style="geocities"] .btn--ghost,
[data-style="geocities"] .new-item-toggle,
[data-style="geocities"] .audio-player-btn,
[data-style="geocities"] .audio-player-vol-btn,
[data-style="geocities"] .rich-content-thumbnail-btn,
@@ -225,22 +432,33 @@
box-shadow: none !important;
}
/* ── Segmented controls ──────────────────────────────────────────── */
/* ── Segmented controls — a sunken well of flat options ───────────── */
[data-style="geocities"] .visibility-toggle {
border: 3px ridge var(--color-border);
background: var(--geo-face);
border: 2px solid;
border-color: var(--geo-face-shade) var(--geo-face-light)
var(--geo-face-light)
var(--geo-face-shade);
padding: 2px;
}
[data-style="geocities"] .visibility-toggle button {
/* Zero the width (not just the style): the generic `button:active` rule flips
border-style to `inset`, and a `none` border keeps its `medium` width — so
on press a 3px border would reappear inside the toggle's own frame. */
border: 0;
background: var(--geo-face);
color: var(--geo-face-ink);
border: 2px solid transparent;
box-shadow: none;
}
[data-style="geocities"] .visibility-toggle button:not(.active):hover {
background: var(--geo-face);
color: #0000a8;
}
/* ── Cards & panels — ridged "table border" with a soft neon halo ─── */
[data-style="geocities"] .dump-card,
[data-style="geocities"] .playlist-card,
[data-style="geocities"] .dump-post-header,
[data-style="geocities"] .dump-rich-content,
[data-style="geocities"] .auth-card,
[data-style="geocities"] .notification-item,
[data-style="geocities"] .page-card,
@@ -270,6 +488,23 @@
box-shadow: 0 0 16px color-mix(in srgb, var(--color-accent) 75%, transparent);
}
/* An unread dump wears the "new!" burst a page pinned on its latest link: a
twelve-pointed star, blinking. */
[data-style="geocities"] .unread-dot {
width: 13px;
height: 13px;
top: 0.05em;
border-radius: 0;
background: light-dark(#ff004d, #ffee00);
clip-path: polygon(
50% 0%, 61% 14%, 79% 7%, 80% 26%, 98% 31%, 87% 46%,
100% 61%, 82% 67%, 82% 86%, 64% 82%, 50% 100%, 36% 82%,
18% 86%, 18% 67%, 0% 61%, 13% 46%, 2% 31%, 20% 26%,
21% 7%, 39% 14%
);
animation: geo-blink 1.3s step-start infinite;
}
/* A pointed-at headline over a photo lights up like a marquee: neon yellow
on a black plate, ringed and glowing in the accent. */
[data-style="geocities"] .journal-card--image .journal-card-title {
@@ -294,15 +529,102 @@
letter-spacing: 0.15em;
}
/* ── App header — rainbow divider ────────────────────────────────── */
/* A list's end is "under construction": hazard tape above and below the
line that says there is no more. */
[data-style="geocities"] .feed-end {
width: fit-content;
max-width: calc(100% - 2rem);
margin: 1.75rem auto 0;
padding: 0.6rem 1.25rem;
border-block: 10px solid;
border-image: repeating-linear-gradient(
-45deg,
#111111 0 10px,
#ffcc00 10px 20px
) 10;
background: var(--color-surface);
color: var(--color-text);
font-weight: 700;
opacity: 1;
}
[data-style="geocities"] .feed-end::before,
[data-style="geocities"] .feed-end::after {
content: "🚧";
margin-inline: 0.6em;
}
/* ── App header — the taskbar ────────────────────────────────────── */
/* Grey in both schemes, lit along its top edge, with the rainbow rule under
it. What it holds is drawn in the controls' ink rather than the page's, so
the text tokens are set again for everything inside it. */
[data-style="geocities"] .app-header,
[data-style="geocities"] .header-search-below {
--color-text: var(--geo-face-ink);
--color-text-secondary: #202020;
--color-text-muted: #404040;
color: var(--geo-face-ink);
box-shadow: inset 0 2px 0 var(--geo-face-light);
border-bottom: 4px solid;
border-image: var(--geo-rainbow) 1;
}
[data-style="geocities"] .app-header-user {
border: 3px outset var(--color-border);
/* The site's name is the Start button. */
[data-style="geocities"] .app-header-brand,
[data-style="geocities"] .app-header-brand:hover {
display: inline-flex;
align-items: center;
gap: 0.35em;
padding: 0.2rem 0.6rem 0.2rem 0.45rem;
background: var(--geo-face);
color: var(--geo-face-ink);
border: 2px solid;
border-color: var(--geo-face-light) var(--geo-face-dark) var(--geo-face-dark)
var(--geo-face-light);
box-shadow: var(--geo-raised);
font-size: var(--text-base);
font-weight: 700;
letter-spacing: 0;
white-space: nowrap;
}
[data-style="geocities"] .app-header-brand:active {
border-color: var(--geo-face-dark) var(--geo-face-light) var(--geo-face-light)
var(--geo-face-dark);
box-shadow: var(--geo-sunken);
}
/* A search field on the taskbar is a white well, typed in black. */
[data-style="geocities"] .app-header .search-bar-input,
[data-style="geocities"] .header-search-below .search-bar-input {
background: #ffffff !important;
color: #000000;
}
/* ── The marquee — a line of welcome crawling under the taskbar ──── */
[data-style="geocities"] .site-marquee {
overflow: hidden;
white-space: nowrap;
background: light-dark(#ffffcc, #000000);
color: light-dark(#cc0033, #39ff14);
border-bottom: 2px solid var(--color-border);
font-weight: 700;
font-size: var(--text-sm);
line-height: 1.9;
}
[data-style="geocities"] .site-marquee-track {
display: inline-block;
animation: geo-marquee 36s linear infinite;
}
[data-style="geocities"] .site-marquee-track > span {
display: inline-block;
padding-inline: 1.5rem;
}
[data-style="geocities"] .site-marquee:hover .site-marquee-track {
animation-play-state: paused;
}
/* ── Rainbow dividers — on each element's real edge ──────────────── */
@@ -325,6 +647,25 @@
letter-spacing: 0.06em;
}
/* A label that sits straight on the wallpaper gets a strip of the page's
plain colour behind it, so no star runs through its letters. */
[data-style="geocities"] .comment-section-title,
[data-style="geocities"] .related-section-title,
[data-style="geocities"] .dump-actions a,
[data-style="geocities"] .empty-state {
width: fit-content;
padding-inline: 0.4em;
background: var(--color-bg);
color: var(--color-text-secondary);
opacity: 1;
}
/* A page group's label keeps its row, which its trailing rule fills. */
[data-style="geocities"] .page-group-label {
background: var(--color-bg);
color: var(--color-text-secondary);
}
/* ── Inputs / textareas — sunken inset field ─────────────────────── */
[data-style="geocities"] input:not([type="range"]):not([type="file"]):not([type="checkbox"]):not([type="radio"]),
[data-style="geocities"] textarea {
@@ -333,19 +674,80 @@
background-color: var(--color-bg);
}
/* ── Modals — heavy ridged frame ─────────────────────────────────── */
/* ── Dialogs — a window: grey frame, blue title bar ──────────────── */
[data-style="geocities"] .modal-card,
[data-style="geocities"] .confirm-modal,
[data-style="geocities"] .user-menu-dropdown,
[data-style="geocities"] .user-list-popover,
[data-style="geocities"] .mention-dropdown,
[data-style="geocities"] .emoji-picker-float {
border: 3px ridge var(--color-border);
[data-style="geocities"] .confirm-modal {
border: 3px outset var(--geo-face);
box-shadow: 0 0 18px color-mix(in srgb, var(--color-border) 45%, transparent);
}
[data-style="geocities"] .modal-header {
border-bottom: 2px solid var(--color-border-subtle);
padding: 0.3rem 0.35rem 0.3rem 0.6rem;
background: var(--geo-titlebar);
border-bottom: none;
}
[data-style="geocities"] .modal-title {
color: #ffffff;
}
[data-style="geocities"] .modal-close-btn {
width: 1.5rem;
height: 1.35rem;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: var(--text-xs);
}
/* ── Menus — a grey sheet, the pointed-at row in the selection's navy ── */
[data-style="geocities"] .user-menu-dropdown,
[data-style="geocities"] .category-switcher-menu,
[data-style="geocities"] .user-list-popover,
[data-style="geocities"] .mention-dropdown {
--color-text: var(--geo-face-ink);
--color-text-secondary: #202020;
--color-text-muted: #404040;
background: var(--geo-face);
color: var(--geo-face-ink);
border: 2px solid;
border-color: var(--geo-face-light) var(--geo-face-dark) var(--geo-face-dark)
var(--geo-face-light);
box-shadow: var(--geo-raised), 3px 3px 0 rgb(0 0 0 / 0.35);
padding: 2px;
gap: 0;
}
[data-style="geocities"] :is(.user-menu-item, .user-list-popover-item, .category-switcher-option) {
background: transparent;
color: var(--geo-face-ink);
border-radius: 0;
transition: none;
}
[data-style="geocities"] .user-menu .user-menu-item:hover,
[data-style="geocities"] .user-list-popover-item:hover,
[data-style="geocities"] .category-switcher-option:hover:not(:disabled),
[data-style="geocities"] .category-switcher-option--active {
background: var(--geo-select);
color: #ffffff;
}
[data-style="geocities"] .user-menu-separator {
border-top: 1px solid var(--geo-face-shade);
border-bottom: 1px solid var(--geo-face-light);
}
[data-style="geocities"] .category-switcher-option {
border: none !important;
box-shadow: none !important;
}
/* The emoji picker keeps the page's colours, in the cards' ridged frame. */
[data-style="geocities"] .emoji-picker-float {
border: 3px ridge var(--color-border);
box-shadow: 0 0 18px color-mix(in srgb, var(--color-border) 45%, transparent);
}
/* ── Avatars — framed snapshots ──────────────────────────────────── */
@@ -373,8 +775,6 @@
box-shadow: 0 0 16px color-mix(in srgb, var(--color-accent) 55%, transparent);
}
/* Queue UI: square off the artwork and the stream progress bar. */
[data-style="geocities"] .global-player .audio-player-btn {
color: var(--color-on-accent);
}
@@ -389,8 +789,11 @@
border-style: dashed;
}
/* ── Pills / badges — squared, and the unread bits blink ─────────── */
[data-style="geocities"] .fdz__upload-icon {
opacity: 0.7;
}
/* ── Categories ──────────────────────────────────────────────────── */
/* The editor is one squared box; flatten its fields (geocities otherwise
forces a sunken inset border on every input) and drop the soft glow. */
[data-style="geocities"] .category-editor {
@@ -403,25 +806,34 @@
background: transparent !important;
}
/* Chips are tags, not raised Win95 buttons — keep a clean flat border
instead of the generic 3px outset bevel. */
[data-style="geocities"] .category-chip {
border: 2px solid var(--color-border);
box-shadow: none;
}
/* ── What is unread blinks ───────────────────────────────────────── */
[data-style="geocities"] .notification-badge {
animation: geo-blink 1.1s step-start infinite;
}
[data-style="geocities"] .notif-dot,
[data-style="geocities"] .unread-dot {
[data-style="geocities"] .notif-dot {
animation: geo-blink 1.3s step-start infinite;
}
/* ── Floating "new" affordance / FAB-style accents ───────────────── */
[data-style="geocities"] .fdz__upload-icon {
opacity: 0.7;
/* Nothing blinks or crawls for someone who asked for less motion: the
marquee then shows its line once, centred and still. */
@media (prefers-reduced-motion: reduce) {
[data-style="geocities"] :is(.notification-badge, .notif-dot, .unread-dot) {
animation: none;
}
[data-style="geocities"] .site-marquee {
text-align: center;
text-overflow: ellipsis;
}
[data-style="geocities"] .site-marquee-track {
animation: none;
}
[data-style="geocities"] .site-marquee-track > span:last-child {
display: none;
}
}
/* ── Casual web-1.0 voice: lowercase everything (code excepted) ──── */
@@ -437,19 +849,6 @@
text-transform: none;
}
/* ── Category switcher dropdown ──────────────────────────────────── */
/* Same ridged frame as the other popovers; option buttons flattened so the
chunky Win95 button bevel doesn't apply to menu rows. (The trigger reuses
.feed-sort-btn and keeps the bevel, matching the sort tabs.) */
[data-style="geocities"] .category-switcher-menu {
border: 3px ridge var(--color-border);
box-shadow: 0 0 18px color-mix(in srgb, var(--color-border) 45%, transparent);
}
[data-style="geocities"] .category-switcher-option {
border: none !important;
box-shadow: none !important;
}
/* Circles App.css draws with a literal 50% (not the radius scale). */
[data-style="geocities"] .notif-icon,
[data-style="geocities"] .notif-dot {

View File

@@ -8,6 +8,8 @@
accent) — the yellow paper is the point, keep it warm.
Syntax hues carry the character: fujiGray italic "comments" for dates,
springGreen "strings" for quotes, oniViolet "keywords" for the brand.
A section opens on a status line, as a buffer ends on one. The page is
flat, the editor's own ground: the cards and the type carry the scheme.
══════════════════════════════════════════════════════════════════════ */
/* ── Palette (light-dark: see tokens.css) ─────────────────────────── */
@@ -32,10 +34,13 @@
/* Surfaces — sunken < bg < surface < raised.
Wave: sumiInk0 · sumiInk3 · sumiInk4 · sumiInk5.
Lotus: lotusWhite1 · lotusWhite3 · lotusWhite2 · lotusWhite3 — paper
darkens where the editor draws gutters and floats. */
--color-bg: light-dark(#f2ecbc, #1f1f28);
--color-surface: light-dark(#e7dfb2, #2a2a37);
Lotus: lotusWhite1 · lotusWhite4 · lotusWhite3 · lotusWhite3. What is
read sits on lotusWhite3, the editor's own paper and the
brightest the scheme has, and the page around the cards is the
shade darker the editor gives its window chrome: the ink loses a
fifth of its contrast on the darker sheet. */
--color-bg: light-dark(#e7dfb2, #1f1f28);
--color-surface: light-dark(#f2ecbc, #2a2a37);
--color-surface-raised: light-dark(#f2ecbc, #363646);
--color-surface-sunken: light-dark(#dcd5ac, #16161d);
@@ -91,15 +96,17 @@
);
/* Theme-only hues, straight from the syntax groups:
comment fujiGray / lotusGray3 · keyword oniViolet / lotusViolet4
comment fujiGray / lotusGray2 · keyword oniViolet / lotusViolet4
string springGreen / lotusGreen · number sakuraPink / lotusPink
chrome sumiInk1 / lotusWhite1 (sidebars, tabline)
visual waveBlue1 / lotusViolet3 (selection) */
--kngw-comment: light-dark(#8a8980, #727169);
chrome sumiInk1 / lotusWhite0 (sidebars, tabline, status line)
visual waveBlue1 / lotusViolet3 (selection)
Lotus's own comment grey, lotusGray3, is too faint for a date set this
small, so the light scheme takes the next grey down. */
--kngw-comment: light-dark(#716e61, #727169);
--kngw-keyword: light-dark(#624c83, #957fb8);
--kngw-string: light-dark(#6f894e, #98bb6c);
--kngw-number: light-dark(#b35b79, #d27e99);
--kngw-chrome: light-dark(#e5ddb0, #181820);
--kngw-chrome: light-dark(#d5cea3, #181820);
--kngw-selection: light-dark(#c9cbd1, #223249);
/* Over a photo the ground is the black scrim in either scheme, so the
accent there stays Wave's crystalBlue — Lotus's lotusBlue4 is picked to
@@ -133,12 +140,17 @@
background: var(--kngw-selection);
}
/* Brand as a keyword — oniViolet, italic like kanagawa's keywordStyle */
/* Brand as a keyword — oniViolet, italic like kanagawa's keywordStyle.
Pointed at, it takes the accent, as a card's title does. */
[data-style="kanagawa"] .app-header-brand {
color: var(--kngw-keyword);
font-style: italic;
}
[data-style="kanagawa"] .app-header-brand:hover {
color: var(--color-accent);
}
/* ── Header — the tabline: a shade off the page, split by a plain rule ── */
[data-style="kanagawa"] .app-header {
border-bottom: 1px solid var(--color-border);
@@ -190,8 +202,8 @@
color: var(--kngw-string);
}
/* Sub-blocks part on a comment divider — the dashed rule and the ellipsis a
source file uses to mark a section, set in the mono face the theme reads in. */
/* Sub-blocks part on a comment divider: the dashed rule a source file marks
a section with, and its three dots. */
[data-style="kanagawa"] .journal-break {
--journal-break-space: 1.9rem;
--journal-break-rule: 1px dashed var(--color-border);
@@ -202,3 +214,33 @@
[data-style="kanagawa"] .vote-btn:not(.vote-btn--active) {
color: var(--kngw-number);
}
/* ── Status lines — the comments and the related dumps open on one ── */
/* The bar is the section's own background, a strip of the chrome as tall as
the label, and the label is the mode block at its left end: the accent,
bold, as an editor shows NORMAL. */
[data-style="kanagawa"] .comment-section,
[data-style="kanagawa"] .related-section {
--kngw-status-height: 1.7rem;
padding-top: 0;
border-top: none;
background: linear-gradient(var(--kngw-chrome), var(--kngw-chrome)) top /
100% var(--kngw-status-height) no-repeat;
}
[data-style="kanagawa"] .comment-section-title,
[data-style="kanagawa"] .related-section .related-section-title {
display: inline-block;
height: var(--kngw-status-height);
margin-bottom: 1.1rem;
padding: 0 0.8rem;
background: var(--color-accent);
color: var(--color-on-accent);
line-height: var(--kngw-status-height);
letter-spacing: 0.06em;
}
/* A dump's note is read at length: full ink, where the base dims it. */
[data-style="kanagawa"] .dump-comment {
opacity: 1;
}

View File

@@ -1,32 +1,32 @@
/* ══════════════════════════════════════════════════════════════════════
Melange theme — after savq/melange-nvim, whose palette is pure 1970s
desert: toasted amber, terracotta, olive, dusty rose, laid on roasted
coffee (dark) or warm plaster (light).
Melange theme — a blend, which is all the name means now. It began as a
port of savq/melange-nvim and kept that scheme's idea rather than its
browns: a handful of hues that only make sense mixed. Here they are a
sorbet's (pink, coral, lilac, sky, mint), run together as one gradient
along the header, through the site's name and between a journal's blocks,
on grounds of berry at night and of blush paper by day.
So this is not an editor skin — it's a warm print. The scheme's own
colours are kept exactly; what's built on them is analog: a soft
Windsor-ish serif, film grain over the whole page, pillowy borderless
cards that sit in warm shadow rather than inside a line, and a spice
gradient — the mélange itself, every hue blended — along the header.
The rest is a print of it: film grain over the whole page, pillowy
borderless cards that sit in a tinted shadow rather than inside a line,
a glow under whatever is active, and one quirky grotesque for all of it.
Dark scheme → melange dark (#292522 grounds, cream ink).
Light scheme → melange light (plaster, and every ink is a brown).
The two schemes are one print and its negative. They trade ink and paper:
the night's lilac-white type is the day's page, the night's plum ground
the day's type. What doesn't trade is the sorbet: the accent is the same
pink in both, steeped a little deeper by day so that it holds on paper,
and what is written on it is the same plum. A pill, a button, the stripe
under the header are the same objects in either.
Dark scheme → berry (plum grounds, lilac-white ink).
Light scheme → the same on paper (lilac-white grounds, plum ink).
══════════════════════════════════════════════════════════════════════ */
/* ── Palette (light-dark: see tokens.css) ─────────────────────────── */
:root[data-style="melange"] {
/* One family, soft-serif: Fraunces sits at the Souvenir/Cooper end of
the 70s, which is exactly where this palette lives.
Its optical-size axis is the catch — left on `auto`, a headline is
cut as a high-contrast Didone while the caption under it is a sturdy
slab, and one family starts reading as three. So opsz is pinned low
and everywhere: sturdy, low-contrast, warm at every size. Any rule
that touches SOFT or WONK repeats opsz, since font-variation-settings
replaces the whole list rather than merging into it. */
--font-body: "Fraunces Variable", Georgia, serif;
--font-display: "Fraunces Variable", Georgia, serif;
--mel-opsz: 14;
/* One family. fonts.css loads its weight axis alone, so it is the same
cut at every size. */
--font-body: "Bricolage Grotesque Variable", system-ui, sans-serif;
--font-display: var(--font-body);
/* Nothing here has a sharp corner. */
--radius-xs: 5px;
@@ -35,121 +35,119 @@
--radius-lg: 16px;
--radius-xl: 22px;
/* Text — a.fg / a.com / a.ui */
--color-text: light-dark(#54433a, #ece1d7);
--color-text-secondary: light-dark(#7d6658, #c1a78e);
--color-text-muted: light-dark(#a98a78, #867462);
--color-on-accent: light-dark(#f7f4f1, #292522);
/* Text — by day, the night's card colour. On the accent it is plum in
both schemes. */
--color-text: light-dark(#2b2338, #f1e8f3);
--color-text-secondary: light-dark(#564769, #c8b8d2);
--color-text-muted: light-dark(#746489, #a898b6);
--color-on-accent: #2a1830;
/* Surfaces — sunken < bg < surface < raised.
Dark: below a.bg · a.bg · a.float · a.sel.
Light: a.sel is the well, a.float is the *page* — melange's light
side keeps its warmth in the floats, so on a page made of
cards the plaster belongs underneath and the cards are the
bleached tints above it. Reversing those two tiers is what
keeps the light scheme from reading as grey. */
--color-bg: light-dark(#e9e1db, #292522);
--color-surface: light-dark(#f6f2ef, #34302c);
--color-surface-raised: light-dark(#fdfbf9, #403a36);
--color-surface-sunken: light-dark(#d9d3ce, #201d1b);
/* Surfaces — sunken < bg < surface < raised: the page is the tinted
ground in both schemes and a card the lighter sheet on it. By day the
page is the night's type colour. */
--color-bg: light-dark(#f1e8f3, #211a2b);
--color-surface: light-dark(#fbf7fc, #2b2338);
--color-surface-raised: light-dark(#fffcff, #372d47);
--color-surface-sunken: light-dark(#e3d6e7, #19131f);
/* Borders — a.ui, but mostly unused: this theme separates by warmth
and shadow, not by line. */
--color-border: light-dark(rgb(169 138 120 / 0.38), rgb(134 116 98 / 0.45));
/* Borders — mostly unused: this theme separates by tint and shadow, not
by line. */
--color-border: light-dark(rgb(117 98 131 / 0.34), rgb(168 152 182 / 0.36));
--color-border-subtle: light-dark(
rgb(169 138 120 / 0.18),
rgb(134 116 98 / 0.22)
rgb(117 98 131 / 0.16),
rgb(168 152 182 / 0.18)
);
/* Accent — b.yellow, the spice. Hover walks it toward c.yellow's burn. */
--color-accent: light-dark(#a06d00, #ebc06d);
--color-accent-hover: light-dark(#bc5c00, #e49b5d);
/* Accent — one pink, deeper on paper than at night. Hovering walks it one
step along the blend, to the coral. */
--color-accent: light-dark(#e2568f, #f48fb1);
--color-accent-hover: light-dark(#ea6a45, #ff9e80);
--color-accent-soft: light-dark(
rgb(160 109 0 / 0.13),
rgb(235 192 109 / 0.15)
rgb(226 86 143 / 0.12),
rgb(244 143 177 / 0.14)
);
--color-focus-ring: light-dark(
rgb(160 109 0 / 0.35),
rgb(235 192 109 / 0.4)
rgb(226 86 143 / 0.4),
rgb(244 143 177 / 0.42)
);
/* Links */
--color-link: light-dark(#a06d00, #ebc06d);
--color-link-hover: light-dark(#bc5c00, #e49b5d);
/* Links — a line of text needs more ink than a pill: the pink again,
steeped until it reads at any size on paper. */
--color-link: light-dark(#b62d6b, #f48fb1);
--color-link-hover: light-dark(#c2452b, #ff9e80);
/* Danger — b.red on d.red */
--color-danger: light-dark(#bf0021, #d47766);
--color-danger-bg: light-dark(#f1dedf, #7d2a2f);
--color-danger-hover: light-dark(#c77b8b, #bd8183);
--color-danger: light-dark(#c0303a, #ff8a80);
--color-danger-bg: light-dark(#f8dde0, #4d2030);
--color-danger-hover: light-dark(#c0303a, #c9404f);
/* Success — b.green on d.green */
--color-success: light-dark(#3a684a, #85b695);
--color-success-bg: light-dark(#d0e9d1, #233524);
--color-success-hover: light-dark(#6e9b72, #78997a);
--color-success: light-dark(#1b7f66, #86d3b4);
--color-success-bg: light-dark(#d5efe6, #1d3a33);
--color-success-hover: light-dark(#17705a, #a3e0c8);
/* Warning — c.yellow */
--color-warning: light-dark(#bc5c00, #e49b5d);
--color-warning: light-dark(#b5541f, #ffb38a);
/* Overlays */
--color-overlay: light-dark(rgb(84 67 58 / 0.3), rgb(32 29 27 / 0.72));
--color-overlay: light-dark(rgb(54 38 63 / 0.3), rgb(15 10 20 / 0.72));
--color-header-user-bg: transparent;
--color-header-user-bg-hover: light-dark(
rgb(84 67 58 / 0.07),
rgb(236 225 215 / 0.07)
rgb(54 38 63 / 0.07),
rgb(241 232 243 / 0.08)
);
/* Misc */
--color-option-bg: light-dark(#e9e1db, #34302c);
--color-option-bg: light-dark(#eee3ef, #2b2338);
--color-option-border: light-dark(
rgb(169 138 120 / 0.18),
rgb(134 116 98 / 0.22)
rgb(117 98 131 / 0.16),
rgb(168 152 182 / 0.18)
);
/* The rest of the desert — melange's c.* row, dusty rather than bright.
These are the theme's own furniture colours. */
--mel-terracotta: light-dark(#bc5c00, #e49b5d);
--mel-olive: light-dark(#6e9b72, #78997a);
--mel-rose: light-dark(#be79bb, #b380b0);
--mel-clay: light-dark(#c77b8b, #bd8183);
--mel-dust: light-dark(#a98a78, #867462);
/* The rest of the blend, beside the accent's pink: the theme's own
furniture colours. By day each is steeped as far as the accent is, a
sorbet still and not a flag's primary. */
--mel-coral: light-dark(#ea6a45, #ff9e80);
--mel-lilac: light-dark(#8262d8, #b9a2f2);
--mel-sky: light-dark(#3a8fd4, #86bdea);
--mel-mint: light-dark(#27a583, #86d3b4);
/* The mélange: every hue of the desert, blended once, left to right. */
/* The mélange: every hue, blended once, left to right in the order of
the wheel so no two neighbours mix to mud. */
--mel-spice: linear-gradient(
90deg,
var(--color-accent),
var(--mel-terracotta) 32%,
var(--mel-clay) 58%,
var(--mel-rose) 78%,
var(--mel-olive)
var(--mel-coral),
var(--color-accent) 26%,
var(--mel-lilac) 54%,
var(--mel-sky) 78%,
var(--mel-mint)
);
/* Shadows are warm and wide — sunlight, not a drop shadow. Light needs
more of them than dark: on plaster, shadow is the only thing holding
a card off the page. */
--shadow-color: light-dark(rgb(122 84 46 / 0.26), rgb(12 9 8 / 0.5));
--shadow-color-soft: light-dark(rgb(122 84 46 / 0.16), rgb(12 9 8 / 0.32));
--shadow-color-strong: light-dark(rgb(122 84 46 / 0.36), rgb(12 9 8 / 0.7));
/* Shadows are tinted with the ground and wide. Light needs more of them
than dark: on paper, shadow is the only thing holding a card off the
page. */
--shadow-color: light-dark(rgb(96 52 118 / 0.2), rgb(8 4 12 / 0.5));
--shadow-color-soft: light-dark(rgb(96 52 118 / 0.13), rgb(8 4 12 / 0.32));
--shadow-color-strong: light-dark(rgb(96 52 118 / 0.32), rgb(8 4 12 / 0.7));
--shadow-1: 0 2px 10px -2px var(--shadow-color-soft);
--shadow-2: 0 14px 34px -10px var(--shadow-color),
0 2px 6px var(--shadow-color-soft);
--shadow-3: 0 32px 70px -16px var(--shadow-color-strong),
0 3px 10px var(--shadow-color-soft);
/* Grain — one tile, two treatments (see body::after). Defaults are the
dark ones; the light scheme overrides them by selector. */
/* Grain — one fine tile for both schemes, laid differently (see
body::after). Defaults are the night's; the day overrides them by
selector. */
--mel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='0.22'/%3E%3C/svg%3E");
--mel-grain-blend: soft-light;
--mel-grain-opacity: 0.55;
/* Glow: the spice catches light where it's interactive. */
/* Glow: the accent catches light where it's interactive. */
--mel-glow: 0 3px 16px -4px
light-dark(rgb(160 109 0 / 0.45), rgb(235 192 109 / 0.35));
light-dark(rgb(226 86 143 / 0.5), rgb(244 143 177 / 0.38));
/* Component skins — no lines anywhere; warmth and shadow do the work. */
/* Component skins — no lines anywhere; tint and shadow do the work. */
--border-width: 1px;
/* The header is a board laid over the page, lighter than it in both
schemes: a.float on coffee, the bleached tint on plaster. */
--header-bg: light-dark(#fdfbf9, #34302c);
schemes. */
--header-bg: light-dark(#fffcff, #2b2338);
--input-focus-bg: var(--color-surface-raised);
--overlay-filter: blur(6px);
--modal-border: transparent;
@@ -161,22 +159,15 @@
--tab-border: transparent;
--btn-primary-shadow: var(--mel-glow);
--btn-primary-shadow-hover: 0 5px 22px -4px
light-dark(rgb(160 109 0 / 0.55), rgb(235 192 109 / 0.45));
light-dark(rgb(226 86 143 / 0.6), rgb(244 143 177 / 0.48));
}
/* ══════════════════════════════════════════════════════════════════════
Structural overrides
══════════════════════════════════════════════════════════════════════ */
/* ── One cut of Fraunces, at one optical size, for every size of text ── */
:root[data-style="melange"],
[data-style="melange"] body {
font-optical-sizing: none;
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 40, "WONK" 0;
}
/* ── The page is sunlit: warm light pools at the top, the ground below
is flat desert. ───────────────────────────────────────────────── */
/* ── The page is lit from above: the accent pools faintly at the top,
the ground below is flat. ────────────────────────────────────── */
[data-style="melange"] body {
background-image: radial-gradient(
120% 55% at 50% -12%,
@@ -190,13 +181,14 @@
/* ── Film grain over everything, so no surface reads as flat pixels.
Sits above the page, below modals and toasts.
The two schemes need opposite grain. On coffee, `soft-light` grits
the surface without lifting the blacks. On plaster that same blend
is invisible — pale grey noise on a pale ground has nothing to
work against — so light multiplies instead, which is what paper
tooth actually is: tiny shadows. Neither blend nor opacity takes
light-dark(), so the two parameters are custom properties and the
scheme is picked by selector below. ───────────────────────────── */
It is the same fine tile in both schemes, and the blend is what
differs. On berry, `soft-light` grits the surface without lifting the
blacks. On paper that blend does nothing, pale grey noise on a pale
ground having nothing to work against, so the day multiplies it:
paper tooth, which is tiny shadows, kept faint enough that the page
stays the colour it is given. Neither blend nor opacity takes
light-dark(), so the two are custom properties and the scheme is
picked by selector below. ───────────────────────────────────────── */
[data-style="melange"] body::after {
content: "";
position: fixed;
@@ -212,22 +204,19 @@
it because they come later and are more specific. */
@media (prefers-color-scheme: light) {
:root[data-style="melange"] {
--mel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.55' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.42'/%3E%3C/svg%3E");
--mel-grain-blend: multiply;
--mel-grain-opacity: 0.85;
--mel-grain-opacity: 0.4;
}
}
:root[data-style="melange"][data-color-scheme="light"] {
--mel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.55' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.42'/%3E%3C/svg%3E");
--mel-grain-blend: multiply;
--mel-grain-opacity: 0.85;
--mel-grain-opacity: 0.4;
}
/* Repeats the fine tile too: forcing dark under a light OS would
otherwise inherit the coarse one from the media query above. */
/* Forcing dark under a light OS would otherwise inherit the day's two from
the media query above. */
:root[data-style="melange"][data-color-scheme="dark"] {
--mel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)' opacity='0.22'/%3E%3C/svg%3E");
--mel-grain-blend: soft-light;
--mel-grain-opacity: 0.55;
}
@@ -239,8 +228,8 @@
}
/* ── Header — a board over the page: no rule under it, just its own
shade, a warm shadow, and the spice laid along its bottom edge as
a single blended stripe. ──────────────────────────────────────── */
shade, a tinted shadow, and the blend laid along its bottom edge
as a single stripe. ──────────────────────────────────────────── */
[data-style="melange"] .app-header {
border-bottom: none;
box-shadow: var(--shadow-1);
@@ -251,25 +240,55 @@
content: "";
position: absolute;
inset: auto 0 0 0;
height: 2px;
height: 3px;
background: var(--mel-spice);
opacity: 0.7;
}
/* Brand — soft and a little wonky, the way 70s display type was cut. */
/* Brand — the site's name is cut out of the blend itself. The fill takes
the place of the text's colour, so the emoji beside it, which sits
outside the name's span, keeps its own. The blend is laid twice as wide
as the name and drifts under it, from its warm end to its cool one and
back, so the name is never the same two colours for long. Pointed at, the
name fills with the accent over the blend, as a card's title turns to it. */
[data-style="melange"] .app-header-brand {
color: var(--mel-terracotta);
font-weight: 600;
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 100, "WONK" 1;
letter-spacing: -0.01em;
font-weight: 800;
letter-spacing: -0.02em;
}
[data-style="melange"] .form-page-title,
[data-style="melange"] .app-header-brand-name {
background: var(--mel-spice) 0 0 / 200% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
transition: color var(--dur-base);
animation: mel-drift 7s ease-in-out infinite alternate;
}
[data-style="melange"] .app-header-brand:hover .app-header-brand-name {
color: var(--color-accent);
}
@keyframes mel-drift {
to {
background-position: 100% 0;
}
}
/* Held still, the name shows the whole blend. */
@media (prefers-reduced-motion: reduce) {
[data-style="melange"] .app-header-brand-name {
background-size: 100% 100%;
animation: none;
}
}
/* Headlines are heavy and set a little tight, as the face likes. */
[data-style="melange"] h1,
[data-style="melange"] .dump-title,
[data-style="melange"] .page-title,
[data-style="melange"] .profile-section-title,
[data-style="melange"] .comment-section-title,
[data-style="melange"] .related-section-title {
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 80, "WONK" 1;
[data-style="melange"] .form-page-title {
font-weight: 800;
letter-spacing: -0.02em;
}
/* Tabs keep the shared uppercase cut, but the active one glows. */
@@ -306,55 +325,50 @@
}
}
/* ── Metadata is dust: quiet, warm, never black ──────────────────── */
[data-style="melange"] .journal-card:not(.journal-card--image) .journal-card-meta,
[data-style="melange"] .dump-card-date,
[data-style="melange"] .comment-time,
[data-style="melange"] .notification-time {
color: var(--mel-dust);
}
/* A pointed-at headline over a photo sits on a pillow of roasted coffee —
the dark scheme's ground in either scheme, since the scrim under it is
dark in both — lit in the dark scheme's amber, in warm shadow. */
/* A pointed-at headline over a photo sits on a pillow of the night ground
— the dark scheme's in either scheme, since the scrim under it is dark in
both — lit in the dark scheme's pink. */
[data-style="melange"] .journal-card--image .journal-card-title {
--journal-title-hover-bg: rgb(41 37 34 / 0.84);
--journal-title-hover-color: #ebc06d;
--journal-title-hover-bg: rgb(33 26 43 / 0.86);
--journal-title-hover-color: #f48fb1;
--journal-title-hover-radius: var(--radius-sm);
--journal-title-hover-shadow: 0 4px 14px -4px rgb(0 0 0 / 0.5);
}
/* ── A quote is a printed pull-quote: big soft mark, terracotta ──── */
/* ── A quote is a printed pull-quote: big mark, in the coral ─────── */
[data-style="melange"] .journal-card--quote {
background: var(--color-surface);
}
[data-style="melange"] .journal-card-quotemark {
color: var(--mel-terracotta);
opacity: 0.28;
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 100, "WONK" 1;
color: var(--mel-coral);
opacity: 0.3;
}
[data-style="melange"] .journal-card-quote-body {
font-size: var(--text-lg);
line-height: 1.35;
font-variation-settings: "opsz" var(--mel-opsz), "SOFT" 70, "WONK" 0;
}
/* The mélange runs along the header's bottom edge; between sub-blocks it
returns as a thinner, softer band of the same blend — the page divided by
its own palette rather than by a line. A rule can't carry a gradient, so
this one is a filled bar and the base break's border stands down. */
/* The blend runs along the header's bottom edge; between sub-blocks it
returns as a softer band of the same — the page divided by its own
palette rather than by a line. A rule can't carry a gradient, so this one
is a filled bar and the base break's border stands down. */
[data-style="melange"] .journal-break {
--journal-break-space: 2rem;
--journal-break-rule: none;
height: 3px;
border-radius: 999px;
background: var(--mel-spice);
opacity: 0.55;
opacity: 0.6;
}
/* Vote tallies wear the dusty rose of the palette's far end */
/* Vote tallies are the blend's lilac, apart from the accent's pink. */
[data-style="melange"] .vote-btn:not(.vote-btn--active) {
color: var(--mel-rose);
color: var(--mel-lilac);
}
/* A dump's note is read at length: full ink, where the base dims it. */
[data-style="melange"] .dump-comment {
opacity: 1;
}

View File

@@ -30,7 +30,12 @@
--color-on-accent: light-dark(#ffffff, #121212);
--color-bg: light-dark(#ffffff, #121212);
--color-surface: light-dark(#ffffff, #1e1e1e);
--color-border: light-dark(#dfdfdf, #3a3a3a);
--color-border: light-dark(#dfdfdf, #323232);
/* The heavy rules: under the masthead, over a section's label. On paper
they are ink. On the night page ink is near white, and a white line
across black is far louder than a black one across white, so there they
are a mid grey, still twice the hairlines' weight to the eye. */
--nyt-rule: light-dark(#121212, #5e5e5e);
--color-border-subtle: light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.12));
--color-accent: light-dark(#121212, #ececec);
--color-accent-hover: light-dark(#000000, #ffffff);
@@ -47,7 +52,7 @@
--color-header-user-bg: transparent;
--color-header-user-bg-hover: light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.1));
--color-option-bg: light-dark(#f4f4f4, #181818);
--color-option-border: light-dark(#dfdfdf, #3a3a3a);
--color-option-border: light-dark(#dfdfdf, #323232);
/* The masthead is printed on the page, in both schemes. */
--header-bg: var(--color-bg);
@@ -77,8 +82,14 @@
}
/* ── The interface's sans — controls, navigation, labels, datelines ── */
/* The button classes are listed beside `button` because a link is drawn as
one too (the 404 page's "Go home"). */
[data-style="nyt"] :is(
button,
.btn-primary,
.btn-secondary,
.btn-danger,
.btn-border,
input,
select,
label,
@@ -190,11 +201,6 @@
font-size: 2.5rem;
line-height: 1.05;
}
/* The base title breaks anywhere, which lets its text be squeezed under the
width of a word: here a word stays whole unless it is wider than the line. */
[data-style="nyt"] .page-title {
overflow-wrap: break-word;
}
[data-style="nyt"] .dump-card-title,
[data-style="nyt"] .playlist-card-title {
font-size: 1.5rem;
@@ -279,14 +285,14 @@
letter-spacing: 0.06em;
}
[data-style="nyt"] .page-group-label {
border-top: 1px solid var(--color-text);
border-top: 1px solid var(--nyt-rule);
padding-top: 0.5rem;
}
/* These two sections already open on a rule: it turns to ink and the label
moves up under it. */
[data-style="nyt"] .comment-section,
[data-style="nyt"] .related-section {
border-top-color: var(--color-text);
border-top-color: var(--nyt-rule);
padding-top: 0.5rem;
}
/* The label's own trailing hairline would double the rule above it. */
@@ -295,7 +301,7 @@
}
[data-style="nyt"] .profile-section-header {
border-bottom: none;
border-top: 1px solid var(--color-text);
border-top: 1px solid var(--nyt-rule);
padding: 0.5rem 0 0;
}
[data-style="nyt"] .profile-section-title {
@@ -398,8 +404,15 @@
padding-bottom: 0.22em;
}
/* Pointed at, the name is underlined, as a headline is. */
[data-style="nyt"] .app-header-brand:hover .app-header-brand-name {
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
[data-style="nyt"] .app-header {
border-bottom: 1px solid var(--color-text);
border-bottom: 1px solid var(--nyt-rule);
}
/* Where there is room the nameplate takes the middle of a row of its own,
@@ -434,7 +447,7 @@
display: none;
}
[data-style="nyt"] .app-header:has(.header-center-slot, .search-bar--expanded) {
border-bottom: 3px double var(--color-text);
border-bottom: 3px double var(--nyt-rule);
}
}
@@ -694,6 +707,60 @@
padding: var(--journal-quote-pad);
}
/* A story with no picture is set as text. The shared mosaic gives it a
generated placeholder under the same dark scrim a photo gets, which on
paper is a grey smudge with a white headline in it: here the placeholder
and the scrim go, the headline is ink at the top of its cell, and the
dateline and the vote close the cell at its foot. A playable one keeps its
badge in the corner. */
[data-style="nyt"] .journal-card--image:has(.journal-card-image .thumb-placeholder) {
padding: 1.05rem;
}
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .journal-card-image {
background: none;
}
[data-style="nyt"] .journal-card--image .journal-card-image .thumb-placeholder {
display: none;
}
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .journal-card-overlay {
flex: 1;
margin-top: 0;
padding: 0;
background: none;
}
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .journal-card-footer {
margin-top: auto;
}
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .journal-card-title,
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .journal-card-title:hover {
color: var(--color-text);
background: none;
}
/* The headline stops short of the play badge in the cell's corner. */
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder):has(.rich-content-play-overlay) .journal-card-title {
max-width: calc(100% - 2.2rem);
}
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .journal-card-meta {
color: var(--color-text-secondary);
}
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .journal-card-comment {
color: var(--color-text-secondary);
}
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .journal-card-overlay .vote-btn:not(.vote-btn--active) {
background: transparent;
color: var(--color-text);
}
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) .unread-dot {
background: var(--color-text);
}
/* On a phone a tile takes a photo's proportions; text takes its own height. */
@media (max-width: 460px) {
[data-style="nyt"] .journal-card--image:has(.thumb-placeholder) {
aspect-ratio: auto;
min-height: 7rem;
}
}
/* ── Primary button — inverted ink-on-paper (auto-flips per scheme) ─ */
[data-style="nyt"] .btn-primary {
background: var(--color-text);