add liveline page: recreation of benji.org/liveline editorial design

Recreates the benji.org/liveline page in the website folder using React,
Tailwind, and the same editorial design system. Charts are replaced with
static SVG placeholders.

Typography:
- Inter variable font from rsms.me (same source as next/font)
- font-weight 475 body, 560 headings, font-optical-sizing: auto
- 14px body, 20px line-height, -0.09px letter-spacing
- Near-black text rgb(17,17,17), 40% opacity secondary
- Display P3 accent colors with @supports fallback

Layout:
- 550px narrow content column, fixed TOC sidebar
- Stagger-in entrance animation with cascading delays
- Fixed header fade gradient (multi-stop white-to-transparent)
- Scroll-spy TOC highlighting via IntersectionObserver

Code blocks:
- Prism.js syntax highlighting with GitHub-light color palette
- Line numbers column with 20px right padding
- 12px / 18px font size

Props tables, dividers, captions, code blocks, bullet lists all
styled to match the original computed values.
This commit is contained in:
Tommy D. Rossi
2026-02-19 09:56:30 +01:00
parent 38098c6153
commit a8481af89b
6 changed files with 1308 additions and 2 deletions
+135
View File
@@ -0,0 +1,135 @@
/*
* Prism.js light theme matching benji.org's GitHub-style syntax highlighting.
*
* Color palette extracted from the original:
* - Keywords (import, function, return): #CF222E (red)
* - Strings: #0A3069 (dark blue)
* - Functions/components: #8250DF (purple)
* - Parameters/properties: #953800 (orange-brown)
* - HTML tags: #116329 (green)
* - Attributes/numbers: #0550AE (blue)
* - Punctuation/text: #1F2328 (near-black)
*/
/* Base code style - override Prism defaults */
code[class*="language-"],
pre[class*="language-"] {
color: #1f2328;
background: none;
text-shadow: none;
font-family: "SF Mono", "SFMono-Regular", "Consolas", "Liberation Mono",
Menlo, Courier, monospace;
font-size: 12px;
line-height: 18px;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
tab-size: 2;
hyphens: none;
}
/* Keywords: import, from, function, return, const, let, var, export, default */
.token.keyword,
.token.module,
.token.imports,
.token.control-flow {
color: #cf222e;
}
/* Strings and template literals */
.token.string,
.token.template-string,
.token.attr-value {
color: #0a3069;
}
/* Function names */
.token.function,
.token.class-name {
color: #8250df;
}
/* Object properties, parameters, variables in destructuring */
.token.property,
.token.parameter {
color: #953800;
}
/* HTML/JSX tag names */
.token.tag .token.tag {
color: #116329;
}
/* Also color component names (PascalCase tags) in JSX */
.token.tag .token.class-name {
color: #116329;
}
/* Attributes in JSX/HTML */
.token.tag .token.attr-name,
.token.attr-name {
color: #0550ae;
}
/* Numbers */
.token.number {
color: #0550ae;
}
/* Operators: =, ===, =>, +, - */
.token.operator {
color: #cf222e;
}
/* Punctuation: brackets, parens, commas, semicolons */
.token.punctuation {
color: #1f2328;
}
/* Booleans, null, undefined */
.token.boolean,
.token.constant,
.token.builtin {
color: #0550ae;
}
/* Comments */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6e7781;
font-style: italic;
}
/* JSX specific: tag punctuation < > / */
.token.tag .token.punctuation {
color: #1f2328;
}
/* Spread operator, rest */
.token.spread {
color: #cf222e;
}
/* Plain text inside tags */
.token.plain-text {
color: #1f2328;
}
/* Regex */
.token.regex {
color: #0a3069;
}
/* Shell/bash specific */
.language-bash .token.function {
color: #0550ae;
}
/* No text shadow on any token */
.token {
text-shadow: none;
}
+147
View File
@@ -0,0 +1,147 @@
/*
* Liveline page styles - recreating benji.org's editorial design system.
*
* Key design decisions from the original:
* - Inter variable font at weight 460 (between normal and medium)
* - Near-black text (#111) not pure black, with 40% opacity for secondary
* - Narrow 550px content column for optimal reading
* - Negative letter-spacing on headings for tight, refined feel
* - 7-layer box shadows for realistic depth
* - Display P3 accent colors (wider gamut than sRGB)
* - Staggered entrance animations with subtle translateY
* - Custom cubic-bezier easings with slight overshoot ("snappy", "swift")
* - Fading header gradient pseudo-element
* - Small font sizes (14px body) for editorial/compact aesthetic
*/
/* ========================================================================
Design tokens
======================================================================== */
.liveline-page {
--ll-font-primary: "Inter var", "Inter", system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--ll-font-secondary: "Newsreader", Georgia, "Times New Roman", serif;
--ll-font-code: "SF Mono", "SFMono-Regular", "Consolas", "Liberation Mono",
Menlo, Courier, monospace;
/* Spacing scale */
--ll-spacing-xxs: 0.5rem;
--ll-spacing-xs: 1rem;
--ll-spacing-sm: 1.5rem;
--ll-spacing-md: 2rem;
--ll-spacing-lg: 2.5rem;
--ll-spacing-xl: 3rem;
--ll-spacing-xxl: 3.5rem;
/* Timing */
--ll-duration-snappy: 220ms;
--ll-ease-snappy: cubic-bezier(0.175, 0.885, 0.32, 1.1);
--ll-duration-swift: 800ms;
--ll-ease-swift: cubic-bezier(0.175, 0.885, 0.32, 1.275);
--ll-duration-smooth: 300ms;
--ll-ease-smooth: cubic-bezier(0.19, 1, 0.22, 1);
/* Colors - near-black, not pure black */
--ll-text-primary: rgb(17, 17, 17);
--ll-text-secondary: rgba(0, 0, 0, 0.4);
--ll-text-tertiary: rgba(0, 0, 0, 0.25);
--ll-bg: #fff;
--ll-border: #e3e3e3;
--ll-code-bg: rgba(0, 0, 0, 0.02);
--ll-selection-bg: rgba(0, 0, 0, 0.08);
/* P3 accent colors (wider gamut) */
--ll-primary: #3e9fff;
--ll-secondary: #f09637;
/* Overlay / glass */
--ll-overlay-filter: blur(1rem);
--ll-overlay-bg: hsla(0, 0%, 100%, 0.8);
--ll-overlay-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04),
0 1.625rem 3.375rem rgba(0, 0, 0, 0.04),
0 1rem 2rem rgba(0, 0, 0, 0.03),
0 0.625rem 1rem rgba(0, 0, 0, 0.024),
0 0.3125rem 0.5rem rgba(0, 0, 0, 0.02),
0 0.125rem 0.25rem rgba(0, 0, 0, 0.016),
0 0 0.125rem rgba(0, 0, 0, 0.01);
}
@supports (color: color(display-p3 1 1 1)) {
.liveline-page {
--ll-primary: color(display-p3 0.243137 0.623529 1 / 1);
--ll-secondary: color(display-p3 0.941176 0.588235 0.215686 / 1);
}
}
/* ========================================================================
Selection
======================================================================== */
.liveline-page {
font-optical-sizing: auto;
font-feature-settings: "liga" 1, "calt" 1;
}
.liveline-page ::selection {
background: var(--ll-selection-bg);
}
/* ========================================================================
Stagger-in animation
======================================================================== */
@keyframes ll-stagger-in {
0% {
opacity: 0;
transform: translateY(8px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.liveline-article > * {
animation: ll-stagger-in 0.5s ease both;
}
.liveline-article > :nth-child(1) { animation-delay: 0s; }
.liveline-article > :nth-child(2) { animation-delay: 0.05s; }
.liveline-article > :nth-child(3) { animation-delay: 0.1s; }
.liveline-article > :nth-child(4) { animation-delay: 0.15s; }
.liveline-article > :nth-child(5) { animation-delay: 0.2s; }
.liveline-article > :nth-child(6) { animation-delay: 0.25s; }
.liveline-article > :nth-child(7) { animation-delay: 0.3s; }
.liveline-article > :nth-child(8) { animation-delay: 0.35s; }
.liveline-article > :nth-child(n + 9) { animation-delay: 0.4s; }
/* ========================================================================
Header fade gradient
======================================================================== */
.liveline-page::before {
content: "";
pointer-events: none;
z-index: 9;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 8rem;
background: linear-gradient(
rgb(255, 255, 255) 0px,
rgba(255, 255, 255, 0.737) 19%,
rgba(255, 255, 255, 0.541) 34%,
rgba(255, 255, 255, 0.382) 47%,
rgba(255, 255, 255, 0.278) 56.5%,
rgba(255, 255, 255, 0.194) 65%,
rgba(255, 255, 255, 0.126) 73%,
rgba(255, 255, 255, 0.075) 80.2%,
rgba(255, 255, 255, 0.042) 86.1%,
rgba(255, 255, 255, 0.021) 91%,
rgba(255, 255, 255, 0.008) 95.2%,
rgba(255, 255, 255, 0.002) 98.2%,
rgba(255, 255, 255, 0) 100%
);
}