+36
-117
@@ -1,135 +1,54 @@
|
||||
.container {
|
||||
.page {
|
||||
height: 100vh;
|
||||
height: 100svh;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 3rem 2rem 2rem;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem 2rem;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.clockGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar:has(> *) {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.sidebarHeader {
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
.masthead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
max-width: 64rem;
|
||||
margin: 0 auto;
|
||||
padding: clamp(2.5rem, 6vw, 5rem) clamp(1.5rem, 4vw, 3rem) 2.5rem;
|
||||
border-bottom: 1.5px solid var(--ink);
|
||||
}
|
||||
|
||||
.launchButton {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #2196f3;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
.wordmark {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: clamp(2.75rem, 8vw, 5rem);
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
.readout {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: 0.08em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--ink-soft);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.main {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
.entries {
|
||||
max-width: 64rem;
|
||||
margin: 0 auto;
|
||||
padding: 0 clamp(1.5rem, 4vw, 3rem) clamp(3rem, 6vw, 5rem);
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
@media (max-width: 768px) {
|
||||
.container[data-sidebar-open="true"] {
|
||||
overflow: hidden;
|
||||
.entries > :first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.wordmark {
|
||||
font-size: clamp(2.5rem, 14vw, 3.5rem);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 2rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0 1rem 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.clockGrid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar:has(> *) {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
max-height: none;
|
||||
border-radius: 0;
|
||||
z-index: 1000;
|
||||
padding: 0;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebarHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0;
|
||||
justify-content: space-between;
|
||||
z-index: 1;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
.readout {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
+59
-86
@@ -1,97 +1,70 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import styles from './app.module.css';
|
||||
import { ClockCard } from './components/clock-card';
|
||||
import { MarkdownRenderer } from './components/markdown-renderer';
|
||||
import { ClockEntry, type ClockInfo } from './components/clock-entry';
|
||||
import { LiveTime } from './components/live-time';
|
||||
|
||||
// Per-clock preview tuning, in curated reading order. `aspect` is the frame
|
||||
// shape; `innerWidth`, when set, renders the iframe as if its viewport were that
|
||||
// many pixels wide (then scales it to fit), so clocks that branch on viewport
|
||||
// size keep their desktop layout.
|
||||
const CLOCK_META: { id: string; aspect: string; innerWidth?: number }[] = [
|
||||
{ id: 'simple', aspect: '16 / 9' },
|
||||
{ id: 'wall-o-clocks', aspect: '21 / 9' },
|
||||
{ id: 'precalculated-grid', aspect: '16 / 9' },
|
||||
{ id: 'lemniscate', aspect: '2 / 1' },
|
||||
{ id: 'spiral', aspect: '16 / 9' },
|
||||
{ id: 'custom', aspect: '4 / 3' },
|
||||
{ id: 'daylight', aspect: '16 / 9', innerWidth: 1280 },
|
||||
{ id: 'crowd', aspect: '16 / 9' },
|
||||
];
|
||||
|
||||
const DEFAULT_ASPECT = '16 / 9';
|
||||
const META = new Map(CLOCK_META.map((meta, order) => [meta.id, { ...meta, order }]));
|
||||
|
||||
function parseClock(path: string, content: string): ClockInfo | null {
|
||||
const id = path.match(/\.\/clocks\/(.+)\/README\.md/)?.[1];
|
||||
const name = content.match(/^#\s+(.+)$/m)?.[1];
|
||||
|
||||
if (!id || !name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tagline = content.match(/^>\s*(.+)$/m)?.[1];
|
||||
const body = content
|
||||
.replace(/^#\s+.+$/m, '')
|
||||
.replace(/^>\s*.+$/m, '')
|
||||
.trim();
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
tagline,
|
||||
body,
|
||||
path: `/${id}/`,
|
||||
aspect: META.get(id)?.aspect ?? DEFAULT_ASPECT,
|
||||
innerWidth: META.get(id)?.innerWidth,
|
||||
};
|
||||
}
|
||||
|
||||
const CLOCKS = Object.entries(import.meta.glob('./clocks/*/README.md', { as: 'raw', eager: true }))
|
||||
.map(([path, content]) => {
|
||||
const id = path.match(/\.\/clocks\/(.+)\/README\.md/)?.[1];
|
||||
const name = content.match(/^#\s+(.+)$/m)?.[1];
|
||||
const description = content.match(/^>\s*(.+)$/m)?.[1];
|
||||
|
||||
if (!id || !name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
path: `/${id}/`,
|
||||
};
|
||||
})
|
||||
.filter((clock) => !!clock);
|
||||
.map(([path, content]) => parseClock(path, content))
|
||||
.filter((clock): clock is ClockInfo => !!clock)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(META.get(a.id)?.order ?? CLOCK_META.length) - (META.get(b.id)?.order ?? CLOCK_META.length),
|
||||
);
|
||||
|
||||
export function App() {
|
||||
const [selectedClock, setSelectedClock] = useState<string | null>(() => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
return CLOCKS.some((c) => c.id === hash) ? hash : null;
|
||||
});
|
||||
|
||||
const selectedClockInfo = CLOCKS.find((c) => c.id === selectedClock);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedClock) {
|
||||
window.history.replaceState(null, '', `#${selectedClock}`);
|
||||
} else {
|
||||
window.history.replaceState(null, '', window.location.pathname);
|
||||
}
|
||||
}, [selectedClock]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => {
|
||||
const hash = window.location.hash.slice(1);
|
||||
if (hash && CLOCKS.some((c) => c.id === hash)) {
|
||||
setSelectedClock(hash);
|
||||
} else {
|
||||
setSelectedClock(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.container} data-sidebar-open={!!selectedClock}>
|
||||
<header className={styles.header}>
|
||||
<h1 className={styles.title}>Clocks</h1>
|
||||
<div className={styles.page}>
|
||||
<header className={styles.masthead}>
|
||||
<h1 className={styles.wordmark}>Clocks</h1>
|
||||
<LiveTime className={styles.readout} />
|
||||
</header>
|
||||
|
||||
<main className={styles.main}>
|
||||
<section className={styles.clockGrid}>
|
||||
{CLOCKS.map((clock) => (
|
||||
<ClockCard
|
||||
key={clock.id}
|
||||
clock={clock}
|
||||
isSelected={selectedClock === clock.id}
|
||||
onSelect={() => setSelectedClock(selectedClock === clock.id ? null : clock.id)}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<aside className={styles.sidebar}>
|
||||
{selectedClock && selectedClockInfo && (
|
||||
<>
|
||||
<div className={styles.sidebarHeader}>
|
||||
<a href={selectedClockInfo.path} className={styles.launchButton}>
|
||||
Launch Clock →
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedClock(null)}
|
||||
className={styles.closeButton}
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<MarkdownRenderer content={selectedClockInfo.content} />
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
<main className={styles.entries}>
|
||||
{CLOCKS.map((clock) => (
|
||||
<ClockEntry key={clock.id} clock={clock} />
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export function ArrowRight({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M2 8h11" />
|
||||
<path d="M9 4l4 4-4 4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
.card {
|
||||
appearance: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: start;
|
||||
padding: 1.5rem;
|
||||
background: #fff;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card[data-selected="true"] {
|
||||
background: #e3f2fd;
|
||||
border: 2px solid #2196f3;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import styles from './clock-card.module.css';
|
||||
|
||||
interface ClockInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface ClockCardProps {
|
||||
clock: ClockInfo;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
export function ClockCard({ clock, isSelected, onSelect }: ClockCardProps) {
|
||||
return (
|
||||
<button onClick={onSelect} className={styles.card} data-selected={isSelected} type="button">
|
||||
<h3 className={styles.name}>{clock.name}</h3>
|
||||
{!!clock.description?.length && <p className={styles.description}>{clock.description}</p>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export type { ClockInfo };
|
||||
@@ -0,0 +1,70 @@
|
||||
.entry {
|
||||
padding: clamp(2.5rem, 5vw, 4.5rem) 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1.25rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: clamp(1.7rem, 3.5vw, 2.4rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
.nameLink {
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.nameLink:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.launch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.55em;
|
||||
padding: 0.55rem 1rem;
|
||||
border: 1px solid var(--ink);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background-color 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.launch:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.launch:hover .arrow {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.tagline {
|
||||
margin: 1rem 0 0;
|
||||
font-style: italic;
|
||||
font-size: clamp(1.2rem, 2.5vw, 1.4rem);
|
||||
line-height: 1.4;
|
||||
color: var(--ink);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ArrowRight } from './arrow-right';
|
||||
import styles from './clock-entry.module.css';
|
||||
import { ClockPreview } from './clock-preview';
|
||||
import { MarkdownRenderer } from './markdown-renderer';
|
||||
|
||||
interface ClockInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
tagline?: string;
|
||||
body: string;
|
||||
path: string;
|
||||
aspect: string;
|
||||
innerWidth?: number;
|
||||
}
|
||||
|
||||
interface ClockEntryProps {
|
||||
clock: ClockInfo;
|
||||
}
|
||||
|
||||
export function ClockEntry({ clock }: ClockEntryProps) {
|
||||
return (
|
||||
<article className={styles.entry}>
|
||||
<header className={styles.head}>
|
||||
<h2 className={styles.name}>
|
||||
<a href={clock.path} className={styles.nameLink}>
|
||||
{clock.name}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<a href={clock.path} className={styles.launch}>
|
||||
Launch
|
||||
<ArrowRight className={styles.arrow} />
|
||||
</a>
|
||||
</header>
|
||||
|
||||
{clock.tagline && <p className={styles.tagline}>{clock.tagline}</p>}
|
||||
|
||||
<ClockPreview
|
||||
src={clock.path}
|
||||
title={clock.name}
|
||||
aspect={clock.aspect}
|
||||
innerWidth={clock.innerWidth}
|
||||
/>
|
||||
|
||||
<MarkdownRenderer content={clock.body} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export type { ClockInfo };
|
||||
@@ -0,0 +1,59 @@
|
||||
.frame {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: clamp(1.5rem, 3vw, 2.5rem) 0;
|
||||
background: var(--paper-plate);
|
||||
border: 1px solid var(--ink);
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.frame:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(243, 239, 230, 0.32);
|
||||
}
|
||||
|
||||
.cue {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45em;
|
||||
padding: 0.4rem 0.7rem;
|
||||
background: var(--accent);
|
||||
color: var(--paper);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.frame:hover .cue {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ArrowRight } from './arrow-right';
|
||||
import styles from './clock-preview.module.css';
|
||||
|
||||
interface ClockPreviewProps {
|
||||
src: string;
|
||||
title: string;
|
||||
aspect: string;
|
||||
innerWidth?: number;
|
||||
}
|
||||
|
||||
export function ClockPreview({ src, title, aspect, innerWidth }: ClockPreviewProps) {
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
const [active, setActive] = useState(false);
|
||||
const [frame, setFrame] = useState<{ width: number; height: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
// Mount the live clock only while it's near the viewport, and unmount it
|
||||
// once scrolled well away — keeps at most the on-screen previews running.
|
||||
const observer = new IntersectionObserver(([entry]) => setActive(entry.isIntersecting), {
|
||||
rootMargin: '40% 0px',
|
||||
});
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || !innerWidth) return;
|
||||
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
const { width, height } = entry.contentRect;
|
||||
setFrame({ width, height });
|
||||
});
|
||||
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, [innerWidth]);
|
||||
|
||||
// Render the iframe at `innerWidth` logical pixels, then scale the whole thing
|
||||
// down to fill the frame — the embedded page sees a desktop-width viewport.
|
||||
const iframeStyle =
|
||||
innerWidth && frame
|
||||
? {
|
||||
position: 'absolute' as const,
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: `${innerWidth}px`,
|
||||
height: `${(frame.height * innerWidth) / frame.width}px`,
|
||||
transform: `scale(${frame.width / innerWidth})`,
|
||||
transformOrigin: 'top left',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
href={src}
|
||||
className={styles.frame}
|
||||
aria-label={`Launch ${title}`}
|
||||
tabIndex={-1}
|
||||
style={{ aspectRatio: aspect }}
|
||||
>
|
||||
{active ? (
|
||||
<iframe
|
||||
src={src}
|
||||
title={`${title} preview`}
|
||||
className={styles.iframe}
|
||||
style={iframeStyle}
|
||||
scrolling="no"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
) : (
|
||||
<span className={styles.placeholder}>{title}</span>
|
||||
)}
|
||||
|
||||
<span className={styles.cue} aria-hidden="true">
|
||||
Launch
|
||||
<ArrowRight />
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function format(date: Date): string {
|
||||
return date.toLocaleTimeString('en-GB', { hour12: false });
|
||||
}
|
||||
|
||||
export function LiveTime({ className }: { className?: string }) {
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(new Date()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<time className={className} dateTime={now.toISOString()}>
|
||||
{format(now)}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,34 @@
|
||||
.markdown {
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
margin-top: 1.1rem;
|
||||
font-family: var(--font-serif);
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.75;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.markdown h1:first-of-type {
|
||||
max-width: calc(100% - 208px);
|
||||
}
|
||||
.markdown p {
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.markdown {
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
.markdown p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
font-size: 0.9em;
|
||||
.markdown a {
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
|
||||
.markdown a:hover {
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.markdown em {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
margin: 1rem 0;
|
||||
padding-left: 1rem;
|
||||
border-left: 3px solid #ddd;
|
||||
}
|
||||
|
||||
.markdown strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
+25
-4
@@ -1,9 +1,30 @@
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
:root {
|
||||
--paper: #f3efe6;
|
||||
--paper-plate: #0a0a0a;
|
||||
--ink: #1c1b18;
|
||||
--ink-soft: #57534a;
|
||||
--accent: #b23a2e;
|
||||
--accent-strong: #8f2b21;
|
||||
--rule: rgba(28, 27, 24, 0.16);
|
||||
|
||||
--font-serif: ui-serif, Georgia, "Iowan Old Style", "Times New Roman", serif;
|
||||
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: system-ui, monospace;
|
||||
}
|
||||
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-serif);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user