diff --git a/.gitignore b/.gitignore index d27d551..66a8c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -206,3 +206,4 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ +.impeccable/ diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..b51db49 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,261 @@ +--- +name: Ship of Theseus +description: Visualizing codebase entropy through the Ship of Theseus paradox +colors: + abyss: "oklch(2.5% 0.005 260)" + surface: "oklch(7% 0.008 260)" + surface-raised: "oklch(10% 0.01 260)" + seafoam: "oklch(68% 0.14 195)" + nebula: "oklch(52% 0.22 285)" + ember: "oklch(72% 0.16 65)" + ice: "oklch(96% 0.003 255)" + mist: "oklch(65% 0.015 255)" + frost: "oklch(45% 0.015 255)" + border: "oklch(20% 0.01 260 / 0.4)" + border-hover: "oklch(35% 0.02 260 / 0.5)" + error-bg: "oklch(20% 0.08 25 / 0.3)" + error-text: "oklch(70% 0.12 25)" +typography: + display: + fontFamily: "'Playfair Display', Georgia, serif" + fontSize: "clamp(2.5rem, 6vw, 4rem)" + fontWeight: 700 + lineHeight: 1.1 + letterSpacing: "-0.02em" + title: + fontFamily: "'Playfair Display', Georgia, serif" + fontSize: "clamp(1.5rem, 3vw, 2.2rem)" + fontWeight: 600 + lineHeight: 1.2 + letterSpacing: "normal" + body: + fontFamily: "'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace" + fontSize: "0.9rem" + fontWeight: 400 + lineHeight: 1.6 + label: + fontFamily: "'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace" + fontSize: "0.7rem" + fontWeight: 500 + lineHeight: 1.4 + letterSpacing: "0.1em" + textTransform: "uppercase" + mono: + fontFamily: "'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace" + fontSize: "0.85rem" + fontWeight: 400 + lineHeight: 1.5 +rounded: + sm: "0.5rem" + md: "1rem" + lg: "1.5rem" + xl: "2rem" + full: "9999px" +spacing: + xs: "0.5rem" + sm: "1rem" + md: "1.5rem" + lg: "2rem" + xl: "3rem" + xxl: "4rem" +components: + button-active: + backgroundColor: "{colors.seafoam}" + textColor: "{colors.abyss}" + rounded: "{rounded.sm}" + padding: "0.4rem 1rem" + button-default: + backgroundColor: "transparent" + textColor: "{colors.mist}" + rounded: "{rounded.sm}" + padding: "0.4rem 1rem" + insight-card: + backgroundColor: "{colors.surface}" + textColor: "{colors.ice}" + rounded: "{rounded.xl}" + padding: "2.5rem" + glass-panel: + backgroundColor: "{colors.surface}" + textColor: "{colors.ice}" + rounded: "{rounded.xl}" + padding: "2.5rem" + badge: + backgroundColor: "oklch(100% 0 0 / 0.03)" + textColor: "{colors.mist}" + rounded: "{rounded.full}" + padding: "0.4rem 0.9rem" + selector-pill: + backgroundColor: "oklch(100% 0 0 / 0.05)" + rounded: "{rounded.full}" + padding: "0.5rem" + fossil-card: + backgroundColor: "oklch(100% 0 0 / 0.02)" + textColor: "{colors.ice}" + rounded: "{rounded.lg}" + padding: "1.5rem 2rem" + pill-small: + backgroundColor: "oklch(100% 0 0 / 0.05)" + rounded: "{rounded.md}" + padding: "0.25rem" + code-block: + backgroundColor: "oklch(0% 0 0 / 0.4)" + rounded: "{rounded.md}" + padding: "0.6rem 1rem" + tooltip: + backgroundColor: "oklch(2.5% 0.005 260 / 0.98)" + rounded: "{rounded.md}" + padding: "1.75rem" +--- + +# Design System: Ship of Theseus + +## 1. Overview + +**Creative North Star: "The Observatory"** + +The Observatory is a place of patient observation. You stand at the instrument, peering through the lens at something vast and slow-moving: the evolution of a codebase across years. The interface around you recedes. The data is the view. Every element is designed to keep you in that state of focused attention. + +This system is dark not because tools look cool in dark mode, but because the scene demands it: a developer late at night, studying the strata of their repository on a large monitor in a dim room. The dark is planetarium dark, not nightclub dark. Surfaces are distinguished by tonal layering, not blurs or shadows. The palette is restrained by default, with a single seafoam accent providing navigational and interactive signal. + +This system explicitly rejects glassmorphism, gradient text, neon-on-black cyberpunk aesthetics, and generic SaaS dashboard conventions. + +### Key Characteristics: +- Dark tonal surfaces with subtle light steps for hierarchy +- One accent color (seafoam) for active and interactive elements +- Serif display for narrative weight, monospace body for technical precision +- Flat with tonal layering; no shadows, no blur +- Sharp, responsive state transitions +- Planetarium atmosphere, not dashboard utility + +## 2. Colors + +The palette is dark with restrained chroma. Neutrals are tinted subtly toward a cool blue hue (chroma 0.005-0.01). The single accent carries the full interactive burden: active buttons, metric highlights, links. + +### Primary +- **Seafoam** (oklch(68% 0.14 195)): The single accent. Active button states, metric values, philosophy links, help icon hover, year labels. Used sparingly; its rarity is what gives it weight. + +### Secondary +- **Nebula** (oklch(52% 0.22 285)): Secondary signal. The milestone marker star, gradient hints in fossil cards. Rare. + +### Tertiary +- **Ember** (oklch(72% 0.16 65)): Tertiary accent. Commit hash badges and fossil year highlights for warmth. + +### Neutral +- **Abyss** (oklch(2.5% 0.005 260)): Primary background. The full-canvas ground. +- **Surface** (oklch(7% 0.008 260)): Card and panel background. The raised layer. +- **Surface-raised** (oklch(10% 0.01 260)): Hovered or interactive container state. +- **Ice** (oklch(96% 0.003 255)): Primary text color. High-emphasis content. +- **Mist** (oklch(65% 0.015 255)): Secondary text, labels, muted content. +- **Frost** (oklch(45% 0.015 255)): Tertiary text, placeholder, disabled. +- **Border** (oklch(20% 0.01 260 / 0.4)): Default dividers and container borders. +- **Border-hover** (oklch(35% 0.02 260 / 0.5)): Hovered container borders. + +### Named Rules +**The Observatory Dimming Rule.** Chroma decreases as lightness approaches 0 or 100. High-chroma colors at extreme lightness values look garish on a dark canvas. Seafoam at oklch(68% 0.14) is the brightest and most saturated point; nothing exceeds it. + +**The One Accent Rule.** Seafoam is the only saturated accent. Purple and orange are used for data differentiation only (chart layers, fossil tags), never for navigation or interactive signal. + +## 3. Typography + +**Display Font:** Playfair Display (Georgia, serif fallback) +**Body Font:** JetBrains Mono (SF Mono, Cascadia Code fallback) +**Label/Mono Font:** JetBrains Mono (SF Mono, Cascadia Code fallback) + +**Character:** The pairing is deliberate: serif for philosophy and narrative weight, monospace for data and engineering precision. The serif is warm and classical; the mono is technical and sharp. They coexist without compromise, each owning its domain. + +### Hierarchy +- **Display** (Bold 700, clamp(2.5rem, 6vw, 4rem), 1.1): The main title. Hero only. One line maximum. Solid color, never gradient. +- **Title** (Semibold 600, clamp(1.5rem, 3vw, 2.2rem), 1.2): Section headings, narrative titles, fossil section headings. +- **Body** (Regular 400, 0.9rem, 1.6): All reading text, tooltip content, card descriptions. Capped at 65-75ch. +- **Label** (Medium 500, 0.7rem, 1.4, uppercase, 0.1em letter-spacing): Control labels, card titles, legend items, badge text. +- **Mono** (Regular 400, 0.85rem, 1.5): Code blocks, fossil code content, axis labels, metric values. + +## 4. Elevation + +This system uses tonal layering, not shadows or blur. Depth is conveyed purely by lightness steps: background (abyss) sits below surface, which sits below surface-raised. There are no box shadows, no backdrop filters. The absence of shadows is deliberate: it keeps the interface quiet and reduces visual noise, letting the data layers in the chart command attention. + +### Tonal Layers +- **Depth 0** (abyss): Full-canvas background. +- **Depth 1** (surface): Cards, panels, container backgrounds. +- **Depth 2** (surface-raised): Hovered containers, focused controls, the tooltip backdrop. + +### Named Rules +**The Flat-By-Default Rule.** No surface casts a shadow at rest. The only depth cue is tonal. Hovered elements may shift up one tonal layer or translate upward by 2-4px, but they never grow a shadow. + +## 5. Components + +### Buttons (Repo Selector, Mode/Scale Toggles) +- **Shape:** Sharply curved corners (0.5rem). +- **Default:** Transparent background, mist text color. Blends into the surface. +- **Active:** Seafoam background, abyss text color, no shadow. The color switch is the signal. +- **Hover:** Text transitions to ice (from mist). Active state text uses abyss (white on seafoam). +- **Transition:** 0.2s ease-out on background-color + color. + +### Chips (Selector Pills) +- **Not used as standalone chips.** The selector-pill is a group container (full border-radius, dark surface). Buttons sit inside it as described above. + +### Cards / Containers (Glass Panels, Insight Cards, Info Cards) +- **Corner Style:** Generous curve (2rem). +- **Background:** Surface tonal layer (oklch(7% 0.008 260)). +- **Shadow Strategy:** None. Flat by default. +- **Border:** 1px solid border color. On hover, border transitions to border-hover. +- **Internal Padding:** 2.5rem (insight card), 1.5rem (info card), 3rem (fossil finder). +- **Hover:** Container translates up 4-8px with same easing, border color shifts to border-hover. + +### Inputs / Fields +- None in current scope. The chart interaction is pointer-based, not form-driven. + +### Navigation +- Not applicable. Single-page application with no navigation chrome. The repo selector bar serves as navigation. + +### Fossil Cards +- **Corner Style:** Curved corners (1.5rem). +- **Background:** Slightly transparent (oklch(100% 0 0 / 0.02)). +- **Border:** 1px solid subtle white (rgba(255,255,255,0.1)). On hover, border shifts toward seafoam. +- **Hover:** 4px upward translate, seafoam glow via box-shadow (no blur backdrop). +- **Padding:** 1.5rem top/bottom, 2rem sides. + +### Tooltip (Chart Hover) +- **Background:** Near-opaque abyss (oklch(2.5% 0.005 260 / 0.98)). +- **Border:** 1px solid border color. +- **Corner Style:** 1.25rem. +- **Padding:** 1.75rem. +- **Width:** Minimum 340px. +- **Shadow:** Strong diffuse shadow (box-shadow, not backdrop-filter) for separation from the chart. + +### Code Block (Fossil Content) +- **Background:** Near-black (oklch(0% 0 0 / 0.4)). +- **Border:** 1px solid border color. +- **Corner Style:** 0.75rem. +- **Font:** Mono, 0.85rem. +- **Prefix:** Seafoam `>` character (opacity 0.5). + +### Badge (Author Badge) +- **Shape:** Full pill (9999px). +- **Background:** Near-white at 3% opacity. +- **Border:** 1px solid border color. +- **Text:** Mist, uppercase, 0.6rem, letter-spacing 0.15em. +- **Hover:** Background intensifies, border lightens, text shifts to ice. + +## 6. Do's and Don'ts + +### Do: +- **Do** use tonal layering for depth: abyss, surface, surface-raised. No shadows, no blurs. +- **Do** use seafoam as the single interactive accent. Its rarity is the source of its signal. +- **Do** use Playfair Display for narrative, JetBrains Mono for data. Never mix them in the same element. +- **Do** use solid text colors. The main title must never use gradient text. +- **Do** use the flat-by-default rule. Surfaces sit flat until hovered. +- **Do** cap body text at 65-75 characters per line. +- **Do** use ease-out (quart or quint) for transitions. No bounce, no elastic. + +### Don't: +- **Don't** use glassmorphism. No backdrop-filter: blur on any surface. The planetarium is not a kaleidoscope. +- **Don't** use gradient text (background-clip: text with gradient). The title is one solid color. +- **Don't** use side-stripe borders. No border-left or border-right over 1px as decorative accent. +- **Don't** replicate the hero-metric template (big number, small label, gradient accent). +- **Don't** create identical card grids with icon + heading + text patterns. +- **Don't** use cyberpunk or hacker aesthetics. No neon-on-black, no glowing borders, no scanlines. +- **Don't** use generic AI slop patterns. If it looks like it was templated, redesign it. +- **Don't** use em dashes. Use commas, colons, or periods. +- **Don't** use `#000` or `#fff`. Tint every neutral toward cool blue (chroma 0.005-0.01). +- **Don't** animate CSS layout properties. Use transform and opacity only. diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..f988b68 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,46 @@ +# Product + +## Register + +product + +## Users + +An intersection of software engineers, open-source maintainers, and philosophy-curious developers. They range from engineering leads assessing codebase health to solo developers exploring how their projects evolve over time. Their context is analytical but reflective — they're not just crunching numbers, they're contemplating the nature of software identity. + +## Product Purpose + +Visualize codebase entropy through the lens of the Ship of Theseus paradox. Every month, the engine snapshots which lines of code survive and which are replaced, rendering a stacked area chart that shows the age composition of a repository over time. Success means giving users a visceral, data-driven understanding of how their codebases are constantly being reborn. + +## Brand Personality + +Atmospheric, bold, narrative. The tone is confident and thoughtful — a data scientist who also reads philosophy. Not academic, not bro-y. Think observatory dark, not cave dark. The interface should feel like peering through a telescope at codebase evolution, not like monitoring server alerts. + +## Anti-references + +- Generic AI slop patterns +- Glassmorphism as a decorative default +- Cyberpunk / hacker / neon-on-black aesthetics +- Generic SaaS dashboard conventions (hero-metric template, identical card grids, side-stripe borders) +- Gradient text +- Overly playful or childish UI patterns + +## Design Principles + +1. **Narrative through data** — The visualization tells a story of codebase evolution. Every element should serve that narrative, not distract from it. The chart is the hero; everything else supports it. + +2. **Philosophical depth, not decoration** — The Ship of Theseus is an ancient paradox. The interface should feel considered and intentional, not assembled from a template. Every visual choice should have a reason rooted in the concept. + +3. **Atmosphere with restraint** — Bold and atmospheric is the goal, but not through glass, gradients, or neon. Use typography, spacing, light, and subtle color to create depth. The dark should feel like a planetarium, not a nightclub. + +4. **Precision earns boldness** — Small details (tooltips, transitions, skeleton states) should be meticulously crafted. That precision is what earns the right to be bold with scale, typography, and the narrative voice. + +5. **Accessibility is craft** — WCAG AA minimum, prefer AAA. Contrast, focus states, reduced motion, and screen reader support are not checkboxes — they are part of the design. Atmospheric does not mean inaccessible. + +## Accessibility & Inclusion + +- WCAG AA minimum, prefer AAA where practical +- Support reduced motion preferences +- Ensure sufficient color contrast for all data visualization layers +- Keyboard-navigable controls and chart interaction +- Screen reader friendly tooltips and data descriptions diff --git a/app.js b/app.js index b1f8636..daad81a 100644 --- a/app.js +++ b/app.js @@ -24,7 +24,10 @@ class TheseusVisualizer { this.vizMode = 'chronological'; // 'chronological' | 'identity' this.yScaleMode = 'linear'; // 'linear' | 'log' this.fossils = {}; + this.reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + this.animDuration = this.reducedMotion ? 0 : 800; + this.focusIndex = undefined; this.init(); } @@ -50,6 +53,8 @@ class TheseusVisualizer { } window.addEventListener('resize', () => this.debouncedRender()); + this.setupKeyboardShortcuts(); + this.setupRepoRequest(); } setupModeToggle() { @@ -78,6 +83,108 @@ class TheseusVisualizer { }); } + setupKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + if (['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) return; + if (e.metaKey || e.ctrlKey || e.altKey) return; + + const key = e.key; + + if (key >= '1' && key <= '9') { + const idx = parseInt(key, 10) - 1; + const btns = document.querySelectorAll('.repo-btn'); + if (idx < btns.length) { + btns[idx].click(); + } + return; + } + + if (key === 'm' || key === 'M') { + const active = document.querySelector('.mode-btn.active'); + if (active) { + const next = active.nextElementSibling || active.previousElementSibling; + if (next) next.click(); + } + return; + } + + if (key === 's' || key === 'S') { + const active = document.querySelector('.scale-btn.active'); + if (active) { + const next = active.nextElementSibling || active.previousElementSibling; + if (next) next.click(); + } + return; + } + }); + } + + setupRepoRequest() { + const form = document.getElementById('repo-request-form'); + const input = document.getElementById('repo-url'); + const feedback = document.getElementById('repo-request-feedback'); + if (!form || !input || !feedback) return; + + function setFeedback(message, type) { + feedback.textContent = message; + feedback.className = 'repo-request-feedback' + (type ? ' ' + type : ''); + feedback.classList.toggle('visible', !!message); + } + + function validateUrl(raw) { + const cleaned = raw + .replace(/^https?:\/\//, '') + .replace(/^www\./, '') + .replace(/\/$/, '') + .replace(/^github\.com\//, ''); + return /^[\w.-]+\/[\w.-]+$/.test(cleaned) ? cleaned : null; + } + + input.addEventListener('input', () => { + input.classList.remove('invalid'); + if (feedback.textContent) setFeedback(''); + }); + + form.addEventListener('submit', (e) => { + e.preventDefault(); + const raw = input.value.trim(); + if (!raw) { + input.classList.add('invalid'); + setFeedback('Enter a repository URL or owner/repo name.', 'error'); + return; + } + + const slug = validateUrl(raw); + if (!slug) { + input.classList.add('invalid'); + setFeedback('Enter a valid GitHub URL (e.g. owner/repo)', 'error'); + return; + } + + const title = `Repository request: ${slug}`; + const body = [ + `**Repository:** ${raw}`, + '', + '---', + 'Submitted via the Ship of Theseus dashboard.', + ].join('\n'); + + const url = new URL('https://github.com/Asifdotexe/Theseus/issues/new'); + url.searchParams.set('title', title); + url.searchParams.set('body', body); + url.searchParams.set('labels', 'repository request'); + + const popup = window.open(url.toString(), '_blank'); + if (!popup || popup.closed || typeof popup.closed === 'undefined') { + setFeedback('Popup was blocked. Allow popups for this site and try again.', 'error'); + return; + } + + setFeedback('Request submitted. I will look into it soon.', 'success'); + input.value = ''; + }); + } + renderSelectors() { this.repoSelector.innerHTML = ''; this.manifest.forEach(repo => { @@ -167,16 +274,34 @@ class TheseusVisualizer { const chartHeight = height - this.margin.top - this.margin.bottom; const svg = d3.select(this.canvas); - svg.selectAll("*").remove(); + const needsBuild = !this._built; + const dimsChanged = this._chartWidth !== chartWidth || this._chartHeight !== chartHeight; + + this.focusIndex = undefined; + + // — Structural phase (first time, resize, or repo switch) — + if (needsBuild || dimsChanged) { + svg.selectAll("*").remove(); + svg.append("defs"); + this._g = svg.append("g") + .attr("class", "chart-container") + .attr("transform", `translate(${this.margin.left},${this.margin.top})`); + this._chartWidth = chartWidth; + this._chartHeight = chartHeight; + this._built = true; + } + + const g = this._g; - // Containers - const g = svg.append("g") - .attr("transform", `translate(${this.margin.left},${this.margin.top})`); + // — Clear data-driven children (keep g itself, defs, and their attributes) — + g.selectAll("g.axis-y, g.axis-x, .milestone-marker, path.layer, .scrubber-line, rect[fill='transparent'], text.axis-label").remove(); + svg.select("defs").selectAll("linearGradient").remove(); - // Scales + // — Scales — const xScale = d3.scaleTime() .domain(d3.extent(this.points, d => d.date)) .range([0, chartWidth]); + this.xScale = xScale; const maxTotal = d3.max(this.points, d => d.total); let yScale; @@ -191,34 +316,29 @@ class TheseusVisualizer { .range([chartHeight, 0]); } - // Color Logic & Gradients - const defs = svg.append("defs"); - + // — Gradients — + const defs = svg.select("defs"); const getBaseColor = (seriesName, seriesIndex) => { if (this.vizMode === 'identity') { - return (seriesIndex === 0) ? '#3bc7c7' : '#f0a33b'; + return (seriesIndex === 0) ? 'oklch(68% 0.14 195)' : 'oklch(72% 0.16 65)'; } const yearIdx = this.years.indexOf(seriesName); - return `hsl(${(180 + yearIdx * 40) % 360}, 85%, 70%)`; + return `oklch(70% 0.14 ${(195 + yearIdx * 36) % 360})`; }; - // Create gradients for each series - const seriesKeys = this.vizMode === 'identity' ? [this.years[0], 'refactored'] : this.years; this.years.forEach((year, i) => { const color = getBaseColor(year, i); const grad = defs.append("linearGradient") .attr("id", `grad-${year}`) .attr("x1", "0%").attr("y1", "0%") .attr("x2", "0%").attr("y2", "100%"); - grad.append("stop").attr("offset", "0%").attr("stop-color", color).attr("stop-opacity", 0.9); grad.append("stop").attr("offset", "100%").attr("stop-color", color).attr("stop-opacity", 0.4); }); - // Specialized gradients for Identity mode if needed if (this.vizMode === 'identity') { ['original', 'refactored'].forEach(id => { - const color = id === 'original' ? '#3bc7c7' : '#f0a33b'; + const color = id === 'original' ? 'oklch(68% 0.14 195)' : 'oklch(72% 0.16 65)'; const grad = defs.append("linearGradient") .attr("id", `grad-id-${id}`) .attr("x1", "0%").attr("y1", "0%") @@ -228,7 +348,7 @@ class TheseusVisualizer { }); } - // Stack & Area + // — Stack & Area (data join) — const stackGenerator = d3.stack() .keys(this.years); @@ -240,7 +360,6 @@ class TheseusVisualizer { .y1(d => yScale(this.yScaleMode === 'log' ? Math.max(1, d[1]) : d[1])) .curve(d3.curveMonotoneX); - // Render Layers (Data Join) const layers = g.selectAll(".layer") .data(stackedData, d => d.key); @@ -259,21 +378,22 @@ class TheseusVisualizer { .attr("d", areaGenerator) .style("opacity", 0) .transition() - .duration(800) + .duration(this.animDuration) + .delay((d, i) => this.reducedMotion ? 0 : i * 50) .style("opacity", 1); layers.transition() - .duration(800) + .duration(this.animDuration) .attr("d", areaGenerator) .attr("fill", getFill); layers.exit().remove(); - // Interaction Components (Legend, Axes, Scrubber) - this.renderLegend(); + // — Axes, Milestones, Interaction, Legend — this.renderAxes(g, chartWidth, chartHeight, xScale, yScale); this.renderMilestoneMarkers(g, chartWidth, chartHeight, xScale); this.setupInteractivity(g, chartWidth, chartHeight, xScale, yScale); + this.renderLegend(); } renderMilestoneMarkers(g, chartWidth, chartHeight, xScale) { @@ -290,6 +410,9 @@ class TheseusVisualizer { const marker = g.append('g') .attr('class', 'milestone-marker') .attr('transform', `translate(${xPos}, 0)`) + .attr('tabindex', '0') + .attr('role', 'button') + .attr('aria-label', `${m.title}: ${m.description}`) .style('cursor', 'pointer'); marker.append('text') @@ -297,73 +420,113 @@ class TheseusVisualizer { .attr('y', 18) .attr('text-anchor', 'middle') .attr('font-size', '14px') - .attr('fill', '#3bc7c7') + .attr('fill', 'oklch(68% 0.14 195)') .text('★') .style('opacity', 0.8) - .style('filter', 'drop-shadow(0 0 4px rgba(59, 199, 199, 0.6))'); + .style('filter', 'drop-shadow(0 0 4px oklch(68% 0.14 195 / 0.6))'); marker.append('title') .text(m.title + ': ' + m.description); - marker.on('mouseenter', function () { + const animDur = this.reducedMotion ? 0 : 200; + + const enlarge = function () { d3.select(this).select('text') .transition() - .duration(200) + .duration(animDur) .attr('font-size', '18px') .style('opacity', 1); - }); + }; - marker.on('mouseleave', function () { + const shrink = function () { d3.select(this).select('text') .transition() - .duration(200) + .duration(animDur) .attr('font-size', '14px') .style('opacity', 0.8); - }); + }; + + marker.on('mouseenter', enlarge) + .on('mouseleave', shrink) + .on('focus', enlarge) + .on('blur', shrink); } }); } renderLegend() { this.legend.innerHTML = ''; - const items = this.vizMode === 'identity' - ? [{ label: 'Original Code', color: '#3bc7c7' }, { label: 'Refactored', color: '#f0a33b' }] - : this.years.map((y, i) => ({ label: y, color: `hsl(${(180 + i * 40) % 360}, 85%, 70%)` })); + + let items; + if (this.vizMode === 'identity') { + items = [{ label: 'Original Code', color: 'oklch(68% 0.14 195)' }, { label: 'Refactored', color: 'oklch(72% 0.16 65)' }]; + } else { + items = this.years.map((y, i) => ({ label: y, color: `oklch(70% 0.14 ${(195 + i * 36) % 360})` })); + } + + // Trigger pill + const years = this.years; + const triggerText = this.vizMode === 'identity' + ? 'Original + Refactored · 2 layers' + : `${years[0]}–${years[years.length - 1]} · ${years.length} years`; + + const trigger = document.createElement('button'); + trigger.className = 'legend-trigger'; + trigger.setAttribute('aria-expanded', 'false'); + trigger.setAttribute('aria-label', `Legend: ${triggerText}. Hover or focus to expand.`); + trigger.innerHTML = `${triggerText}`; + this.legend.appendChild(trigger); + + // Panel + const panel = document.createElement('div'); + panel.className = 'legend-panel'; + panel.setAttribute('role', 'tooltip'); + this.legend.appendChild(panel); items.forEach(item => { - const div = document.createElement('div'); - div.className = 'legend-item'; - div.style.cursor = 'pointer'; - div.innerHTML = ` - - ${item.label} - `; - - div.onmouseenter = () => { + const el = document.createElement('div'); + el.className = 'legend-item'; + el.innerHTML = `${item.label}`; + + el.onmouseenter = () => { const label = item.label; - const firstYear = this.years[0]; + const firstYear = years[0]; - d3.selectAll(".chart-area").style("opacity", 0.1); + d3.selectAll('.chart-area').style('opacity', 0.1); if (this.vizMode === 'identity') { if (label === 'Original Code') { - d3.selectAll(`.chart-area[data-year='${firstYear}']`).style("opacity", 1); + d3.selectAll(`.chart-area[data-year='${firstYear}']`).style('opacity', 1); } else { - // All years except the first one - d3.selectAll(".chart-area") - .filter(function () { return d3.select(this).attr("data-year") !== firstYear; }) - .style("opacity", 1); + d3.selectAll('.chart-area') + .filter(function () { return d3.select(this).attr('data-year') !== firstYear; }) + .style('opacity', 1); } } else { - d3.selectAll(`.chart-area[data-year='${label}']`).style("opacity", 1); + d3.selectAll(`.chart-area[data-year='${label}']`).style('opacity', 1); } }; - div.onmouseleave = () => { - d3.selectAll(".chart-area").style("opacity", 1); + el.onmouseleave = () => { + d3.selectAll('.chart-area').style('opacity', 1); }; - this.legend.appendChild(div); + panel.appendChild(el); + }); + + // Toggle aria-expanded on hover + const toggleExpanded = (expanded) => { + trigger.setAttribute('aria-expanded', String(expanded)); + }; + + this.legend.addEventListener('mouseenter', () => toggleExpanded(true)); + this.legend.addEventListener('mouseleave', () => toggleExpanded(false)); + + this.legend.addEventListener('focusin', (e) => { + if (this.legend.contains(e.target)) toggleExpanded(true); + }); + this.legend.addEventListener('focusout', (e) => { + if (!this.legend.contains(e.relatedTarget)) toggleExpanded(false); }); } @@ -383,13 +546,13 @@ class TheseusVisualizer { .call(yAxis); yGroup.selectAll(".tick line") - .attr("stroke", "#374151") + .attr("stroke", "oklch(30% 0.01 260)") .attr("stroke-dasharray", "3,3") .attr("stroke-opacity", 0.5); yGroup.selectAll("text") .attr("x", -10) - .attr("fill", "#6b7280") + .attr("fill", "oklch(55% 0.015 255)") .attr("font-size", "10px") .attr("font-family", "inherit"); @@ -407,20 +570,20 @@ class TheseusVisualizer { xGroup.selectAll("text") .attr("y", 15) - .attr("fill", "#8b949e") + .attr("fill", "oklch(55% 0.015 255)") .attr("font-size", "11px") .attr("letter-spacing", "0.05em") .attr("font-family", "inherit"); - xGroup.select(".domain").attr("stroke", "rgba(255, 255, 255, 0.1)"); - xGroup.selectAll(".tick line").attr("stroke", "rgba(255, 255, 255, 0.1)"); + xGroup.select(".domain").attr("stroke", "oklch(100% 0 0 / 0.1)"); + xGroup.selectAll(".tick line").attr("stroke", "oklch(100% 0 0 / 0.1)"); // Axis Labels g.append("text") .attr("class", "axis-label") .attr("x", width / 2) .attr("y", height + 40) - .attr("fill", "#6b7280") + .attr("fill", "oklch(55% 0.015 255)") .attr("font-size", "12px") .attr("text-anchor", "middle") .text("Time"); @@ -430,7 +593,7 @@ class TheseusVisualizer { .attr("transform", "rotate(-90)") .attr("x", -height / 2) .attr("y", -45) - .attr("fill", "#6b7280") + .attr("fill", "oklch(55% 0.015 255)") .attr("font-size", "12px") .attr("text-anchor", "middle") .text("Lines of Code"); @@ -441,21 +604,25 @@ class TheseusVisualizer { .attr("class", "scrubber-line hidden") .attr("y1", 0) .attr("y2", height) - .attr("stroke", "rgba(255,255,255,0.2)") + .attr("stroke", "oklch(100% 0 0 / 0.2)") .attr("stroke-width", 1); - const bisect = d3.bisector(d => d.date).left; + const self = this; + g.append("rect") .attr("width", width) .attr("height", height) .attr("fill", "transparent") - .on("mousemove", (event) => { + .attr("tabindex", "0") + .attr("role", "img") + .attr("aria-label", "Chart of code composition by year. Use Arrow Left and Arrow Right to navigate data points.") + .on("mousemove", function (event) { const mouseX = d3.pointer(event)[0]; const date = xScale.invert(mouseX); - const idx = bisect(this.points, date, 1); - const d0 = this.points[idx - 1]; - const d1 = this.points[idx]; + const idx = bisect(self.points, date, 1); + const d0 = self.points[idx - 1]; + const d1 = self.points[idx]; // Handle single-point or edge cases if (!d0 && !d1) return; @@ -467,17 +634,47 @@ class TheseusVisualizer { const snappedX = xScale(d.date); scrubber.attr("x1", snappedX).attr("x2", snappedX).classed("hidden", false); - const svgRect = this.canvas.getBoundingClientRect(); - this.showTooltip(d, snappedX + this.margin.left, d3.pointer(event)[1] + this.margin.top); + const svgRect = self.canvas.getBoundingClientRect(); + self.showTooltip(d, snappedX + self.margin.left, d3.pointer(event)[1] + self.margin.top); }) .on("mouseleave", () => { - this.hideTooltip(); + self.hideTooltip(); + scrubber.classed("hidden", true); + }) + .on("focus", () => { + if (self.focusIndex === undefined || self.focusIndex >= self.points.length) { + self.focusIndex = self.points.length - 1; + } + self.updateFocusPoint(xScale, scrubber); + }) + .on("blur", () => { + self.hideTooltip(); scrubber.classed("hidden", true); + }) + .on("keydown", (event) => { + if (event.key === "ArrowLeft" || event.key === "ArrowRight") { + event.preventDefault(); + const len = self.points.length; + if (len === 0) return; + + if (self.focusIndex === undefined) { + self.focusIndex = len - 1; + } + + if (event.key === "ArrowLeft") { + self.focusIndex = Math.max(0, self.focusIndex - 1); + } else { + self.focusIndex = Math.min(len - 1, self.focusIndex + 1); + } + + self.updateFocusPoint(xScale, scrubber); + } }); } showTooltip(point, x, y) { this.tooltip.classList.remove('hidden'); + this.tooltip.setAttribute('role', 'tooltip'); const dateStr = point.date instanceof Date ? point.date.toISOString().split('T')[0] @@ -526,7 +723,7 @@ class TheseusVisualizer {
- + Foundation (${foundationYear})
@@ -536,7 +733,7 @@ class TheseusVisualizer {
- + Refactored
@@ -547,7 +744,7 @@ class TheseusVisualizer { ${!isFoundationAlive && oldestSurvivingYear && oldestSurvivingYear !== foundationYear ? `
- + Oldest surviving (${oldestSurvivingYear})
@@ -582,6 +779,16 @@ class TheseusVisualizer { this.tooltip.classList.add('hidden'); } + updateFocusPoint(xScale, scrubber) { + const d = this.points[this.focusIndex]; + if (!d) return; + + const snappedX = xScale(d.date); + scrubber.attr("x1", snappedX).attr("x2", snappedX).classed("hidden", false); + + this.showTooltip(d, snappedX + this.margin.left, this.tooltip.offsetHeight + 20); + } + updateInsights() { if (!this.points || this.points.length === 0) return; const first = this.points[0]; @@ -619,7 +826,7 @@ class TheseusVisualizer { let totalAge = 0; this.years.forEach(y => { const lines = last[y] || 0; - const age = currentYear - parseInt(y); + const age = currentYear - parseInt(y, 10); totalAge += lines * age; }); const meanAge = totalAge / totalLines; @@ -632,7 +839,7 @@ class TheseusVisualizer { let peakYear = '--'; let peakVal = 0; this.years.forEach(y => { - if (parseInt(y) < currentYear) { + if (parseInt(y, 10) < currentYear) { const val = last[y] || 0; if (val > peakVal) { peakVal = val; @@ -704,6 +911,31 @@ class TheseusVisualizer { document.getElementById('survivor-content').textContent = survivor.content ? survivor.content.trim() : 'No fossil data'; document.getElementById('survivor-commit').textContent = survivor.view_commit || survivor.commit || ''; + // Keyboard interaction for fossil card divs + ['fossil-genesis', 'fossil-survivor'].forEach(id => { + const card = document.getElementById(id); + if (!card || card.dataset.fossilInited) return; + card.dataset.fossilInited = 'true'; + card.setAttribute('tabindex', '0'); + card.setAttribute('role', 'link'); + + const openLink = () => { + const link = card.querySelector('.fossil-link'); + if (link && link.href) window.open(link.href, '_blank'); + }; + + card.addEventListener('click', (e) => { + if (e.target.closest('.fossil-link')) return; + openLink(); + }); + + card.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + openLink(); + } + }); + }); } createSVGElement(tag, attrs = {}) { diff --git a/index.html b/index.html index 1324b90..5fbd52e 100644 --- a/index.html +++ b/index.html @@ -1,296 +1,380 @@ + + + + Ship of Theseus | Code Visualizer + + + + + + + + - - - - Ship of Theseus | Code Visualizer - - - - - - - - + +
- -
- -
-
-
Project by Asif Sayyed
-

The Ship of Theseus

-

- Track how much of your codebase's original code still - survives over time. -

+
+
+
Project by Asif Sayyed
+

The Ship of Theseus

+

+ Track how much of your codebase's original code still + survives over time. +

-
-
- -
+
+
+ +
-
-
- Mode: -
- - +
+
+ Mode: +
+ + +
-
-
- Scale: -
- - +
+ Scale: +
+ + +
-
-

-
+

+
-
- -
- -
-
-

How to read this chart

-

- The X-axis shows time. The Y-axis shows total lines of - code. Each colored band represents code that was - originally written in a specific year. -

-
-
-

How the data is collected

-

- Every month, we analyze the repository and use git blame - to determine when each line was last modified. This - gives us a snapshot of how much original code survives - over time. -

-
-
- -
-

Ancient Code Fragments

-

Click the file path below to view on GitHub

-
-
-
- Historical Fossil - ---- -
-
- -- - -
-
- Loading... +
+

+ How old is the code? + ? +

+
+
+

--

+

Repository birth year

+
+
+

+ -- +

+

Earliest code still here

+
-

The oldest line ever written in this repo's history, even if deleted long ago.

-
-
- Living Fossil - ---- +
+

+ When was the biggest rewrite? + ? +

+
+

--

+

Biggest rewrite date

-
- -- - +
+
+

+ Which year's code survives most? + ? +

+
+

--

+

+ Year with most surviving code +

-
- Loading... +
+
+

+ What's the average code age? + ? +

+
+

--

+

Average code age

-

The oldest line that is still alive in the codebase today.

-
-
+ -
-

Where did this all come from?

-
-

- Honestly, I'm just a guy who spent a bit too much time - reading Plato and not enough time touching grass. This - project is basically what happens when you combine a bit - of a philosophy obsession with a healthy dose of data - engineering. I've always felt that data isn't just - numbers in a JSON file, it's a living record of - evolution, like a digital ancestry. +

+

+ + Request a repository +

+

+ Don't see yours? Drop the URL below and I'll add it to the + tracker.

- -

- I wanted to see if I could apply the - Ship of Theseus - paradox to software. If you haven't heard of it, it's an - ancient Greek thought experiment that asks: if you - replace every single part of a ship, plank by plank, is - it still the same ship? Or is it just a new ship wearing - its ancestor's name tag? +

+
+ + + Full URL or owner/repo format +
+ +

+
+

+ Opens a pre-filled issue on GitHub. No data leaves your + browser.

- -

- We do this to codebases all the time. We refactor, - delete, and rewrite until the original 2013 'timber' is - long gone. This tool is my way of staring at that - Identity Problem without having to - write a 50-page thesis. It gives us a window into how - our projects are constantly being reborn. Is it still - the same repo? I have no idea, but the data is - fascinating, and looking at entropy is better than - staring at a blank terminal. +

+
+

Ancient Code Fragments

+

+ Click the file path below to view on GitHub

- -

- If you find this digital paradox as fascinating as I do, - consider dropping a ⭐ on - GitHub. - It helps keep the ship afloat! -

- -
- — Asif Sayyed
- Data Scientist who also happens to read far too - much philosophy +
+
+
+ Historical Fossil + ---- +
+
+ -- + +
+
+ Loading... +
+

+ The oldest line ever written in this repo's history, + even if deleted long ago. +

+
+
+
+ Living Fossil + ---- +
+
+ -- + +
+
+ Loading... +
+

+ The oldest line that is still alive in the codebase + today. +

+
-
-
+
-
- -
-
+
+
+ + Where did this all come from? + +
+

+ Built by Asif Sayyed, a data scientist wondering if + the + Ship of Theseus + paradox applies to codebases, if you replace every + line over time, is it still the same repo? This tool + tracks how much original code survives as projects + evolve. +

+
+ Asif Sayyed
+ Data Scientist who also happens to read far too + much philosophy +
+
+
+
- - +
+ +
+ + + diff --git a/style.css b/style.css index 75dff70..c4f01c9 100644 --- a/style.css +++ b/style.css @@ -1,13 +1,39 @@ :root { - --bg-dark: #0a0a0c; - --accent-cyan: #3bc7c7; - --accent-purple: #8b5cf6; - --accent-orange: #f0a33b; - --text-primary: #f8fafc; - --text-secondary: #94a3b8; - --glass-border: rgba(255, 255, 255, 0.08); - --font-serif: 'Playfair Display', serif; - --font-mono: 'JetBrains Mono', monospace; + --abyss: oklch(2.5% 0.005 260); + --surface: oklch(7% 0.008 260); + --surface-raised: oklch(10% 0.01 260); + --seafoam: oklch(68% 0.14 195); + --nebula: oklch(52% 0.22 285); + --ember: oklch(72% 0.16 65); + --ice: oklch(96% 0.003 255); + --mist: oklch(65% 0.015 255); + --frost: oklch(45% 0.015 255); + --border: oklch(20% 0.01 260 / 0.4); + --border-hover: oklch(35% 0.02 260 / 0.5); + --error-bg: oklch(20% 0.08 25 / 0.3); + --error-text: oklch(70% 0.12 25); + --font-serif: 'Playfair Display', Georgia, serif; + --font-mono: 'JetBrains Mono', 'SF Mono', 'Cascadia Code', monospace; + --text-xs: 0.65rem; + --text-sm: 0.85rem; + --weight-semibold: 600; + --space-xs: 0.5rem; + --space-sm: 1rem; + --space-md: 1.5rem; + --space-lg: 2rem; + --space-xl: 3rem; + --space-xxl: 4rem; + --radius-sm: 0.5rem; + --radius-md: 1rem; + --radius-lg: 1.5rem; + --radius-xl: 2rem; + --radius-full: 9999px; + --ease-out-quart: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); + --duration-instant: 150ms; + --duration-short: 200ms; + --duration-medium: 400ms; + --duration-long: 800ms; } .visually-hidden { @@ -31,16 +57,16 @@ } body { - background-color: var(--bg-dark); - color: var(--text-primary); + background-color: var(--abyss); + color: var(--ice); font-family: var(--font-mono); + font-size: 0.9rem; line-height: 1.6; margin: 0; overflow-x: hidden; min-height: 100vh; } -/* Noise overlay */ .noise-overlay { position: fixed; top: 0; @@ -55,87 +81,82 @@ body { .container { max-width: 1400px; margin: 0 auto; - padding: 4rem 2rem; + padding: var(--space-xxl) var(--space-lg); display: flex; flex-direction: column; - gap: 3rem; + gap: var(--space-xl); } -/* Hero Section */ .hero { text-align: center; display: flex; flex-direction: column; align-items: center; - gap: 1.5rem; - /* Reduced gap to accommodate badge */ + gap: var(--space-md); } .author-badge { display: inline-block; padding: 0.4rem 0.9rem; - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--glass-border); - border-radius: 9999px; + background: oklch(100% 0 0 / 0.03); + border: 1px solid var(--border); + border-radius: var(--radius-full); font-family: var(--font-mono); font-size: 0.6rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.15em; - color: var(--text-secondary); - transition: all 0.3s ease; + color: var(--mist); cursor: default; + transition: background var(--duration-short) var(--ease-out-quart), + border-color var(--duration-short) var(--ease-out-quart), + color var(--duration-short) var(--ease-out-quart), + transform var(--duration-short) var(--ease-out-quart); } .author-badge:hover { - opacity: 1; - background: rgba(255, 255, 255, 0.06); - border-color: rgba(255, 255, 255, 0.2); - color: var(--text-primary); + background: oklch(100% 0 0 / 0.06); + border-color: var(--border-hover); + color: var(--ice); transform: translateY(-1px); } .title { font-family: var(--font-serif); - font-size: 4rem; + font-size: clamp(2.5rem, 6vw, 4rem); font-weight: 700; letter-spacing: -0.02em; + line-height: 1.1; margin: 0; - background: linear-gradient(to bottom, #fff, #94a3b8); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--ice); } .subtitle { font-size: 1.1rem; - color: var(--text-secondary); + color: var(--mist); max-width: 600px; } - -/* Selector Pill */ .selector-pill { display: flex; - gap: 0.5rem; - padding: 0.5rem; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--glass-border); - border-radius: 9999px; - backdrop-filter: blur(8px); + gap: var(--space-xs); + padding: var(--space-xs); + background: oklch(100% 0 0 / 0.05); + border: 1px solid var(--border); + border-radius: var(--radius-full); } .controls-bar { display: flex; flex-direction: column; align-items: center; - gap: 1.5rem; + gap: var(--space-md); width: 100%; } .settings-row { display: flex; - gap: 2rem; + gap: var(--space-lg); flex-wrap: wrap; justify-content: center; align-items: center; @@ -151,35 +172,37 @@ body { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; - color: var(--text-secondary); + color: var(--mist); opacity: 0.8; } .selector-pill.small { padding: 0.25rem; gap: 0.25rem; - border-radius: 0.75rem; + border-radius: var(--radius-md); } .repo-btn, .mode-btn, .scale-btn { padding: 0.75rem 1.5rem; - border-radius: 9999px; + border-radius: var(--radius-full); border: none; background: transparent; - color: var(--text-secondary); + color: var(--mist); font-family: var(--font-mono); font-size: 0.9rem; cursor: pointer; - transition: all 0.3s ease; + transition: color var(--duration-instant) var(--ease-out-quart), + background-color var(--duration-instant) var(--ease-out-quart), + transform var(--duration-instant) var(--ease-out-quart); } .mode-btn, .scale-btn { padding: 0.4rem 1rem; font-size: 0.75rem; - border-radius: 0.5rem; + border-radius: var(--radius-sm); text-transform: uppercase; letter-spacing: 0.05em; } @@ -187,55 +210,62 @@ body { .repo-btn:hover, .mode-btn:hover, .scale-btn:hover { - opacity: 1; - color: var(--text-primary); + color: var(--ice); + transform: translateY(-1px); +} + +.repo-btn:active, +.mode-btn:active, +.scale-btn:active { + transform: scale(0.97); +} + +.repo-btn:focus-visible, +.mode-btn:focus-visible, +.scale-btn:focus-visible { + outline: 2px solid var(--seafoam); + outline-offset: 2px; } .repo-btn.active, .mode-btn.active, .scale-btn.active { - background: var(--accent-cyan); - color: var(--bg-dark); + background: var(--seafoam); + color: var(--abyss); font-weight: 600; - box-shadow: 0 0 15px rgba(59, 199, 199, 0.3); } .repo-description { font-style: italic; font-size: 0.9rem; - color: var(--text-secondary); - margin-top: 0.5rem; + color: var(--mist); + margin-top: var(--space-xs); } -/* Visualization Canvas */ -.glass-panel { - background: rgba(17, 19, 25, 0.7); - border: 1px solid var(--glass-border); - border-radius: 2rem; - backdrop-filter: blur(20px); +.surface-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); overflow: hidden; position: relative; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); + transition: border-color var(--duration-short) var(--ease-out-quart); } -.insight-card { - padding: 2.5rem; - transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); - overflow: visible; +.surface-panel:hover { + border-color: var(--border-hover); } .visualization-canvas { min-height: 700px; display: flex; flex-direction: column; - padding-bottom: 3rem; + padding-bottom: var(--space-xl); position: relative; } -/* ─── Skeleton Loader ─────────────────────────────────────────────────────── */ @keyframes shimmer { - 0% { background-position: -1000px 0; } - 100% { background-position: 1000px 0; } + 0% { background-position: -1000px 0; } + 100% { background-position: 1000px 0; } } .skeleton-loading-overlay { @@ -254,46 +284,44 @@ body { pointer-events: none; } -/* Shimmer base — applied to every skeleton element */ .skeleton-pill, .skeleton-tick, .skeleton-wave { background: linear-gradient( 90deg, - rgba(255, 255, 255, 0.04) 0%, - rgba(255, 255, 255, 0.10) 40%, - rgba(59, 199, 199, 0.08) 55%, - rgba(255, 255, 255, 0.04) 100% + oklch(100% 0 0 / 0.04) 0%, + oklch(100% 0 0 / 0.10) 40%, + oklch(68% 0.14 195 / 0.08) 55%, + oklch(100% 0 0 / 0.04) 100% ); background-size: 1000px 100%; animation: shimmer 1.8s infinite linear; - border-radius: 9999px; + border-radius: var(--radius-full); } -/* Legend row */ .skeleton-legend { display: flex; justify-content: center; - gap: 2rem; - padding-bottom: 1rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.04); + gap: var(--space-lg); + padding-bottom: var(--space-sm); + border-bottom: 1px solid oklch(100% 0 0 / 0.04); } .skeleton-pill { height: 14px; - border-radius: 9999px; + border-radius: var(--radius-full); } + .skeleton-pill:nth-child(1) { width: 60px; animation-delay: 0s; } .skeleton-pill:nth-child(2) { width: 80px; animation-delay: 0.15s; } .skeleton-pill:nth-child(3) { width: 70px; animation-delay: 0.3s; } .skeleton-pill:nth-child(4) { width: 65px; animation-delay: 0.45s; } -/* Chart body */ .skeleton-chart { flex: 1; display: flex; flex-direction: column; - gap: 0.5rem; + gap: var(--space-xs); } .skeleton-chart-inner { @@ -303,7 +331,6 @@ body { min-height: 400px; } -/* Y-axis ticks */ .skeleton-y-axis { width: 40px; display: flex; @@ -317,14 +344,14 @@ body { height: 10px; border-radius: 4px; } + .skeleton-y-axis .skeleton-tick { width: 32px; } -.skeleton-y-axis .skeleton-tick:nth-child(1) { animation-delay: 0.0s; } +.skeleton-y-axis .skeleton-tick:nth-child(1) { animation-delay: 0s; } .skeleton-y-axis .skeleton-tick:nth-child(2) { animation-delay: 0.1s; } .skeleton-y-axis .skeleton-tick:nth-child(3) { animation-delay: 0.2s; } .skeleton-y-axis .skeleton-tick:nth-child(4) { animation-delay: 0.3s; } .skeleton-y-axis .skeleton-tick:nth-child(5) { animation-delay: 0.4s; } -/* Stacked area mock */ .skeleton-area { flex: 1; display: flex; @@ -338,52 +365,63 @@ body { .skeleton-wave { border-radius: 0; - animation-timing-function: ease-in-out; + animation-timing-function: var(--ease-out-quart); } -.skeleton-wave.wave-1 { flex: 2; animation-delay: 0.05s; opacity: 0.5; } -.skeleton-wave.wave-2 { flex: 3; animation-delay: 0.2s; opacity: 0.7; } -.skeleton-wave.wave-3 { flex: 4; animation-delay: 0.35s; opacity: 1; } -/* X-axis ticks */ +.skeleton-wave.wave-1 { flex: 2; animation-delay: 0.05s; opacity: 0.8; } +.skeleton-wave.wave-2 { flex: 3; animation-delay: 0.2s; opacity: 0.6; } +.skeleton-wave.wave-3 { flex: 4; animation-delay: 0.35s; opacity: 0.4; } + .skeleton-x-axis { display: flex; justify-content: space-between; padding: 0 44px 0 48px; } + .skeleton-x-axis .skeleton-tick.short { width: 30px; height: 10px; } -.skeleton-x-axis .skeleton-tick:nth-child(1) { animation-delay: 0.0s; } + +.skeleton-x-axis .skeleton-tick:nth-child(1) { animation-delay: 0s; } .skeleton-x-axis .skeleton-tick:nth-child(2) { animation-delay: 0.1s; } .skeleton-x-axis .skeleton-tick:nth-child(3) { animation-delay: 0.2s; } .skeleton-x-axis .skeleton-tick:nth-child(4) { animation-delay: 0.3s; } .skeleton-x-axis .skeleton-tick:nth-child(5) { animation-delay: 0.4s; } .skeleton-x-axis .skeleton-tick:nth-child(6) { animation-delay: 0.5s; } -/* Keep pulse for any legacy use */ @keyframes pulse { - 0% { opacity: 0.3; } - 50% { opacity: 0.8; } + 0% { opacity: 0.3; } + 50% { opacity: 0.8; } 100% { opacity: 0.3; } } +@keyframes error-slide { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} .error-banner { - background: rgba(153, 27, 27, 0.2); - color: #f87171; - border: 1px solid rgba(153, 27, 27, 0.4); - padding: 1rem; - border-radius: 1rem; - margin-bottom: 1rem; + background: var(--error-bg); + color: var(--error-text); + border: 1px solid oklch(20% 0.08 25 / 0.4); + padding: var(--space-sm); + border-radius: var(--radius-sm); + margin-bottom: var(--space-sm); text-align: center; + animation: error-slide var(--duration-short) var(--ease-out-quart) both; } .hidden { - display: none !important; + display: none; } -/* Chart Canvas */ .chart-wrapper { flex: 1; width: 100%; @@ -398,28 +436,50 @@ svg#main-chart { min-height: 550px; } +svg#main-chart rect[tabindex="0"]:focus { + outline: 2px solid var(--seafoam); + outline-offset: -2px; +} + .custom-tooltip { position: absolute; pointer-events: none; - background: rgba(10, 10, 12, 0.98); - border: 1px solid var(--glass-border); + background: var(--abyss); + border: 1px solid var(--border); padding: 1.75rem; - border-radius: 1.25rem; - backdrop-filter: blur(16px); + border-radius: var(--radius-md); z-index: 100; min-width: 340px; - box-shadow: 0 30px 60px rgba(0, 0, 0, 0.8); - transition: opacity 0.2s ease; + max-width: min(340px, 90vw); + box-shadow: 0 30px 60px oklch(0% 0 0 / 0.8); + opacity: 0; + visibility: hidden; + transform: scale(0.96); + transform-origin: top left; + transition: opacity var(--duration-short) var(--ease-out-quart), + transform var(--duration-short) var(--ease-out-quart), + visibility 0s var(--duration-short); +} + +.custom-tooltip.hidden { + display: block; +} + +.custom-tooltip:not(.hidden) { + opacity: 1; + visibility: visible; + transform: scale(1); + transition-delay: 0s; } .milestone-banner { display: flex; gap: 0.75rem; align-items: flex-start; - background: linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(59, 199, 199, 0.1)); - border: 1px solid rgba(139, 92, 246, 0.3); + background: linear-gradient(135deg, oklch(52% 0.22 285 / 0.15), oklch(68% 0.14 195 / 0.1)); + border: 1px solid oklch(52% 0.22 285 / 0.3); border-radius: 0.75rem; - padding: 1rem; + padding: var(--space-sm); margin-bottom: 1.25rem; } @@ -435,34 +495,33 @@ svg#main-chart { .milestone-title { font-weight: 600; font-size: 0.9rem; - color: var(--text-primary); + color: var(--ice); margin-bottom: 0.35rem; } .milestone-desc { font-size: 0.75rem; - color: var(--text-secondary); + color: var(--mist); line-height: 1.4; opacity: 0.9; } .milestone-marker text { - transition: all 0.2s ease; + transition: all var(--duration-short) var(--ease-out-quart); } .milestone-marker:hover text { - filter: drop-shadow(0 0 8px rgba(59, 199, 199, 0.8)); + filter: drop-shadow(0 0 8px oklch(68% 0.14 195 / 0.8)); } - .tooltip-header { - font-size: 0.75rem; + font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; - color: var(--text-secondary); + color: var(--mist); margin-bottom: 0.75rem; - border-bottom: 1px solid var(--glass-border); - padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border); + padding-bottom: var(--space-xs); } .tooltip-item { @@ -471,13 +530,13 @@ svg#main-chart { align-items: center; font-size: 0.95rem; padding: 0.35rem 0; - gap: 1rem; + gap: var(--space-sm); } .label-group { display: flex; align-items: center; - gap: 0.5rem; + gap: var(--space-xs); } .color-dot { @@ -489,84 +548,188 @@ svg#main-chart { .value-group { display: flex; align-items: center; - gap: 1rem; + gap: var(--space-sm); } .percent-tag { font-size: 0.75rem; - color: var(--text-secondary); - opacity: 0.7; + color: var(--mist); font-family: var(--font-mono); } .tooltip-divider { height: 1px; - background: var(--glass-border); + background: var(--border); margin: 1.25rem 0; } -/* Legend Styling */ .chart-legend { + position: relative; display: flex; - flex-wrap: wrap; justify-content: center; - gap: 2.5rem; - padding: 1.5rem 0; - margin-bottom: 2rem; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + padding: var(--space-xs) 0; + margin-bottom: var(--space-md); + border-bottom: 1px solid oklch(100% 0 0 / 0.05); } -.legend-item { +.legend-trigger { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.85rem; + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--mist); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-full); + cursor: pointer; + transition: color var(--duration-instant) var(--ease-out-quart), + border-color var(--duration-instant) var(--ease-out-quart); + -webkit-user-select: none; + user-select: none; +} + +.legend-trigger:hover, +.legend-trigger:focus-visible { + color: var(--ice); + border-color: var(--seafoam); + outline: none; +} + +.legend-trigger-icon { + display: inline-block; + width: 0.45rem; + height: 0.45rem; + border-right: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + transform: rotate(-45deg); + margin-top: -0.1rem; + transition: transform var(--duration-short) var(--ease-out-quart); +} + +.legend-trigger[aria-expanded="true"] .legend-trigger-icon { + transform: rotate(45deg); +} + +.legend-panel { + position: absolute; + top: calc(100% + 0.5rem); + left: 50%; + transform: translateX(-50%); display: flex; + flex-wrap: wrap; + gap: 0.4rem 1rem; + padding: 1rem 1.25rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-md); + z-index: 50; + min-width: 200px; + max-width: min(420px, 90vw); + opacity: 0; + visibility: hidden; + transition: opacity var(--duration-short) var(--ease-out-quart), + visibility 0s var(--duration-short); + box-shadow: 0 20px 50px oklch(0% 0 0 / 0.7); +} + +.chart-legend:hover .legend-panel, +.chart-legend:focus-within .legend-panel { + opacity: 1; + visibility: visible; + transition-delay: 0s; +} + +.legend-item { + display: inline-flex; align-items: center; - gap: 0.5rem; - font-size: 0.75rem; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; + gap: 0.3rem; + font-size: 0.65rem; + font-family: var(--font-mono); + color: var(--mist); + cursor: pointer; + padding: 0.15rem 0; + white-space: nowrap; + transition: color var(--duration-instant) var(--ease-out-quart); +} + +.legend-item:hover { + color: var(--ice); +} + +.legend-item .color-dot { + width: 5px; + height: 5px; + border-radius: 50%; + flex-shrink: 0; +} + +@media (prefers-reduced-motion: reduce) { + .legend-panel { + transition: none; + } +} + +.legend-item:hover { + color: var(--ice); } -/* Insights Cards */ .insights-grid { display: grid; grid-template-columns: 1fr; - gap: 2rem; + gap: var(--space-lg); position: relative; } -@media (min-width: 992px) { +@media (min-width: 640px) { .insights-grid { - grid-template-columns: repeat(3, 1fr); - z-index: 1; + grid-template-columns: repeat(2, 1fr); } } -@media (min-width: 768px) and (max-width: 991px) { +@media (min-width: 1024px) { .insights-grid { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(3, 1fr); + z-index: 1; + } + + #card-ancestry { + grid-column: span 2; } } +#card-ancestry .card-content-split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-md); + align-items: end; +} + .insight-card { padding: 2.5rem; - transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + transition: transform var(--duration-medium) var(--ease-out-quint), + border-color var(--duration-short) var(--ease-out-quart); } .insight-card:hover { transform: translateY(-8px); - border-color: rgba(255, 255, 255, 0.3); - box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.6); + border-color: var(--border-hover); } .card-title { - font-size: 0.9rem; + font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; - color: var(--text-secondary); - margin-bottom: 1.5rem; + font-weight: 500; + color: var(--mist); + margin-bottom: var(--space-md); display: flex; align-items: center; - gap: 0.5rem; + gap: var(--space-xs); } .help-icon { @@ -577,89 +740,55 @@ svg#main-chart { height: 1.1rem; font-size: 0.7rem; font-weight: 700; - color: var(--text-secondary); - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.15); + color: var(--mist); + background: oklch(100% 0 0 / 0.08); + border: 1px solid oklch(100% 0 0 / 0.15); border-radius: 50%; cursor: help; position: relative; flex-shrink: 0; - transition: all 0.2s ease; + transition: all var(--duration-short) var(--ease-out-quart); z-index: 10; } -.help-icon:hover { - background: var(--accent-cyan); - color: var(--bg-dark); - border-color: var(--accent-cyan); +.help-icon:hover, +.help-icon:focus-visible { + background: var(--seafoam); + color: var(--abyss); + border-color: var(--seafoam); + outline: none; } .help-icon::after { content: attr(data-tooltip); position: fixed; - transform: translateX(-50%); - background: rgba(0, 0, 0, 0.98); - border: 1px solid var(--glass-border); - color: var(--text-primary); - padding: 0.5rem 0.75rem; - border-radius: 0.5rem; - font-size: 0.7rem; - font-weight: 400; - text-transform: none; - letter-spacing: normal; - opacity: 0; - pointer-events: none; - transition: opacity 0.2s ease; - z-index: 9999; - width: max-content; - max-width: 300px; - min-width: 180px; - line-height: 1.4; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); - text-align: center; left: 50%; top: 50%; - margin-top: -100px; -} - -.help-icon:hover::after { - opacity: 1; -} - -.help-icon:hover { - background: var(--accent-cyan); - color: var(--bg-dark); - border-color: var(--accent-cyan); -} - -.help-icon::after { - content: attr(data-tooltip); - position: fixed; - bottom: auto; - top: auto; transform: translateX(-50%); - background: rgba(0, 0, 0, 0.95); - border: 1px solid var(--glass-border); - color: var(--text-primary); - padding: 0.5rem 0.75rem; - border-radius: 0.5rem; + margin-top: -100px; + background: oklch(0% 0 0 / 0.95); + border: 1px solid var(--border); + color: var(--ice); + padding: var(--space-xs) 0.75rem; + border-radius: var(--radius-sm); font-size: 0.7rem; font-weight: 400; text-transform: none; letter-spacing: normal; + line-height: 1.4; opacity: 0; pointer-events: none; - transition: opacity 0.2s ease; + transition: opacity var(--duration-short) var(--ease-out-quart); z-index: 10000; width: max-content; max-width: 300px; min-width: 180px; - line-height: 1.4; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); + box-shadow: 0 10px 30px oklch(0% 0 0 / 0.5); text-align: center; } -.help-icon:hover::after { +.help-icon:hover::after, +.help-icon:focus-visible::after { opacity: 1; } @@ -668,92 +797,73 @@ svg#main-chart { font-size: 3.5rem; font-weight: 700; line-height: 1; - margin-bottom: 0.5rem; - color: var(--accent-cyan); + margin-bottom: var(--space-xs); + color: var(--seafoam); } .metric-value.small { font-size: 2.2rem; } -.card-content-split { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 1.5rem; - align-items: end; -} - .metric-label { font-size: 0.9rem; - color: var(--text-secondary); -} - -/* Info Grid */ -.info-grid { - display: grid; - grid-template-columns: 1fr; - gap: 2rem; -} - -@media (min-width: 768px) { - .info-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -.info-card { - padding: 1.5rem; - font-size: 0.9rem; -} - -.info-card .card-title { - font-size: 0.8rem; - margin-bottom: 0.75rem; -} - -.card-text { - line-height: 1.6; - color: var(--text-secondary); + color: var(--mist); + line-height: 1.4; } -/* Footer */ .footer { text-align: center; - padding-top: 2rem; - border-top: 1px solid var(--glass-border); + padding-top: var(--space-lg); + border-top: 1px solid var(--border); } .footer-text { font-size: 0.8rem; - color: var(--text-secondary); + color: var(--mist); } -/* Personal Narrative Section */ .personal-narrative { width: 100%; - margin: 4rem 0 6rem; - padding: 4rem; - background: rgba(255, 255, 255, 0.02); - border-radius: 2rem; - border: 1px solid var(--glass-border); + margin: var(--space-xl) 0; + padding: var(--space-lg); + border-radius: var(--radius-lg); + border: 1px solid var(--border); text-align: center; } .narrative-title { font-family: var(--font-serif); - font-size: 2.2rem; - margin-bottom: 2.5rem; - color: var(--text-primary); + font-size: clamp(1.25rem, 2.5vw, 1.8rem); + font-weight: 500; + color: var(--ice); + cursor: pointer; + user-select: none; +} + +.narrative-title::-webkit-details-marker { + color: var(--mist); +} + +.narrative-title::marker { + color: var(--mist); + font-size: 0.8em; +} + +.narrative-title:hover { + opacity: 0.85; } .narrative-content { font-family: var(--font-serif); - font-size: 1.15rem; - line-height: 1.8; - color: var(--text-secondary); + font-size: 1rem; + font-weight: 400; + line-height: 1.6; + color: var(--mist); display: flex; flex-direction: column; - gap: 1.5rem; + gap: var(--space-md); + max-width: 75ch; + margin: 1.5rem auto 0; } .narrative-content p { @@ -761,15 +871,15 @@ svg#main-chart { } .philosophy-link { - color: var(--accent-cyan); + color: var(--seafoam); text-decoration: none; - border-bottom: 1px solid rgba(59, 199, 199, 0.3); - transition: all 0.3s ease; + border-bottom: 1px solid oklch(68% 0.14 195 / 0.3); + transition: all var(--duration-short) var(--ease-out-quart); } .philosophy-link:hover { - border-bottom-color: var(--accent-cyan); - background: rgba(59, 199, 199, 0.05); + border-bottom-color: var(--seafoam); + background: oklch(68% 0.14 195 / 0.05); } .signature { @@ -777,7 +887,7 @@ svg#main-chart { font-family: var(--font-serif); font-style: italic; font-size: 1.4rem; - color: var(--text-primary); + color: var(--ice); opacity: 0.9; line-height: 1.4; } @@ -785,48 +895,49 @@ svg#main-chart { .signature-subtitle { font-style: normal; font-size: 0.85rem; - color: var(--text-secondary); + color: var(--mist); display: block; - margin-top: 0.5rem; + margin-top: var(--space-xs); letter-spacing: 0.05em; } -/* Scrollbar */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { - background: var(--bg-dark); + background: var(--abyss); } ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); + background: oklch(100% 0 0 / 0.1); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.2); + background: oklch(100% 0 0 / 0.2); } -/* Hall of Fossils Section */ .fossil-finder { - padding: 3rem; + padding: var(--space-xl); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-xl); } .section-title { font-family: var(--font-serif); - font-size: 2rem; - margin-bottom: 0.5rem; - color: var(--text-primary); + font-size: clamp(1.5rem, 3vw, 2rem); + font-weight: 600; + margin-bottom: var(--space-xs); + color: var(--ice); text-align: center; } .fossil-hint { text-align: center; font-size: 0.8rem; - color: var(--text-secondary); - opacity: 0.6; + color: var(--mist); margin-bottom: 2.5rem; font-style: italic; } @@ -843,16 +954,34 @@ svg#main-chart { } } +@keyframes fossil-enter { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + .fossil-card { - background: rgba(255, 255, 255, 0.02); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 1.5rem; - padding: 1.5rem 2rem; + background: oklch(100% 0 0 / 0.02); + border: 1px solid oklch(100% 0 0 / 0.1); + border-radius: var(--radius-lg); + padding: var(--space-md) var(--space-lg); position: relative; overflow: hidden; text-align: left; cursor: pointer; - transition: all 0.3s ease; + animation: fossil-enter var(--duration-medium) var(--ease-out-quint) both; + transition: transform var(--duration-medium) var(--ease-out-quint), + border-color var(--duration-short) var(--ease-out-quart), + box-shadow var(--duration-medium) var(--ease-out-quint); +} + +.fossil-card:nth-child(2) { + animation-delay: 100ms; } .fossil-card::before { @@ -862,15 +991,15 @@ svg#main-chart { left: 0; right: 0; bottom: 0; - background: radial-gradient(ellipse at top left, rgba(59, 199, 199, 0.08), transparent 50%), - radial-gradient(ellipse at bottom right, rgba(240, 163, 59, 0.08), transparent 50%); + background: radial-gradient(ellipse at top left, oklch(68% 0.14 195 / 0.08), transparent 50%), + radial-gradient(ellipse at bottom right, oklch(72% 0.16 65 / 0.08), transparent 50%); pointer-events: none; opacity: 0.6; } .fossil-card:hover { - border-color: rgba(59, 199, 199, 0.3); - box-shadow: 0 0 40px rgba(59, 199, 199, 0.1); + border-color: oklch(68% 0.14 195 / 0.3); + box-shadow: 0 0 40px oklch(68% 0.14 195 / 0.1); transform: translateY(-4px); } @@ -878,24 +1007,24 @@ svg#main-chart { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; + margin-bottom: var(--space-sm); } .fossil-label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.1em; - color: var(--accent-cyan); + color: var(--seafoam); display: flex; align-items: center; - gap: 0.5rem; + gap: var(--space-xs); } .fossil-label::after { - content: '↗'; + content: '\2197'; font-size: 0.7rem; opacity: 0; - transition: opacity 0.3s ease; + transition: opacity var(--duration-medium) var(--ease-out-quart); } .fossil-card:hover .fossil-label::after { @@ -906,15 +1035,14 @@ svg#main-chart { font-family: var(--font-mono); font-size: 1.5rem; font-weight: 700; - color: var(--text-primary); + color: var(--ice); opacity: 0.8; } .fossil-meta { font-size: 0.8rem; - color: var(--text-secondary); + color: var(--mist); margin-bottom: 1.25rem; - opacity: 0.6; display: flex; gap: 0.75rem; align-items: center; @@ -922,53 +1050,53 @@ svg#main-chart { } .fossil-link { - color: var(--accent-cyan); + color: var(--seafoam); text-decoration: underline; text-underline-offset: 3px; - text-decoration-color: rgba(59, 199, 199, 0.4); + text-decoration-color: oklch(68% 0.14 195 / 0.4); cursor: pointer; - transition: all 0.2s ease; + transition: all var(--duration-short) var(--ease-out-quart); padding: 0.25rem 0.5rem; border-radius: 0.25rem; - background: rgba(59, 199, 199, 0.05); + background: oklch(68% 0.14 195 / 0.05); display: inline-block; } .fossil-link:hover { - background: rgba(59, 199, 199, 0.15); - text-decoration-color: rgba(59, 199, 199, 0.8); + background: oklch(68% 0.14 195 / 0.15); + text-decoration-color: oklch(68% 0.14 195 / 0.8); } .fossil-commit { font-family: var(--font-mono); font-size: 0.7rem; - color: var(--accent-orange); - background: rgba(240, 163, 59, 0.1); + color: var(--ember); + background: oklch(72% 0.16 65 / 0.1); padding: 0.15rem 0.4rem; border-radius: 0.25rem; } .fossil-code { - background: rgba(0, 0, 0, 0.4); - border: 1px solid rgba(255, 255, 255, 0.08); + background: oklch(0% 0 0 / 0.4); + border: 1px solid oklch(100% 0 0 / 0.08); border-radius: 0.75rem; - padding: 0.6rem 1rem; + padding: 0.6rem var(--space-sm); font-family: var(--font-mono); font-size: 0.85rem; - color: var(--text-primary); + color: var(--ice); opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center; - gap: 0.5rem; + gap: var(--space-xs); text-align: left; } .fossil-code::before { content: '>'; - color: var(--accent-cyan); + color: var(--seafoam); opacity: 0.5; flex-shrink: 0; display: inline; @@ -977,8 +1105,215 @@ svg#main-chart { .fossil-description { font-size: 0.75rem; font-style: italic; - color: var(--text-secondary); - opacity: 0.5; + color: var(--mist); margin-top: 1.25rem; line-height: 1.5; } + +.repo-request { + text-align: center; + padding: var(--space-lg); +} + +.repo-request-title { + font-family: var(--font-serif); + font-size: 1.75rem; + font-weight: var(--weight-semibold); + color: var(--ice); + margin-bottom: 0.75rem; + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; +} + +.repo-request-icon { + font-size: var(--text-sm); + color: var(--mist); + opacity: 0.6; +} + +.repo-request-desc { + font-family: var(--font-mono); + font-size: 0.9rem; + font-weight: 400; + color: var(--mist); + margin-bottom: var(--space-md); +} + +.repo-request-form { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-sm); + flex-wrap: wrap; +} + +.repo-request-field { + position: relative; +} + +.repo-request-input { + padding: 0.6rem 1rem; + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--ice); + background: oklch(0% 0 0 / 0.4); + border: 1px solid var(--border); + border-radius: var(--radius-md); + width: 240px; + max-width: 100%; + outline: none; + transition: border-color var(--duration-instant) var(--ease-out-quart); +} + +.repo-request-input::placeholder { + color: var(--mist); + opacity: 0.5; +} + +.repo-request-input:focus { + border-color: var(--seafoam); +} + +.repo-request-input:focus-visible { + border-color: var(--seafoam); + box-shadow: 0 0 0 2px var(--seafoam); +} + +.repo-request-hint { + display: block; + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--mist); + opacity: 0.75; + margin-top: 0.25rem; + text-align: left; +} + +.repo-request-submit { + padding: 0.6rem 1.25rem; + font-family: var(--font-mono); + font-size: 0.85rem; + font-weight: 600; + color: var(--abyss); + background: var(--seafoam); + border: none; + border-radius: var(--radius-md); + cursor: pointer; + transition: opacity var(--duration-instant) var(--ease-out-quart), + transform var(--duration-instant) var(--ease-out-quart); + white-space: nowrap; +} + +.repo-request-submit:focus-visible { + outline: 2px solid var(--seafoam); + outline-offset: 2px; +} + +.repo-request-submit:hover { + opacity: 0.85; +} + +.repo-request-submit:active { + transform: scale(0.97); +} + +.repo-request-note { + font-family: var(--font-mono); + font-size: 0.65rem; + color: var(--mist); + opacity: 0.6; + margin-top: var(--space-sm); +} + +.repo-request-feedback { + flex-basis: 100%; + font-family: var(--font-mono); + font-size: var(--text-sm); + min-height: 1.5em; + opacity: 0; + transition: opacity var(--duration-short) var(--ease-out-quart); +} + +.repo-request-feedback.visible { + opacity: 1; +} + +.repo-request-feedback.error { + color: var(--ember); +} + +.repo-request-feedback.success { + color: var(--seafoam); +} + +.repo-request-input.invalid { + border-color: var(--ember); +} + +@media (max-width: 480px) { + .repo-request-input { + width: 100%; + } + + .repo-request-form { + flex-direction: column; + align-items: stretch; + } + + .repo-request-submit { + width: 100%; + } +} + +@media (pointer: coarse) { + .repo-btn, + .mode-btn, + .scale-btn { + min-height: 44px; + } + + .fossil-card, + .repo-request-input, + .repo-request-submit { + min-height: 44px; + } + + .help-icon { + width: 44px; + height: 44px; + } +} + +@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; + } + + .insight-card:hover, + .fossil-card:hover { + transform: none; + } + + .author-badge:hover { + transform: none; + } + + .repo-btn:hover, + .mode-btn:hover, + .scale-btn:hover { + transform: none; + } + + .skeleton-pill, + .skeleton-tick, + .skeleton-wave { + animation: none; + } +}