mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
TABLES. next-mdx-remote had no remark plugins, so GFM tables rendered as literal pipe characters in a paragraph. Four published posts were affected, not just the new one: bauhaus-echo-launch, flutter-3d-physics-dice, vegan-iq-data-pipeline and smirk-v0-3-0. remark-gfm is now wired into MDXRemote's mdxOptions. BACKTICKS. @tailwindcss/typography wraps every inline <code> in literal backtick characters via code::before / code::after. The served HTML was already clean; the marks were injected by CSS. Monospace already marks code as code, so the backticks were a redundant second marker that reads as a typo in running prose. Suppressed across .prose. TRANSPORT. The previous version's "transports" section covered only Grin slate channels. Tor is a separate mechanism and was undocumented: the wallet accepts a .onion backend origin over plain http, alongside private ranges and .local, because onion services carry their own transport authentication and no CA issues for them. The onion pattern matches the label immediately before .onion, so an onion-lookalike public host is refused. On the backend, the admin plane binds to loopback and TOR_ADMIN_ONION adds exactly one onion host to its Host allowlist as a DNS-rebinding guard. There is no bundled Tor daemon and no I2P support anywhere in either repo. The post now says so rather than leaving it implied. COPY. Removed the editorial voice throughout: no more "the part worth explaining", "the instructive one", "worth dwelling on". A technical report states what a thing does and lets the reader decide what matters. Headings are now descriptive rather than argumentative.
376 lines
12 KiB
CSS
376 lines
12 KiB
CSS
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
@layer base {
|
|
:root {
|
|
--background: 0 0% 100%;
|
|
--foreground: 222.2 84% 4.9%;
|
|
|
|
--card: 0 0% 100%;
|
|
--card-foreground: 222.2 84% 4.9%;
|
|
|
|
--popover: 0 0% 100%;
|
|
--popover-foreground: 222.2 84% 4.9%;
|
|
|
|
--primary: 222.2 47.4% 11.2%;
|
|
--primary-foreground: 210 40% 98%;
|
|
|
|
--secondary: 210 40% 96.1%;
|
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
|
|
--muted: 210 40% 96.1%;
|
|
--muted-foreground: 215.4 16.3% 46.9%;
|
|
|
|
--accent: 210 40% 96.1%;
|
|
--accent-foreground: 222.2 47.4% 11.2%;
|
|
|
|
--destructive: 0 84.2% 60.2%;
|
|
--destructive-foreground: 210 40% 98%;
|
|
|
|
--border: 214.3 31.8% 91.4%;
|
|
--input: 214.3 31.8% 91.4%;
|
|
--ring: 222.2 84% 4.9%;
|
|
|
|
--radius: 0.5rem;
|
|
}
|
|
|
|
.dark {
|
|
--background: 222.2 84% 4.9%;
|
|
--foreground: 210 40% 98%;
|
|
|
|
--card: 222.2 84% 4.9%;
|
|
--card-foreground: 210 40% 98%;
|
|
|
|
--popover: 222.2 84% 4.9%;
|
|
--popover-foreground: 210 40% 98%;
|
|
|
|
--primary: 210 40% 98%;
|
|
--primary-foreground: 222.2 47.4% 11.2%;
|
|
|
|
--secondary: 217.2 32.6% 17.5%;
|
|
--secondary-foreground: 210 40% 98%;
|
|
|
|
--muted: 217.2 32.6% 17.5%;
|
|
--muted-foreground: 215 20.2% 65.1%;
|
|
|
|
--accent: 217.2 32.6% 17.5%;
|
|
--accent-foreground: 210 40% 98%;
|
|
|
|
--destructive: 0 62.8% 30.6%;
|
|
--destructive-foreground: 210 40% 98%;
|
|
|
|
--border: 217.2 32.6% 17.5%;
|
|
--input: 217.2 32.6% 17.5%;
|
|
--ring: 212.7 26.8% 83.9%;
|
|
}
|
|
}
|
|
|
|
@layer base {
|
|
* {
|
|
@apply border-border;
|
|
}
|
|
body {
|
|
@apply bg-background text-foreground;
|
|
}
|
|
}
|
|
@layer components {
|
|
/* High-contrast content masking for accessibility and streamer protection */
|
|
.mask-backdrop {
|
|
background-color: hsl(var(--card) / 0.98);
|
|
backdrop-filter: blur(64px);
|
|
-webkit-backdrop-filter: blur(64px);
|
|
border-width: 2px;
|
|
border-color: hsl(var(--border) / 0.5);
|
|
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: transform 300ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
box-shadow 300ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
border-color 300ms ease;
|
|
}
|
|
|
|
.section-container {
|
|
@apply mask-backdrop rounded-[2.5rem] p-12 md:p-20 text-card-foreground;
|
|
}
|
|
|
|
.glass-card {
|
|
@apply mask-backdrop rounded-[2rem] p-10 text-card-foreground;
|
|
}
|
|
|
|
.glass-card:hover {
|
|
border-color: hsl(var(--primary) / 0.3);
|
|
}
|
|
|
|
/* Bento grid entrance animation */
|
|
@keyframes bento-enter {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(16px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.bento-item {
|
|
animation: bento-enter 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
}
|
|
|
|
/* Quiet bento: at rest the card chrome (opaque bg, 64px frost, border, shadow) is
|
|
nearly invisible so only the title + the 3D render/image float on the page; the
|
|
glass lights up on hover / focus / tap. Title + image keep their own visibility.
|
|
Higher specificity than .mask-backdrop, so it wins at rest without !important. */
|
|
.bento-item .glass-card {
|
|
background-color: transparent;
|
|
backdrop-filter: none;
|
|
-webkit-backdrop-filter: none;
|
|
border-color: transparent;
|
|
box-shadow: none;
|
|
transition: background-color 0.3s ease, backdrop-filter 0.3s ease,
|
|
border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
|
|
}
|
|
.bento-item:hover .glass-card,
|
|
.bento-item:focus-within .glass-card {
|
|
backdrop-filter: blur(64px);
|
|
-webkit-backdrop-filter: blur(64px);
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 32px 64px -16px rgb(0 0 0 / 0.3);
|
|
}
|
|
|
|
/* Focus-visible ring for keyboard navigation */
|
|
.bento-item:focus-visible {
|
|
outline: 2px solid hsl(var(--ring));
|
|
outline-offset: 4px;
|
|
border-radius: 2.5rem;
|
|
}
|
|
|
|
/* Respect reduced motion preferences */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.bento-item {
|
|
animation: none;
|
|
}
|
|
.bento-item:hover .glass-card {
|
|
transform: none;
|
|
}
|
|
.glass-card {
|
|
transition: border-color 300ms;
|
|
}
|
|
.glass-card .object-contain,
|
|
.glass-card .object-cover {
|
|
transition: none !important;
|
|
}
|
|
}
|
|
|
|
/* Typography customization for blog posts */
|
|
.prose {
|
|
--tw-prose-body: hsl(var(--foreground));
|
|
--tw-prose-headings: hsl(var(--foreground));
|
|
--tw-prose-lead: hsl(var(--muted-foreground));
|
|
--tw-prose-links: hsl(var(--primary));
|
|
--tw-prose-bold: hsl(var(--foreground));
|
|
--tw-prose-counters: hsl(var(--muted-foreground));
|
|
--tw-prose-bullets: hsl(var(--muted-foreground));
|
|
--tw-prose-hr: hsl(var(--border));
|
|
--tw-prose-quotes: hsl(var(--foreground));
|
|
--tw-prose-quote-borders: hsl(var(--border));
|
|
--tw-prose-captions: hsl(var(--muted-foreground));
|
|
--tw-prose-code: hsl(var(--foreground));
|
|
--tw-prose-pre-code: hsl(var(--foreground));
|
|
--tw-prose-pre-bg: hsl(var(--muted));
|
|
--tw-prose-th-borders: hsl(var(--border));
|
|
--tw-prose-td-borders: hsl(var(--border));
|
|
}
|
|
|
|
.prose h2 {
|
|
@apply text-xl font-semibold mt-10 mb-4 pb-2 border-b border-border;
|
|
}
|
|
|
|
.prose h3 {
|
|
@apply text-lg font-medium mt-6 mb-3;
|
|
}
|
|
|
|
.prose p {
|
|
@apply leading-relaxed mb-4;
|
|
}
|
|
|
|
.prose ul {
|
|
@apply my-4 ml-4 space-y-2;
|
|
}
|
|
|
|
.prose li {
|
|
@apply leading-relaxed;
|
|
}
|
|
|
|
.prose a {
|
|
@apply text-emerald-600 dark:text-emerald-400 underline underline-offset-2 hover:text-emerald-700 dark:hover:text-emerald-300 transition-colors;
|
|
}
|
|
|
|
.prose strong {
|
|
@apply font-semibold;
|
|
}
|
|
|
|
.prose hr {
|
|
@apply my-8 border-border;
|
|
}
|
|
|
|
.prose table {
|
|
@apply w-full my-6 text-sm;
|
|
}
|
|
|
|
.prose thead th {
|
|
@apply text-left font-semibold p-3 bg-muted/50 border-b border-border;
|
|
}
|
|
|
|
.prose tbody td {
|
|
@apply p-3 border-b border-border;
|
|
}
|
|
|
|
.prose tbody tr:hover {
|
|
@apply bg-muted/30;
|
|
}
|
|
}
|
|
|
|
/* Respect users who prefer reduced motion */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
}
|
|
|
|
/* Splash logo zoom on click-to-enter (CSS, so the logo needs no framer-motion
|
|
wrapper and stays a clean LCP candidate). */
|
|
@keyframes splash-zoom { 0% { transform: scale(1); } 50% { transform: scale(1.06); } 100% { transform: scale(1.14); } }
|
|
.splash-zoom { animation: splash-zoom 0.8s ease-in-out forwards; }
|
|
|
|
/* Click-to-enter Cherenkov wash (was framer-motion; now CSS so the splash ships no
|
|
JS animation library). */
|
|
@keyframes splash-wash { 0% { opacity: 0; } 50% { opacity: 0.9; } 100% { opacity: 0; } }
|
|
.splash-wash { animation: splash-wash 0.8s ease-in-out forwards; }
|
|
|
|
/* Tiny terminal inside the nebula monitor: type "such" once, backspace, then a blinking
|
|
caret on the left. width via steps() is the typewriter; border-right is the caret. */
|
|
.nebula-term {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 9px;
|
|
line-height: 1;
|
|
letter-spacing: 0.5px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
width: 0;
|
|
border-right: 1.5px solid currentColor;
|
|
/* type "such", hold, backspace (nebula-type, once); blink a finite number of times
|
|
(nebula-caret); then fade the whole thing out so it stops being distracting. */
|
|
/* blink ~4 times, type "such", backspace, blink ~4 more, then fade away. */
|
|
animation:
|
|
nebula-type 2.2s steps(4, end) 3.2s forwards,
|
|
nebula-caret 0.8s steps(1, end) 11,
|
|
nebula-term-out 0.6s ease-in 8.4s forwards;
|
|
}
|
|
@keyframes nebula-type {
|
|
0% { width: 0; }
|
|
36% { width: 4ch; }
|
|
64% { width: 4ch; }
|
|
90% { width: 0; }
|
|
100% { width: 0; }
|
|
}
|
|
@keyframes nebula-caret { 0%, 50% { border-color: currentColor; } 50.01%, 100% { border-color: transparent; } }
|
|
@keyframes nebula-term-out { to { opacity: 0; } }
|
|
|
|
/* Hero title: an animated gradient sheen. LCP-safe: the text paints immediately with the
|
|
gradient; only the gradient POSITION animates, which never delays the largest paint. The
|
|
global reduced-motion rule freezes it to the base gradient.
|
|
The wider 300% ramp over an 18s cycle makes this a slow drift rather than a visible sweep,
|
|
and the symmetric cubic-bezier easing removes the hard turnaround the old ease-in-out had
|
|
at each end of the travel. */
|
|
/* The brand's four-colour system (S green, U magenta, C blue, H teal), one per glyph.
|
|
Light theme uses the light palette; .dark swaps in the dark palette, which is the
|
|
same hues lifted for contrast on near-black -- from packs/such-software/pack.json. */
|
|
.hero-title {
|
|
background-image: linear-gradient(110deg, #1a5c38 0%, #b83670 30%, #176a92 55%, #12857e 80%, #1a5c38 100%);
|
|
background-size: 300% auto;
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
color: transparent;
|
|
animation: hero-sheen 18s cubic-bezier(0.37, 0, 0.63, 1) infinite;
|
|
}
|
|
.dark .hero-title {
|
|
background-image: linear-gradient(110deg, #35925c 0%, #e8689e 30%, #4aade0 55%, #3ad4c6 80%, #35925c 100%);
|
|
}
|
|
.brand-gradient-text {
|
|
background-image: linear-gradient(90deg, #1a5c38, #b83670, #176a92, #12857e);
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
color: transparent;
|
|
}
|
|
.dark .brand-gradient-text {
|
|
background-image: linear-gradient(90deg, #35925c, #e8689e, #4aade0, #3ad4c6);
|
|
}
|
|
@keyframes hero-sheen {
|
|
0%, 100% { background-position: 0% center; }
|
|
50% { background-position: 100% center; }
|
|
}
|
|
/* Soft aura behind the hero title (decorative). Gentle breathing scale/opacity. */
|
|
@keyframes hero-aura { 0%, 100% { opacity: 0.55; transform: translateX(-50%) scale(1); } 50% { opacity: 0.8; transform: translateX(-50%) scale(1.08); } }
|
|
.hero-aura { animation: hero-aura 7s ease-in-out infinite; }
|
|
/* Pre-paint splash suppression. The inline <head> script stamps this attribute when the
|
|
visitor has already entered this session (or asked for reduced motion), so the
|
|
server-rendered overlay is hidden in the very first painted frame instead of being
|
|
removed a frame later by hydration -- which is what the refresh flash was. The overlay
|
|
is `fixed`, so this removes nothing from the document flow and shifts no layout. */
|
|
html[data-splash="skip"] #splash-overlay {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Splash handover burst: the four pillar cells disperse as the mark hands over to the page.
|
|
The blobs sit where the monogram's four cells were (a 2x2 grid about the centre), so this
|
|
reads as the logo coming apart rather than as a generic flash.
|
|
|
|
ONLY transform and opacity animate, both compositor properties. This deliberately avoids
|
|
animating filter/background: the matrix animation pegged a core doing exactly that, because
|
|
a filter change re-renders the whole filter graph every frame. Soft edges come from a static
|
|
radial-gradient that is painted once and then only transformed. */
|
|
@keyframes splash-burst {
|
|
from { transform: translate(-50%, -50%) scale(0.35); opacity: var(--burst-peak); }
|
|
to { transform: translate(-50%, -50%) scale(3.1); opacity: 0; }
|
|
}
|
|
.splash-burst-blob {
|
|
position: absolute;
|
|
width: 34vmin;
|
|
height: 34vmin;
|
|
border-radius: 9999px;
|
|
opacity: 0;
|
|
will-change: transform, opacity;
|
|
animation: splash-burst 420ms cubic-bezier(0.16, 0.84, 0.44, 1) forwards;
|
|
/* Half-transparent by construction: the gradient's own alpha never reaches 1, and
|
|
--burst-peak holds the whole layer under half opacity. */
|
|
--burst-peak: 0.42;
|
|
}
|
|
.dark .splash-burst-blob { --burst-peak: 0.5; }
|
|
.splash-burst-s { background: radial-gradient(circle, rgb(26 92 56 / 0.55), transparent 70%); }
|
|
.splash-burst-u { background: radial-gradient(circle, rgb(184 54 112 / 0.55), transparent 70%); }
|
|
.splash-burst-c { background: radial-gradient(circle, rgb(23 106 146 / 0.55), transparent 70%); }
|
|
.splash-burst-h { background: radial-gradient(circle, rgb(18 133 126 / 0.55), transparent 70%); }
|
|
.dark .splash-burst-s { background: radial-gradient(circle, rgb(53 146 92 / 0.55), transparent 70%); }
|
|
.dark .splash-burst-u { background: radial-gradient(circle, rgb(232 104 158 / 0.55), transparent 70%); }
|
|
.dark .splash-burst-c { background: radial-gradient(circle, rgb(74 173 224 / 0.55), transparent 70%); }
|
|
.dark .splash-burst-h { background: radial-gradient(circle, rgb(58 212 198 / 0.55), transparent 70%); }
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.splash-burst-blob { display: none; }
|
|
}
|
|
|
|
/* @tailwindcss/typography wraps every inline <code> in literal backtick characters via
|
|
code::before / code::after. The monospace treatment already marks code as code, so the
|
|
backticks are a second, redundant marker that reads as a typo in running prose:
|
|
`homeDomain` instead of homeDomain. Turn them off across the blog. */
|
|
.prose :where(code):not(:where([class~="not-prose"] *))::before,
|
|
.prose :where(code):not(:where([class~="not-prose"] *))::after {
|
|
content: none;
|
|
}
|