❯ anyplot.ai — design system
─────────────────────────────
brand · frontend · plots — one reference for every adjustment
The complete design system for anyplot.ai — brand identity, frontend visual language, and plot color system in one document. Use this for any frontend adjustment, content tone decision, or plot styling choice.
The guiding principle throughout: the website is the neutral frame; the plots are the content. Every design choice should make room for the data visualizations to do the work — and every interaction should feel native to someone who lives in a terminal or code editor.
Scope: this guide governs the product — the anyplot.ai website, UI copy, marketing surfaces, and brand voice. Repository prose (docs/, README.md, agentic/docs/, CHANGELOG.md entries, PR and issue text) follows the Google developer documentation style guide instead, as anchored in the write-docs skill. Where the two overlap, product surfaces follow this guide (lowercase labels, spaced em-dashes, mono voice); repository docs follow Google style (sentence-case headings, second person, numbered procedures).
anyplot.ai is a considered reference work styled like a code editor over a paper magazine. Three layers, each with a clear role:
| Layer | Role | Anchor |
|---|---|---|
| Brand | Identity, voice, logo, tone of communication | Lowercase default; any.plot() wordmark with green dot |
| Frontend | Website visual language: typography, layout, components | Editorial-scientific paper × terminal/code-editor overlay |
| Plots | Color palette for every visualization across every catalogue library, in Python, R (ggplot2), Julia (Makie), and JavaScript | imprint 8-color categorical, brand #009E73 first |
Aesthetic direction: arXiv paper × tmux/lazygit rather than SaaS dashboard or AI startup. The reader should feel they're browsing a curated journal that happens to live inside a terminal — section headers carry shell prompts (❯, $, ~/plots/), hero text types itself with a blinking cursor, action buttons read as method calls (.copy(), .open(), .download()).
Color discipline: #009E73 (brand green) appears only in small, deliberate moments — logo dot, italic accents, hover states, active navigation, terminal cursor. Everything else is warm grayscale, so when a chart appears its colors land with full impact.
Pipeline transparency: AI is part of the story, not a hidden production detail. Humans submit ideas (plot requests as GitHub issues); AI drafts the spec from the idea; humans approve the spec before any code runs; AI generates the implementations and reviews them; on rejection AI retries and humans tune the rules or refine the spec. Communicated openly in copy and methodology pages.
1. Reserve color for moments that earn it. The imprint palette has eight categorical colours. If the website itself uses them liberally — brightly colored buttons, banners, hero backgrounds — the actual plots lose their visual punch. We treat the palette as precious: the brand green appears in maybe five to ten places on a given page (logo, one italic headline accent, terminal cursor, primary CTA hover, active nav indicator, a handful of link underlines). Everything else is one of the eight gray tones.
2. Warmth over clinical. Pure #FFFFFF backgrounds make palette colors look harsh and the layout feel like a banking app. We use a warm off-white (#F5F3EC) as the base, with slightly lighter surfaces (#FAF8F1) for cards. This gives the page a paper-like quality and makes the saturated plot colors look intentional rather than loud. Dark mode mirrors this: #121210 rather than pure black, with a subtle warm undertone.
3. Code is the native register. Section headers carry shell prompts (❯ libraries, $ plots, ~/anyplot/), action buttons read as method calls (.copy(), .open(), .download()), the hero headline types itself with a blinking cursor. The site speaks the dialect of its visitors. This is not skinning — it's the framing device. Removing the editorial paper underneath would over-tip into "developer toy"; removing the terminal layer would feel sterile and product-marketing-y. The two layers depend on each other.
4. One typeface, two voices. The entire site is set in MonoLisa Code — body, UI, navigation, prompts, buttons, logo, display headlines, everything. The editorial accent is not a second font but MonoLisa's italic + ss01 stylistic set (the script variant), enabled globally via font-feature-settings: "ss01". Whenever text is set in font-style: italic, it renders as a flowing script that carries the emotional/editorial register while staying on the monospace grid.
- Roman (upright MonoLisa Code) — default for all prose, labels, navigation, buttons, code, logo. Bold weights (700) for display sizes.
- Script (italic MonoLisa Code + ss01) — accents only: hero sub-headlines (
— any library.), section titles (libraries,specs), taglines (steal like an artist.), emphasized words (<em>your</em>). Reserve for semantic emphasis, not decoration.
This is a stricter system than the typical 3-font stack and depends on MonoLisa's breadth (all weights 100–900, italic with ss01) rather than bringing in a separate serif. The trade-off: no Fraunces editorial warmth; in exchange, a single coherent voice. MonoLisa 3 ships a proportional sibling, MonoLisa Text; it is registered (--text) for experiments but not part of the system until a decision says where it belongs.
5. Generous vertical rhythm. Section padding is 80px vertical on desktop. Hero sections get 80–100px. Cards have 24–28px internal padding. The layout should feel unhurried, like a journal you read cover to cover rather than a dashboard you scan.
6. Self-evident, not explained. Buttons should not need labels saying "click here to copy" — they say .copy() and that is enough. Icons may exist but are secondary; the primary affordance is text in mono that reads like code. If a coder would understand df.head() without help, they will understand .preview() on a plot card. Explanation prose belongs in docs, not on the page chrome.
7. Details reward attention. Small touches that reward a user who looks closely: the cursor blinks in the hero, the logo dot pulses subtly in the hero-plot legend, active navigation items have a small green dot prefix, the palette strip at the bottom expands the hovered color and contracts the rest, code blocks have ● ● ● macOS-style header dots in 10% opacity, the side plot stacks scroll lazily on ultrawide. None essential to function — they signal "this was made with care."
The brand voice is:
- Precise — we say exactly what we mean, with the right technical terminology
- Understated — we let the work speak; we don't overhype
- Curious — we share interesting details and reasoning, not just conclusions
- Respectful of the reader — we assume technical literacy without being condescending
- Slightly playful — the occasional small joke, mostly structural (the method-call logo, the
import anyplot as apalias, comments-as-prose) - Code-native — when in doubt, write it as code. Pipelines as arrows (
spec → code → review), state as flags, errors as stderr lines. Prose only where prose actually helps. - AI-honest — AI generates the code and runs the reviews. We say so plainly. We don't hype it ("AI-powered"), and we don't hide it ("magically generated").
Tone attributes we avoid:
- Sales-y — no "unlock", "supercharge", "revolutionary", "game-changing"
- Corporate — no "solutions", "leverage", "empower", "synergies"
- Breathless — no excessive exclamation marks, no "literally" or "amazing"
- AI-hype — no "powered by AI", "intelligent", "smart" — but we do describe what the AI actually does
- Emoji-heavy — avoid emojis in documentation and formal writing
anyplot.ai carries three ideas:
- "any" — library-agnostic. Not "py" because the project's long-term vision extends beyond one language.
- "plot" — the core object. We're not abstracting away the plot; we're cataloguing it.
- ".ai" — the modern TLD, inherited from
pyplots.ai. Signals "contemporary technical project" without claiming AI-generated content.
Pronunciation: "any-plot dot AI" — three distinct words, not "any-plottie". The dot is pronounced because the domain structure matters.
Alternative casual forms:
- "anyplot" (when domain isn't needed)
- "ap" (in code:
import anyplot as ap)
Relationship to pyplots.ai: pyplots.ai is the predecessor. The rebrand is communicated as evolution, not replacement: "anyplot.ai is the next chapter of pyplots.ai — broader in scope (beyond Python), broader in language (beyond English), and anchored in a colorblind-safe palette that travels across every library."
The logo is typographic, rendered in MonoLisa Bold:
any.plot()
Specifically:
anyin--ink(near-black on light, near-white on dark).in--imprint-green(#009E73), scaled to 145% with 2–3px horizontal marginplotin--ink()in--inkat 45% opacity, normal weight (not bold)
The . is the single most important brand element. It is:
- The method-call operator, signaling "this is code"
- A data point, the atomic unit of a plot
- The color anchor (the one place brand green always appears)
All three readings are intentional.
Geometry:
letter-spacing: -0.02em (slight negative tracking for density)
font-weight: 700 (bold) for letters, 400 (regular) for parens
Sizes:
| Context | Size | Notes |
|---|---|---|
| Favicon / app icon | 16–64px | Reduce to ap over the green square — the dot, moved below the a so both letters keep their full size. Outlined paths, generated by scripts/generate_favicon.py |
| Top-nav | 18–22px | Standard use |
| Hero section | 40–64px | Occasional, paired with headline |
| Large display | 80–96px+ | Landing hero, posters |
Color variants:
- On light background: ink-dark letters, green dot — default
- On dark background: ink-light letters, green dot stays the same
- On brand green background: do not use — too little contrast. Use monochrome white version instead.
- Monochrome: all letters in
--ink, dot also in--ink. Used in print, fax-friendly contexts, and the occasional t-shirt.
What the logo is not: not an illustration, icon, or mascot. We don't have a plot-graph silhouette, a Python-snake variant, or an anthropomorphic data point. The typographic wordmark is the entire brand mark. Resist the urge to add visual flourishes.
Clear space: maintain at least 1em (one letter-height) around the wordmark on all sides. Inside the clear space, no other text, graphics, or UI elements may appear.
Canonical hero block:
any.plot() — any library.
get inspired.
grab the code.
make it yours.
The first line (any.plot() — any library.) is the wordmark + product proposition in one breath. The three lines below are the user journey, in the imperative, in lowercase, each one standalone. Together they describe exactly what the site is for: discovery → copy → adaptation. Use this hero block on:
- Landing page hero (full block, with terminal box framing and blinking cursor on the last line)
- Meta description (compress to:
any library. get inspired. grab the code. make it yours.) - README headline (full block, in a code fence)
- OG card (line 1 in serif, lines 3–5 in mono)
Short forms:
- "any library." — when only one phrase fits (browser tab title suffix, social bio one-liner)
- "get inspired. grab the code. make it yours." — when the wordmark is already shown nearby (e.g., footer subtitle below the logo)
Alternative taglines for specific contexts:
- "a catalogue of scientific plotting" — editorial framing, when contextualizing the project in academic writing
- *"from anyplot import " — code-playful, good for dev-audience posts
- "plots that everyone can see" — accessibility-focused, good for technical articles about colorblind safety
- "human ideas. ai builds the rest." — methodology framing, for about/methodology pages
Rotate alternatives as appropriate. The canonical hero block is always the first choice.
Taglines we explicitly don't use:
- "the future of data visualization" (overclaim)
- "AI-powered plotting catalogue" (lazy AI-hype framing — say what the AI does, don't sloganize it)
- "plot anything, anywhere, anytime" (cliché, generic)
- "the GitHub of plots" (imitative framing)
See §2.2 for the five tone attributes (precise, understated, curious, respectful, slightly playful) and what to avoid.
Default: lowercase. UI labels, section titles, taglines, button text — all lowercase unless there's a specific reason.
Reasons to use uppercase:
- Proper nouns (library names like
matplotlibare lowercase because that's how they're written;GitHubis uppercase because that's how it's written — follow upstream conventions) - First word of a sentence in long-form prose
- Proper names of people or places
- Code identifiers that are uppercase in their original context
The lowercase default matches the code-forward aesthetic and keeps the site feeling quiet.
Punctuation:
- Oxford comma: yes, always (we write for a technical audience that tends to appreciate grammatical clarity)
- Em-dashes: used freely for parenthetical asides — like this — with spaces around them (European style)
- Sentence structure: prefer short, declarative sentences. Longer sentences are fine when the content demands it.
- Questions: rare. We usually state things rather than ask them.
Announcing a new library:
Do:
plotnine support just landed. 74 new examples using the grammar of graphics.Don't:
🎉 Big news! We're excited to announce that plotnine is now fully supported on anyplot.ai! Check out our awesome new collection of 74 examples! 🚀
Error message:
Do:
$ ap.load("ggplot") ✗ unknown library: "ggplot" supported: matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, lets-plot, ggplot2, makie, chartjs, d3, echarts, highcharts, muixDon't:
Oops! Looks like we don't support "ggplot" yet. But don't worry — we're always adding new libraries! 🙂
Changelog entry (format per CHANGELOG.md, Keep a Changelog):
Do:
### Added - **altair support.** 74 examples across the catalog (#412). ### Changed - **Palette exports accept `theme="light"|"dark"`** (#418). - **seaborn examples now include legends** (was: omitted) (#421).Don't:
🎉 v0.4.0 is here! We've been working hard to bring you some amazing new features...
GitHub issue response to a feature request:
Do:
interesting idea. doesn't fit the catalogue scope right now — anyplot is static examples, this needs interactive features. keeping open for discussion.Don't:
Thanks so much for this amazing feature request!! ❤️ We'll definitely consider this for our roadmap!
Describing the AI pipeline:
Do:
// pipeline idea → human-submitted (github issue) spec → ai-drafted, human-approved code → ai-generated (per library) review → ai-evaluated fix → ai-retries; humans tune rulesDon't:
Powered by cutting-edge AI to bring you the best plotting examples!Don't:
Specs hand-crafted by experts.— they aren't; they're AI-drafted from a human one-liner, then human-approved.
The pattern: state the mechanism, name what does what, no breathless framing. When in doubt, format as code — pipelines as arrows, results as +/~/-/✗.
Long-form transformation example:
Instead of:
🚀 Supercharge your data viz workflow with AI-powered plot generation! Unlock 1000+ beautifully crafted charts across multiple libraries. Ship faster, iterate smarter, and empower your team to create stunning visualizations!Write:
A catalogue of thousands of plotting examples across fifteen libraries in four languages (Python, R, Julia, JavaScript). Plot ideas come from humans; AI drafts the spec, generates code for every library, and reviews each implementation. Humans approve specs and tune the rules when something repeatedly fails. Every example uses the same colorblind-safe palette, so switching libraries never breaks your color grammar.
The second version is shorter, says what's actually happening (humans submit ideas + approve + tune; AI does the drafting/generating/reviewing), and reads as if written by someone who cares about what they're building.
What anyplot is:
- A reference catalogue, like a cookbook or an atlas. You come for specific examples.
- Library-agnostic, showing the same chart types across matplotlib, seaborn, plotly, and others.
- Colorblind-safe by default. Every example uses the imprint palette. This isn't a feature, it's a baseline.
- Copyable. Every example is self-contained, with the full code visible and executable.
- Curated. We don't aggregate every plot on the internet — we maintain a considered collection.
- AI-built, human-shaped. Plot ideas come from humans (GitHub issues). Everything else — drafting the spec, generating code per library, reviewing it — runs on AI. Humans approve specs before any code is generated, and when something repeatedly fails review we either refine the spec or tune the AI rules. We never patch generated code by hand. The pipeline is documented and visible.
What anyplot is not:
- A plotting library. We don't compete with matplotlib or plotly. We help you use them better.
- An AI plot generator the user prompts. Users browse a curated catalogue: its plots come from specs humans approve, and the AI generates them offline. The experimental chat at
/agentadapts a plot the user picked from that catalogue to their own data; it doesn't invent a plot from a prompt. - A dashboard tool. We show static examples. Interactive dashboards are a different product category.
- A tutorial platform. We assume you know how to run Python code. We don't teach from zero.
- A community forum. We accept contributions through GitHub but we're not building a social layer.
Narrative hooks for talking about anyplot — adapt to context, don't recite verbatim:
The pipeline story: Humans submit plot ideas as GitHub issues. AI drafts the spec from each idea; humans approve it before any code is generated. AI then generates implementations across every supported library from the same spec — Python for most, plus R, Julia, and JavaScript — and reviews each one for visual quality and spec compliance. When something doesn't pass review, AI retries — and when it keeps failing, we refine the spec or tune the AI rules. We never patch the code by hand. That makes anyplot a catalogue that maintains itself: when matplotlib ships a new release, we re-run the pipeline; when a better example pattern emerges, we update the spec and every library regenerates. Humans curate; AI executes.
The palette story: Every plot uses the imprint palette, colourblind-safe and tuned for warm-paper rendering. About 8% of men have some form of color vision deficiency — most plotting libraries ignore this entirely. We make it the default.
The library-agnostic story: A "Gentoo penguin" is always blue, whether you draw it in matplotlib, plotly, or bokeh. The palette travels with you across libraries. Switching tools doesn't mean re-learning your color grammar.
The catalogue story: Thousands of examples across fifteen libraries in four languages, each reproducible and copy-pasteable. No ads. No affiliate links. No "suggested tutorials you might like." Just the plots and the code that made them.
The origin story: It started as pyplots.ai, a small Python-only catalogue I built in a weekend. It grew when I realized people wanted the same examples across different libraries, and the same safe palette everywhere. anyplot is the grown-up version.
The personal touch: Built in Visp, a small town in the Swiss Alps. By a data analyst who spent too many hours trying to figure out why the same chart looked different in seaborn than in plotly.
Library:
- Package name:
anyplot(lowercase, one word) - Import convention:
import anyplot as ap - Sub-modules:
anyplot.mpl,anyplot.plotly,anyplot.bokeh, etc. — one sub-module per supported library - Palette:
anyplot.palette(the imprint palette — categorical + semantic anchors + cmap helpers). - Datasets:
anyplot.load("penguins"),anyplot.load("iris")— notload_penguins(); consistent loader signature
Domain:
- Primary:
anyplot.ai - Language redirects:
python.anyplot.ai→anyplot.ai/python/, etc. - Documentation:
docs.anyplot.ai(planned)
Examples and identifiers:
- A slug (URL-safe):
matplotlib-scatter-penguins-species - A title (human-readable):
Scatter plot of penguin species - A library: one of the supported libraries (Python, R, Julia, or JavaScript)
- Tags: chart type, data domain, complexity level
Slugs are the canonical identifier. They're used in URLs, filenames, and ap.load() calls.
anyplot's imprint palette is a bespoke, colourblind-safe categorical palette tuned for warm-paper rendering. 8 hues in a hybrid sort order plus 3 semantic anchors (amber for warning, theme-adaptive neutral, theme-adaptive muted). It sits in the academic-publishing family — same neighbourhood as Okabe-Ito, Paul Tol "muted", and ColorBrewer Set2 — and was validated against deuteranopia, protanopia, and tritanopia at full simulation severity. Full design rationale: palette-variants/v3/decision-rationale.md.
Why this palette over a one-shot import of Okabe-Ito:
Closes the hue-coverage gap of the 7-hue Okabe-Ito layout. With 7 categorical slots you have to choose either lime or cyan, either lavender or rosé. imprint ships all six primary hue families (red / yellow / green / cyan / blue / purple) plus two tertiary tones.
Stable across backgrounds and CVD modes. Every hue holds worst-pair ΔE above the 10-point discrimination floor through n=6 under simulated CVD. The semantic-red anchor is deferred to slot 4 so it stays a free named anchor for bad/loss/error roles instead of being burned on slot 2 of every chart.
Eight is the practical cap. Research by Ware, Glasbey, and Miller on distinguishable categorical colours converges on 7 ± 2 as the practical limit. imprint stops at 8 — and recommends adding a marker shape or linestyle from n=6 upward.
imprint_palette = [
"#009E73", # slot 0 · brand green ★ always first series
"#C475FD", # slot 1 · lavender — creative / artistic
"#4467A3", # slot 2 · blue — cool / water / info
"#BD8233", # slot 3 · ochre — earth / commodity
"#AE3030", # slot 4 · matte red — semantic anchor: bad / loss / error
"#2ABCCD", # slot 5 · cyan — sky / tech-cool
"#954477", # slot 6 · rose — wellness / health
"#99B314", # slot 7 · lime — growth / nature
]
# Semantic anchors outside the categorical pool — never returned by palette[:n]
ANYPLOT_AMBER = "#DDCC77" # warning / caution (fixed hex)
ANYPLOT_NEUTRAL = "#1A1A17" # totals / baseline / outline — flips to #F0EFE8 on dark theme
ANYPLOT_MUTED = "#6B6A63" # other / rest / disabled — flips to #A8A79F on dark themeThe colours:
| Slot | Hex | Name | Role |
|---|---|---|---|
| 0 | #009E73 |
brand green | Logo dot, first category in any plot, primary CTAs |
| 1 | #C475FD |
lavender | Second category, creative / artistic |
| 2 | #4467A3 |
blue | Third category, cool / water / info |
| 3 | #BD8233 |
ochre | Fourth category, earth / commodity |
| 4 | #AE3030 |
matte red | Deferred semantic anchor — bad / loss / error |
| 5 | #2ABCCD |
cyan | Sky / tech-cool |
| 6 | #954477 |
rose | Wellness / health |
| 7 | #99B314 |
lime | Growth / nature |
Semantic anchors (outside the categorical pool — never returned by palette[:n], reached only by name):
| Anchor | Hex (light → dark) | Role |
|---|---|---|
| amber | #DDCC77 |
warning / caution (single fixed hex) |
| neutral | #1A1A17 → #F0EFE8 |
totals / baseline / outline (theme-adaptive) |
| muted | #6B6A63 → #A8A79F |
other / rest / disabled (theme-adaptive) |
The neutral pattern follows the semantic-tokens convention used in Apple HIG, Material Design, and GitHub Primer — the categorical hues stay identical across themes so a category keeps its identity; only the structural neutrals flip.
Order matters. Slots are picked by the hybrid-v3 sort: brand green pinned at slot 0; slots 1..3 filled by greedy max-min worst-CVD ΔE constrained to distinct hue families (red / yellow / green / cyan / blue / purple); semantic red deferred past slot 3 so it stays a free anchor; slots 4..7 filled by unconstrained CVD-greedy on the remainder.
green → lavender → blue → ochre → matte red → cyan → rose → lime
slot 0 slot 1 slot 2 slot 3 slot 4 slot 5 slot 6 slot 7
Plots and content sit inside surface containers with consistent styling:
| Surface | Light | Dark | Use |
|---|---|---|---|
bg-page |
#F5F3EC |
#121210 |
Outer page background |
bg-surface |
#FAF8F1 |
#1A1A17 |
Cards, plot containers |
bg-elevated |
#FFFDF6 |
#242420 |
Modals, tooltips |
The warm off-whites (#F5F3EC for the page, #FAF8F1 for its surfaces) are the foundation — pure white would make the saturated palette colors look harsh.
A warm-tinted grayscale (reddish-brown undertone instead of blue-gray) matches the paper-like background and avoids the "tech blue" feel.
| Token | Light | Dark | Role |
|---|---|---|---|
--ink |
#1A1A17 |
#F0EFE8 |
Primary text |
--ink-soft |
#4A4A44 |
#B8B7B0 |
Secondary text |
--ink-muted |
#6B6A63 |
#A8A79F |
Tertiary, meta, labels |
--rule |
rgba(26,26,23,0.15) |
rgba(240,239,232,0.15) |
Borders, dividers |
All three text tokens pass WCAG 2.1 AA contrast (≥ 4.5:1 for normal text) against both --bg-page surfaces:
| Token | on --bg-page light (#F5F3EC) |
on --bg-page dark (#121210) |
|---|---|---|
--ink |
15.7 : 1 (AAA) | 16.3 : 1 (AAA) |
--ink-soft |
8.03 : 1 (AAA) | 9.32 : 1 (AAA) |
--ink-muted |
4.89 : 1 (AA) | 7.76 : 1 (AAA) |
Computed on 2026-11-04 with the WCAG 2.1 contrast formula, the one the WebAIM Contrast Checker uses, from the values in app/src/styles/tokens.css. Never use imprint palette colours as body-text colors — they are reserved for plot data marks (and a few documented chrome accents like #009E73 in §4.4).
There is one brand green, #009E73, in both themes. #007A59 (colors.primaryDark, and --code-comment on the cream page) is a helper shade only: for green text that must reach 4.5:1 on cream, and for hover accents. It is never a second brand colour, and never the light theme's green.
#009E73 (brand green) appears in the UI only in these contexts:
- Logo dot (always)
- Italic accent words in headlines (e.g.,
any library.) - Hero terminal cursor (the blinking
▌at the end ofmake it yours._) - Active navigation item (green dot prefix + underline on hover)
- Code-style action button hover (
.copy()→ green on hover) and CTA hover (filled background swap) - Code block syntax highlighting (comments — italic, signals editorial commentary inside code)
- Palette strip and any deliberate palette display
- Small status indicators (pulsing dot in plot-card header)
- Terminal prompt glyphs:
$and>>>in the plot-of-the-day terminal, and>>>in the agent chat window's status line (§7.9). They always stand beside words, which carry the meaning.
It does not appear in:
- Background colors (never)
- Borders of regular cards (use
--rule=rgba(26,26,23,0.15)) - Body text emphasis (use serif italic instead)
- Icon colors outside of logos (use gray tones)
- Static decorative dots or glyphs (except the terminal prompt glyphs of item 9)
- Default colour on in-prose links (see below)
In-prose link treatment. Links inside body text default to --ink-soft with a 1px --rule underline (via text-decoration). On hover the colour flips to --imprint-green and the underline thickens to currentColor. Do not set color: colors.primary as the default on inline links — brand green stays a signal colour that only appears on interaction. The reusable sx object is exported from app/src/theme/tokens.ts (re-exported via src/theme) as proseLinkStyle; import it everywhere a contextual link lives in prose (About, Legal, MCP, Palette, Stats).
import { proseLinkStyle } from '../theme';
<Link href="…" sx={proseLinkStyle}>label</Link>Method-call CTAs (.copy(), subject.verb(), hero buttons) keep their own hover-green treatment — the rule above is scoped to textual prose links only.
The imprint palette and its semantic anchors map onto UI status roles:
#AE3030(matte red, slot 4) — destructive actions, error states#DDCC77(amber anchor) — "new" badges, warning highlights#4467A3(blue, slot 2) — informational links in prose, footnotes#BD8233(ochre, slot 3) — accent for hover highlights on secondary elements
Lavender (#C475FD), cyan (#2ABCCD), rose (#954477), and lime (#99B314) are plot-only and do not appear in the UI chrome. This preserves their visual impact for the data visualizations.
Using them in navigation or buttons breaks the color hierarchy.
--serif: 'MonoLisa Code', 'MonoLisa Fallback', Consolas, Menlo, Monaco, 'DejaVu Sans Mono', monospace;
--sans: 'MonoLisa Code', 'MonoLisa Fallback', Consolas, Menlo, Monaco, 'DejaVu Sans Mono', monospace;
--mono: 'MonoLisa Code', 'MonoLisa Fallback', 'JetBrains Mono', Consolas, monospace;
--text: 'MonoLisa Text', 'MonoLisa Code', 'MonoLisa Fallback', system-ui, sans-serif;The --serif and --sans variables are kept as aliases to --mono so existing imports resolve, but all three resolve to the same MonoLisa Code stack. --text is MonoLisa Text, the proportional sibling introduced in MonoLisa 3: registered for experiments, unused by default (the browser fetches nothing for a family no rule uses). The editorial accent comes from italic + ss01 stylistic set, not from a second font. ss01 is enabled globally on html via font-feature-settings: "ss01" — it only affects glyphs rendered with font-style: italic, so upright text is unaffected.
OpenType features in use (v3 tags — they changed from v2):
ss01— script variant, italic only. Wasss02in v2; in v3ss02means "alt i r", so never carry the old tag forward.dlig— coding ligatures (->,!=,===). v2 shipped them underliga(on by default); v3 moved them todlig(off by default).tokens.cssre-enables them onpre,code,kbd,samponly, so prose never ligates.GRAD(variable axis, −50…50) — typographic color without width change. Off (0) everywhere; the hook for a dark-mode experiment (font-variation-settings: 'GRAD' 15under[data-theme='dark']) is documented intokens.css.
Loading:
- MonoLisa 3.000: commercial license required. Self-hosted on GCS (
gs://anyplot-static/fonts/v3/{code,text}/) as 70 Unicode-range subsets per family (woff2, variablewght100–900 +GRAD, normal + italic), plus the four variable TTFs underfonts/v3/desktop/for the OG-image renderer and design-sync. The italic subsets ship with thess01feature table intact — critical for the script variant. CORS restricted toanyplot.ai+ localhost.app/src/styles/fonts.cssis generated from the order page's builder CSS byautomation/scripts/monolisa_fonts_css.py; a font update is a re-run, never a hand edit. - MonoLisa Fallback:
@font-facewithlocal('Consolas'), local('Menlo'), local('Monaco'), local('DejaVu Sans Mono')+ size-adjust metrics — prevents CLS while the webfont loads. - No Google Fonts: Fraunces and Inter were removed in favor of MonoLisa-only. ~40–60 KB transfer + one handshake saved.
The key rule: mono is the default, italic-ss01 is the editorial accent. Everything is MonoLisa. Upright for structure, italic (which triggers ss01 script glyphs) for emphasis. No second font. (One exception outside the site: the footer strip the agents service bakes into user plots, "made with any.plot()" and "anyplot.ai/", is set in JetBrains Mono, because MonoLisa's EULA covers desktop, web, application and ePub use only; see agents/anyplot/render/watermark.py.)
| Element | Variant | Size | Weight | Notes |
|---|---|---|---|---|
| Display headlines | mono + script | clamp(44, 4.5vw, 76px) |
400–700 | Hero H1 mixes upright bold (code token) + italic-script (prose accent). |
| Section titles | mono italic + ss01 | clamp(36, 4.5vw, 56px) |
300–400 | Script variant on title word. Preceded by a shell prompt. |
| Shell prompt prefix | mono upright | 0.6× section-title size | 500 | ❯ $ ~/path/ — --ink-muted, sits before the title. |
| Body paragraph | mono upright | 14–16px | 300–400 | Default body text. Lowercase preferred. |
| Intro / lede | mono upright | 18–19px | 300–500 | Paragraph under H1. Subtitle (one spec · every library…) at 500. |
| Tagline | mono italic + ss01 | 18–22px | 400 | Script variant, e.g. steal like an artist. |
| Eyebrow / Kicker | mono upright | 11px | 400 | Lowercase, tracked .08em, preceded by a 18×1px green rule. |
| UI labels & buttons | mono upright | 12–13px | 500 | Buttons lowercase, e.g. browse plots →. |
| Navigation links | mono upright | 14px | 500 | Active state: green • prefix. |
| Code / logo | mono upright | context-dependent | 700 | Logo: any.plot() with green dot and ghosted (). |
| Stats / numerals | mono upright | 24–32px | 600 | Quiet support — no italic/green accents on numerals anymore. |
| Emphasis in body | mono italic + ss01 | inherits | inherit | <em>your</em> reads as script within upright prose. |
| Inline code in prose | mono upright | inherits ×0.95 | 500 | Background --bg-elevated, padding 2–4px, radius 3px. |
The hero is a split-column layout: editorial text on the left (paper-tier), terminal-framed plot-of-the-day on the right (catalog-tier). The full-width "terminal box" around everything was dropped; the plot panel alone carries the terminal metaphor now.
— the open plot catalogue ┌─ ~/anyplot ──────────────────┐
│ $ python plots/…/plotly.py │
any.plot() │ │
— any library. │ [plot rendered here] │
│ │
one spec · every library · │ │
always current. │ │
│ │
every plot begins as a │ │
library-agnostic spec… │ │
│ │
steal like an artist._▌ │ │
│ │
[browse plots →] or connect via └─────────────────────────────┘
mcp / github →
────────────────────────────────────────────────────────────────
1 9 310 2,677 441k
languages libraries specifications implementations lines of code
H1 construction (two lines):
- Line 1:
any.plot()— MonoLisa Bold (700), upright, with.in--imprint-green(circle viascale(1.3)), ghosted()at opacity 0.45. This matches the NavBar logo exactly for brand consistency. - Line 2:
— any library.— MonoLisa italic +ss01, weight 400, same 0.75em size asany.plot()so heights align. The script variant reads as the editorial subline. - Size:
clamp(2.75rem, 4.5vw, 4.75rem)on the H1 container, with0.75emon both spans.
Layer hierarchy (top to bottom, left column):
- Eyebrow —
— the open plot catalogue, lowercase, 11px MonoLisa upright,--imprint-green, tracked.08em, preceded by 18×1px green rule. - H1 — see above.
- Subtitle —
one spec · every library · always current., MonoLisa upright weight 500, 18px,--ink. Must fit on one line (whiteSpace: nowrap+ stepped responsive size). - Intro prose — 4–6 lines MonoLisa upright weight 300,
--ink-soft, 18px.<em>your</em>renders as italic-ss01 script accent inline. - Tagline —
steal like an artist., MonoLisa italic +ss01, 22px. Typed out by TypewriterText (see §8.1) at ~68ms/char, with a trailing green block cursor that fades 2.5s after typing completes. - CTAs — primary black pill
browse plots →, two stacked text-links next to it:or connect via mcp →,or clone on github →. Lowercase throughout. - NumbersStrip — single
borderTop, five quiet stats (1.5rem MonoLisa weight 600 in--ink-soft), labels 12px--ink-muted. Hangs directly under the hero inside the same 88svh section so the libraries peek below.
CSS sketch:
.hero-h1 {
font-family: var(--mono);
font-size: clamp(2.75rem, 4.5vw, 4.75rem);
line-height: 0.95;
letter-spacing: -0.03em;
color: var(--ink);
}
.hero-h1 .wordmark { font-weight: 700; font-size: 0.75em; letter-spacing: -0.02em; }
.hero-h1 .dot { background: var(--imprint-green); border-radius: 50%; /* circle */ }
.hero-h1 .parens { font-weight: 400; opacity: 0.45; }
.hero-h1 .accent {
font-style: italic;
font-feature-settings: "ss01"; /* script variant */
font-weight: 400;
font-size: 0.75em;
white-space: nowrap;
}
.hero-tagline { font-style: italic; font-feature-settings: "ss01"; }
.hero-cursor { width: 0.55em; background: var(--imprint-green); animation: blink 1s steps(2) infinite; transition: opacity 0.6s; }
@keyframes blink { 50% { opacity: 0; } }NumbersStrip construction (quiet proof-strip, not dominant numerals):
- Number in MonoLisa weight 600, 24–28px,
--ink-soft, no italic, no accent color. - Label below in MonoLisa lowercase 12px,
--ink-muted, tracked.04em. - Single
borderTop: 1px solid var(--rule), no bottom border (matches section-header convention).
Inside plot containers (axis labels, legends, annotations), use monospace throughout to keep plots consistent with the code-forward brand and align with how readers see plot labels in Jupyter notebooks and documentation.
- Tick labels, legends, annotations: MonoLisa or fallback monospace, 10–12px
- Axis labels: MonoLisa, 11–13px,
--ink-soft - Plot titles: MonoLisa, 13–15px,
--ink, rendered outside the plot in the container header rather than inside the axes
Different page types deserve different widths. The system has three tiers:
| Tier | Max width | Used for |
|---|---|---|
| paper | 1240px | Nested reading columns inside editorial pages (About, Legal, MCP, Palette) |
| catalog | 2200px | Outer container — applied globally by RootLayout |
| hero-flank | full vw | Landing hero side plot stacks (only ≥1600px viewport) |
How it's applied. RootLayout wraps every page in maxWidth: var(--max-catalog) (2200px). Paper-width reading columns are opt-in via nested maxWidth: 760 (or var(--max) for wider prose) inside editorial pages — the typical pattern is a full-width SectionHeader followed by a maxWidth: 760, mx: 'auto' prose column underneath. Stats keeps catalog tier since it's data-dense.
Why three tiers:
- paper: keeps reading line-length comfortable (60–80 chars) for editorial content
- catalog: lets ultrawide screens show many plot cards per row (the user came to browse — give them room)
- hero-flank: rewards ultrawide users with extra showcase content (vertical scrolling thumbnails) without compressing the paper-width hero
CSS sketch:
.tier-paper { max-width: 1240px; margin: 0 auto; padding: 0 var(--gutter); }
.tier-catalog { max-width: 2200px; margin: 0 auto; padding: 0 var(--gutter); }
.tier-flank { width: 100vw; } /* full viewport width, used by hero side stacks only */Grid-column safety on mobile. Any CSS-grid column that nests potentially wide content (plots, palette strips, raw images) must use minmax(0, 1fr) rather than 1fr so the column can shrink below its min-content and the page doesn't scroll horizontally on narrow viewports. This applies to HeroSection, PaletteSection, and any similar two-column editorial layout.
Other layout primitives:
- Gutter: 24px on mobile, scaling to 96px on wide screens (set as
--gutter) - Hero box: paper-tier, terminal-style box (see §7.1) ~700–900px wide inside the paper container
- Library grid (catalog tier):
repeat(auto-fill, minmax(280px, 1fr))— responsive across the full 2200px
- Section padding: 80px vertical on desktop
- Hero sections: 80–100px vertical
- Card padding: 24–28px internal
This is significantly more breathing room than most modern sites, and it's deliberate — see principle §2.1.5.
Each section starts with a shell-prompt prefix that names the section as if it were a directory or command. The prompt sits inline with the section title:
❯ libraries
─────────────────────────────────────────────────────────
matplotlib seaborn plotly bokeh altair ...
$ plots
─────────────────────────────────────────────────────────
[scatter-basic] [bar-grouped] [line-multi] ...
~/anyplot/about
─────────────────────────────────────────────────────────
- Prefix glyph:
❯is the default everywhere (navigation, list, editorial).$may be used for explicit action/list sections if it adds meaning;~/path/for hierarchical/about/meta sections. Earlier drafts used§in places (landing, about, libraries) — those have all been migrated to❯. No new glyph enters the system. - Prefix font: MonoLisa weight 500, color
--ink-muted, scaled to ~0.6× the title size - Title font: MonoLisa italic +
ss01stylistic set, 1.6–2rem weight 400; the<em>child is rendered in--imprint-green(script accent), the rest of the h2 stays--ink - Spacing: header wrapper is
pt: 2.5(20px),pb: 1.5(12px) to sit the underline close to the baseline,mb: 4(32px) under the rule before content resumes. Sections wrap in<Box component="section" sx={{ py: { xs: 2, md: 3 } }}>so the page rhythm is uniform across editorial pages. - Underline: 1px solid
--rule, full container width - Component: reuse
app/src/components/SectionHeader.tsx— it encodes the pt/pb/mb/border rules above. Do not re-implement.
The pattern reads as if the user just typed a command and got a section as output. It collapses the editorial "Section §01" framing into something more native to the visitor.
The site opens with a thin horizontal rule displaying:
~/anyplot.ai · main · v1.1.0 │ // the open plot catalogue. │ ◐ dark
Lowercase, monospace, three slots. The left slot shows the domain + git branch + current release tag (the tag is fetched live from the GitHub releases API with 1h localStorage cache); main links to the branch tree, the version links to the release page. The center slot is a code-comment-style sub-tagline. The right slot is a ◐ half-circle theme toggle. This bar immediately positions the site as a tool/publication hybrid rather than a marketing page, and puts the domain in view at all times.
Sticky behaviour. Masthead is position: sticky, top: 0, zIndex: 100 on every route except /plots. On /plots it flows with the page so the FilterBar can pin cleanly at the top of the viewport without z-index competition (both used to fight for top 0 and the 0% counter would visually stack over any.plot()). The masthead bg is opaque var(--bg-page) (no backdrop-filter) so the NavBar scrolls behind it without a cross-fade artefact.
In catalog tier (max-width: 2200px): grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)). On a 2200px viewport this yields ~7 cards per row (with default 280px min); on a 1280px viewport, ~4 cards. Responsive without explicit breakpoints.
Status update: The terminal-box framing no longer wraps the full hero. The left column is now plain editorial text; on the landing page only the plot-of-the-day panel on the right keeps the terminal treatment (with a ~/anyplot tab label and a $ python … prompt header). The CSS below still applies to that panel. See §5.3 for the current hero structure. Since 2026-11-06 the agent chat uses the same construction for its window, with its own tab label and a quieter rule (§7.9).
The plot panel sits inside a single ASCII-style box drawn with CSS borders. The box framing makes the panel feel like a terminal window rendered on editorial paper.
┌─ ~/anyplot ──────────────────────┐
│ │
│ ❯ any.plot() — any library. │
│ │
│ get inspired. │
│ grab the code. │
│ make it yours._▌ │
│ │
│ [.start()] [.browse()] │
│ │
└──────────────────────────────────┘
.hero-box {
position: relative;
border: 1px solid var(--ink-muted);
border-radius: 6px; /* slight, terminal-window-like */
background: var(--bg-surface);
padding: 36px 40px 32px;
font-family: var(--mono);
max-width: 720px;
margin: 0 auto;
}
.hero-box::before {
content: "~/anyplot";
position: absolute;
top: -0.7em;
left: 24px;
padding: 0 10px;
background: var(--bg-page); /* punches through the border */
font-size: 12px;
color: var(--ink-muted);
letter-spacing: 0.05em;
}The ::before pseudo-element ("~/anyplot") sits on top of the border line, like a tab label on a tmux pane or a fieldset legend. On dark mode the same construction works because --bg-page swaps automatically.
The fundamental display unit for any visualization:
background: var(--bg-surface);
border: 1px solid var(--rule);
border-radius: 12px;
padding: 28px;
box-shadow: 0 1px 2px rgba(0,0,0,0.02),
0 24px 48px -24px rgba(0,0,0,0.08);Two-layer shadow: the first (0 1px 2px) gives a sharp edge that keeps the card visually anchored; the second (0 24px 48px -24px) is a soft ambient drop that suggests elevation without being heavy.
The card has three vertical zones:
┌──────────────────────────────────────┐
│ scatter-basic ↗ open │ ← header: mono slug + open-in-new icon
├──────────────────────────────────────┤
│ │
│ [ plot SVG area ] │
│ │
├── dashed rule ──────────────────────┤
│ matplotlib · 120 LOC │ ← footer: library + meta in mono-muted
│ │
│ .copy() .open() .download() │ ← actions: code-style buttons (see §7.4)
└──────────────────────────────────────┘
The dashed rule between plot and footer differentiates the meta from the plot without drawing attention. Solid rules feel too structural for this role.
Action buttons appear on card hover (opacity: 0 → 1, 0.2s).
Used in the catalogue grid. Similar to plot card but with specific additions:
- Top 2px accent bar, initially
scaleX(0), animates toscaleX(1)on hover (color varies per library via--accentcustom property) - Hover lifts card by 3px (
translateY) and softens border toward brand green - Mini-plot thumbnail uses SVG at fixed 120px height
- Library name in mono-bold, example count in mono-muted (
matplotlib · 142 plots)
The color of the accent bar is a subtle way to give each library a personality without breaking the shared palette system — every library gets one of the imprint colours as its accent.
Three variants. All buttons read as method calls in mono — there is no ambiguity about what they do, and they don't need icons or tooltips to explain themselves.
Action buttons (code-style — primary affordance throughout the site)
.btn-action {
font-family: var(--mono);
font-size: 13px;
font-weight: 500;
color: var(--ink-muted);
background: transparent;
border: none;
padding: 6px 10px;
border-radius: 4px;
letter-spacing: -0.01em;
cursor: pointer;
transition: color 0.2s, background 0.2s;
}
.btn-action::before { content: "."; color: var(--ink-muted); }
.btn-action:hover {
color: var(--imprint-green);
background: var(--bg-elevated);
}
.btn-action:hover::before { color: var(--imprint-green); }Examples: .copy(), .open(), .download(), .preview(), .share(), .fork(), .raw(). The leading . is part of the visual language — it signals "this is a method on the thing in front of you" without saying so. No icons needed for common actions.
Two forms of the pseudo-function style, used in different contexts:
- Implicit subject —
.verb()— when a card or surrounding surface visually provides the subject (plot card, detail pane). See the button examples above. - Explicit subject —
subject.verb()— when the action stands alone, detached from a carrier. Format followsany.plot(): the subject is the object/namespace (--ink-muted),.verb()is the method (--ink, green on hover).
Examples of the explicit form:
- Hero CTAs:
plots.browse(),plots.find() - Secondary links:
github.clone(),mcp.connect() - Search prompt:
❯ plots.find(_▌)(or bare.find()inside a detail pane) - Page-level fallbacks:
page.miss(),results.empty()
Pluralisation: prefer the natural plural for collections (plots.browse(), libs.list()) and the singular for a specific item (plot.open(), spec.copy()). Keep everything lowercase — the wordmark-style capitalisation stops at the logo.
Accessibility: the pseudo-function style is a visual convention — it belongs on visible labels, tooltip titles, and placeholder text. aria-label and any hidden <label> fallback for assistive tech must stay in readable human language ("Copy code", "Browse plots", "Zoom in"), because screen readers announce punctuation literally (.copy() → "dot copy open close paren"). Icon-only controls should always carry a descriptive aria-label even when their tooltip uses the method-call style.
Hero CTA (filled — only on landing hero, plus the agent chat's step button)
.btn-cta {
font-family: var(--mono);
font-size: 14px;
font-weight: 500;
background: var(--ink);
color: var(--bg-page);
border: 1px solid transparent;
border-radius: 4px;
padding: 10px 16px;
transition: background 0.2s, color 0.2s;
}
.btn-cta::before { content: "."; opacity: 0.6; }
.btn-cta:hover {
background: var(--imprint-green);
color: #FFF;
}
.btn-cta:hover::before { opacity: 1; }Used for [.start()] and [.browse()] in the hero box. The one other use is the agent chat (/agent, one page for visitors and admins), whose composer row has one filled button, the step's action (§7.9, Composer row): the plot button from the moment data is stored (.create_plot() →, .update_plot() → once the library has a plot, plot the new data, .plot_all() for an admin who turned on the admin menu's fan-out; app/src/sections/agent-chat/styles.ts, composerFilledSx). Before there is data the row has no filled button: the data line above the box is the highlighted way in. That variant hovers with the light theme's ink (#1A1A17) as the foreground in both themes, because 14 px white text on the green reads 3.4:1 and the dark theme's ink 3.0:1, under the 4.5:1 of WCAG AA, while the dark ink on it reads 5.1:1. Pill shape replaced with a more terminal-native rectangle (4px radius). Dark-to-green hover transition stays as the signature moment.
Ghost (rare — only when a primary already occupies visual space)
.btn-ghost {
font-family: var(--mono);
font-size: 13px;
background: transparent;
color: var(--ink);
border: 1px solid var(--rule);
border-radius: 4px;
padding: 8px 14px;
}
.btn-ghost:hover { border-color: var(--ink-muted); }The two vertical strips of plot thumbnails flanking the hero on ultrawide viewports. See §8.5 for the auto-scroll animation and viewport gating.
Container:
.hero-flank {
display: none;
position: absolute;
top: 0; bottom: 0;
width: clamp(180px, 18vw, 320px);
overflow: hidden;
mask-image: linear-gradient(to bottom, transparent, black 10%, black 90%, transparent);
}
.hero-flank--left { left: 0; }
.hero-flank--right { right: 0; }
@media (min-width: 1600px) { .hero-flank { display: block; } }Each thumbnail in the stack:
- 1:1 aspect ratio, 12px border-radius
- Static plot image (PNG/SVG), no axes, no labels — pure visual
- Dimmed by default (
opacity: 0.5), full opacity on hover - Click navigates to the plot's spec page
Warm-dark block, even in light mode. The reasoning: code is "different material" — like a photograph inside a magazine. Forcing it to be light-on-light would make it blend into the surrounding text and lose its "this is code" signal. The light-mode background matches --ink (warm near-black), so the code reads as inverted body text — softer than pure black, in-keeping with the editorial aesthetic. Dark mode steps a shade deeper than --bg-page for a recessed/inset feel.
background: var(--code-bg); /* #1A1A17 light · #0E0E0C dark */
color: var(--code-text); /* #E8E8E0 in both modes */
border-radius: 12px;
padding: 28px 32px;
font: 14px/1.7 var(--mono);
box-shadow: 0 24px 48px -16px rgba(0,0,0,0.2);The fake macOS window-controls (● ● ● in 10% opacity) are rendered via a ::before pseudo-element at the top-left. Playful but not distracting. They also subtly communicate "this is a screenshot of real code running somewhere" which fits the developer-tool framing.
Syntax highlighting uses the imprint palette:
- Keywords → sky blue
- Strings → brand green
- Function names → orange
- Comments → gray (
#666, italic) - Variables → purple
- Numbers → yellow (rare, because the cold Yellow works fine on dark bg)
Horizontal text links, no icons, no boxes. On hover, a 1px green underline animates in from the left. On active state, a small green bullet (•) appears 12px to the left of the text.
.nav a { position: relative; }
.nav a::after {
content: '';
position: absolute; bottom: 0; left: 0; right: 0;
height: 1px;
background: var(--imprint-green);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav a:hover::after { transform: scaleX(1); }The cubic-bezier(0.4, 0, 0.2, 1) is the standard material-motion "ease-in-out" curve — smooth but with enough snap to feel crisp.
Mobile NavBar (xs < 600px). The nav collapses into a two-row grid so nothing drops off:
┌───────────────────────────────────┐
│ any.plot() plots.search()│ row 1: logo + search link
├───────────────────────────────────┤
│ specs plots map libs stats pal mcp│ row 2: seven items · space-between
└───────────────────────────────────┘
- Row 1:
any.plot()wordmark (17px),plots.search()method-call link (11px) right-aligned. - Row 2: all seven nav items (
NAV_LINKSinapp/src/layouts/NavBar.tsx) evenly distributed. Short labels kick in onxswhere the full word doesn't fit:libraries→libs,palette→pal. Keepspecs,plots,map,stats,mcpas-is. - Font size ramps 12px xs → 12px sm → 13px md. Gap ramps 2 → 2.25 → 3.5.
- The NavBar is static (flows with the page). On
/plotsthe masthead also flows so the FilterBar is the only sticky surface; on every other page the masthead stays sticky while the NavBar scrolls away naturally.
Masthead on mobile. ThemeToggle collapses to the glyph only (◐ / ☀) — label hidden via display: { xs: 'none', sm: 'inline' } — so the breadcrumb (~/anyplot.ai · main · v1.1.0) has room to stay visible even on narrow viewports. Grid columns on xs are 1fr auto auto (left breadcrumb takes all remaining width, toggle hugs the right edge); on sm+ it's 1fr auto 1fr with the block-comment tagline in the middle.
Search affordance on mobile. plots.search() is a plain text link (no pill, no ⌘K keycap) — on /plots it focuses the filter-bar search; elsewhere it routes there with ?focus=search. Hidden on xs since the nav-row carries the entry point.
Top-right of the nav, styled as an inline mono input that reads ❯ .find(_) with a ⌘K hint badge to its right. Borderless until hover. Clicking opens a command palette which expands the cursor and accepts typing — the user feels they're continuing the same line of code.
❯ .find(_▌) ⌘K
Active palette state replaces the placeholder cursor with the user's typed query, still in mono. Results appear below the input as a list of slug · library · tags lines, each prefixed with ❯ to maintain the prompt continuity.
The agent chat (/agent) is one terminal-style window on the page, since the v4 design of 2026-11-06 (app/src/sections/agent-chat/ChatFrame.tsx, styles in styles.ts: frameSx, frameRuleSx, headerChipSx). Inside it the chat convention holds: the person's messages on the right in light bubbles, the assistant's on the left without a box.
❯ agent · use with my data
// experimental AI · answers can be wrong ⓘ
┌─ ~/anyplot.ai/agent ─────────────────────────────────────┐
│ Basic Scatter Plot [seaborn ▾] [English ▾] │
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
│ [▦▦▦] You came from Basic Scatter Plot. It needs │
│ [▦▦▦] (x · numbers) (y · numbers) ⓘ │
│ [▦▦▦] drop a file or paste a table below ⓘ │
│ ──────────────────────── # data ──────────────────────── │
│ the thread │
│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │
│ # no data at hand? .example_data() · .open() a file │
│ ❯ [message box ] [.create_plot() →] │
│ >>> step 1 of 2 · check the data and… .stop() .feedback()│
└──────────────────────────────────────────────────────────┘
- Frame: §7.1's construction on the page colour (
--bg-page), so the tab label punches through the top border. The border is1px solid var(--rule), not the hero's--ink-muted: the window holds cards and a message box with borders of their own, and a strong outer line would compete with them. Frame and rules are decoration; every control inside keeps its own boundary. Radius 6px; padding 26px 24px 14px fromsmup, 22px 12px 12px at phone width. - Tab label:
~/anyplot.ai/agentin mono at 13px (smallText),--ink-soft, hidden from screen readers, since the section's name ("Chat") says what it is. - Header row, in plain words, no command line: the spec's title as the link back in
--ink(no←), the library and reply-language dropdowns (seaborn ▾,English ▾: the language in its own words; the language a library belongs to and how the reply language was set live in the accessible names only, which start with the words on screen, so a voice command finds them: WCAG 2.5.3), and in an admin's browser only the admin menu at the row's right end (see One page for everyone below). The dropdowns are 40px high fromsmup and 44px at phone width, on--bg-elevatedwith a--ruleborder. At phone width the title takes the first line and the controls wrap below it. The thumbnail and both ⓘ stood in the row until the start item took them (2026-11-07). - Start item (since 2026-11-07,
StartItem.tsx): the thread's first item, a message like any other, so it stays first and scrolls up with the talk once data arrives; nothing vanishes. Three lines in the body text at 14px:You came from **<the spec's title>**. It needsin--ink; the spec's roles as chips in the words of someone with a table, not a dtype (x · numbers,numbers or datesfor a role of two kinds,y · numbers, one or morefor a family,series · categories, optional; the kinds read numbers, categories, dates, text, yes/no), mono in a pill with a1px solid var(--rule)edge, an optional role's pill dashed and in--ink-soft, with the columns ⓘ after the last chip (the role lines in the spec's words); anddrop a file or paste a table belowin--ink-softwith the how-to ⓘ. Beside them stands the catalogue plot's thumbnail in the library in use (CataloguePreview.tsx, 16:9, 144px wide fromsmup; a click opens it large). The item repeats no control:.example_data()and.open()are the data line's, right above the box. At phone width it is compact: a 96px thumbnail beside the first line, the chips and the pointer below it at the full width, both in 13px, where the chips wrap. - Cards (since 2026-11-07,
CardFrame.tsx,cardFrameSxinstyles.ts): the data card and every plot card are framed like the window, at a card's size:1px solid var(--rule), radius 6px, on--bg-page, with the card's label on its top border in mono at 13px and--ink-soft, so it punches through the rule. The label says what the card holds:data v3 · made long · 48 rows × 4 cols, the data version's number in the chat (DataSnapshot.n, the server's number, which it gives every version of the session once), how it came about and its shape, andplot v2 · matplotlib · title in German, the version, its library and the request it answered, cut to 40 characters (first plotor.start_over()when no request asked for it). A label that is a button keeps the words on screen in its accessible name, in their order ("Plot v2, matplotlib, title in German", "Data v2, sheet "2026", 12 rows by 5 columns", the·as commas), and a voice command that says them finds it (WCAG 2.5.3); the live data card's label, which names no control, is read out in the same words, since·and×read badly. On the border the label keeps one line and ends in an ellipsis where the card is too narrow for it.- Folding: a plot card's label and an older data version's are toggles (
aria-expanded). Every plot version but the latest, and every data version but the live one, folds to its label as one line in the flow,▸ plot v1 · matplotlib · first plot,▸ data v2 · sheet "2026" · 12 rows × 5 cols, a 44px target at phone width and 32px fromsmup. It is the same button open and folded, so a click leaves the focus on it. The plot version's status (ok,needs attention, its dot in the status colour) stands in the theme row, before the run's line. - Data versions (since 2026-11-07,
FoldedData.tsx): the live data card at the latest version stays open in both steps; the pinned header above the thread, with its.show()and.hide()for the card, is gone. An older version is anarticlenamed like a plot card ("Data version 2") and opens read-only: a line that says so (# an earlier version, read-only · the open data card holds the latest), its change lines, how it was read (read as csv · from "sales.xlsx" · sheet "2026"), its warnings and its preview, which shows at every width there, because an older version has no.view_all(); it has no other control. A version a turn makes puts the live card right after that turn's message, before its reply, as a plot card stands before its reply; the card arrives after the reply streamed in, and the thread scrolls by the card's height so the reply stays where the user reads it (HoldReadingPosition.tsx; the browser's own scroll anchoring moved it 240 px on a desktop, since it anchors on the message above the card). One of the card's own controls (.undo(),.to_long(), the pickers) moves the card to the thread's end, before and after a plot alike, after a plot as a new# datachapter, so each older version stays folded with its own summary under it (owner, 2026-11-07). The card moves under its own key, so it keeps what it had open. The keyboard focus follows it: back to the control the user pressed if it is still there and enabled, else onto the card's label (tabIndex-1, never in the tab order; its green ring shows on every focus, also after a mouse click, since it shows where the card went). The card's own lines name no version number (# changed,# undone · back to the version before), and neither does its full view, which saysdata v3: the label's number is the only one the page shows. Since 2026-11-08 that number is the server's, which the assistant names versions by too; an undo brings the restored version's own number back, and its label says so (data v1 · back to v1). - Data card content: the preview heads each column
name · typeon one line, the type in the card's words (integerreadsnumber,datetimereadsdate). The bindings stand in one line under the preview,x (across): month · y (up): revenue · color: region: onlyxandysay which way they run, each column in--ink, a required role without a column as—, an optional one left open not at all..change()at the line's end opens the dropdowns (every control they had stays) and becomes.hide(). They open by themselves while a required role has no column or the server refused the bindings, and stay open after the fix until.hide(), so the control under the focus never goes. While they are closed, a bound column's warning (negative values, a wide table's number column) stands under the line. The dropdowns and the start item's columns ⓘ name a role's kinds in the chips' words (numbers or dates). - Phone: the data card shows no preview table; its action line under the bindings holds
.view_all()and.change(). A plot card's image actions leave the icons over the image's top right corner, which holds the plot's title there, and stand by their names on the action line right under the image (.download(),.copy_png(),.open(),.interactive()or.preview()), 44px targets.
- Folding: a plot card's label and an older data version's are toggles (
- Chapters (since 2026-11-07,
ChapterLine.tsx,threadModel.ts): the thread reads in chapters, each opened by a line# dataor# plotin mono at 13px and--ink-soft, centred between two1px solid var(--rule)rules (kit 3, option C). The name is anh3heading, its#hidden from screen readers; the line around it is a row, and what a chapter will count later (its questions, the plot's remembered decisions) stands beside the heading between the name and the right-hand rule, never inside the heading's name. A chapter starts where the talk turns: the first turn opens# data, the first plot turn after data opens# plot, and a change of the data after a plot (a request, a paste, a version of the card's own controls) opens# dataagain. A question and a library switch stay in the chapter they fall in. A switch is a line of its own in--ink-soft,· plotting in seaborn from here, 3 decisions kept, without a count when it keeps none. The chapter lines carry ids, so a later jump to them (a chapter's questions) lands on them; the page'sscroll-padding-top, the masthead's 56px (MASTHEAD_CLEARANCE), keeps a jump and the keyboard focus below the site's sticky masthead. - Rules:
1px dashed var(--rule)under the header row, and between the thread and the composer (the thread always holds the start item). - Composer row (since 2026-11-07,
Composer.tsx): two fixed places, the message box and the plot button, under one quiet data line that is never gone. Nothing comes or goes and nothing moves; a state only changes how the data line reads, whether the plot button is filled (§7.4, from the moment data is stored) and which controls are inert. An inert control isaria-disabled, notdisabled, so it keeps the keyboard focus, dims to 45% and ignores its click.- Data line, right above the box, mono at 13px: before the first data it is highlighted,
# no data at hand? .example_data() · .open() a file, the words# no data at hand?in the green text shade (--code-comment:#007A59on the light page for 4.5:1,#009E73on the dark one), the two links in--inkunderlined in the brand green#009E73, the rest in--ink-muted. After the first data the same line reads# other data? .open() · .example_data(), muted:--ink-mutedwords, the links in--ink-softwithout underline, their hover the green text shade..example_data()is inert there, since it would replace the data; it keeps its element, so a focus on it survives the change. The links are<button>s with the site's focus ring and a 44px target at phone width (28px fromsmup); at phone width the line drops "at hand" and "a file". - Plot button, 44px high, 14px mono at every width: a ghost, inert, before data,
.create_plot() →with data,.update_plot() →once the library has a plot (it sends the box's text and is inert while the box is empty, except after a binding change or with a switch's carry offer, when it plots with an empty box too),plot the new dataafter a change to confirmed data or a new paste after a plot, and.plot_all()with the admin fan-out on; after a library switch it keeps its name. Its minimum width is that of its longest name, so a new name moves nothing. - Message box: there is no
.send(): Enter sends, and on a touch screen (pointer: coarse) a small↵inside the box sends too, because a phone keyboard labels Enter "return". A1pxborder in the brand green#009E73in both themes (3.1:1 against the light page, so it meets the 3:1 a control's boundary needs; the green is a line here, never text) and a green❯before it fromsmup. Empty, it is one line, its placeholder cut with an ellipsis where the line is too short; it grows with its text. Before data the placeholder readspaste a table or drop a file. - Width: from
smup the box and the plot button share one line, the button at the right end, as wide as its longest name; a row too narrow for both wraps the button to a line of its own at the right end. Belowsm(the page's phone width,PHONE_QUERY) the row stacks them (owner, 2026-11-07): the box takes the whole line, so its placeholder reads at 390px, and the plot button stands under it at the full width, in the same size. The order stays box first, then the button, on screen, in the DOM and for Tab.
- Data line, right above the box, mono at 13px: before the first data it is highlighted,
- Status line: the frame's foot,
>>> step N of 2 · …, in mono at 13px and--ink-soft; the>>>is a terminal prompt glyph in the one brand green#009E73in both themes (§4.4, item 9), as in the plot-of-the-day terminal, and hidden from screen readers. It replaces the step header and the step lines above the message box, and it is the plot button's description: after the step it names what the button waits for, the decisions a plot after a switch keeps, the plot that runs (drawing your plot · usually 1–3 min), the line of the admin fan-out while it holds the box, or the confirmed data by its card's number and its shape (data v3 fits · 48 rows · 4 columns; the page never shows the server's version number, which starts over with every paste). At its right end stand the actions that are not the step's, as quiet buttons in the line's size (44px targets at phone width):.stop()while a run is in progress, framed in the error red with its words in--ink-soft(red text at 13px reads about 2.9:1 on the dark page; the red turns the words on hover), a button of its own that Tab reaches,.start_over()once the library has a plot, and.feedback()before the first plot, whose form opens above the line. When the run ends while.stop()holds the focus, the focus moves to the message box. A hidden polite region beside it announces the step when it changes. - Notice: the experimental notice stands above the frame as one muted line,
// experimental AI · answers can be wrong ⓘ, right-aligned fromsmup and at the left at phone width, in--ink-softat 13px, for every user (for an admin too since 2026-11-06). No coloured rule and no box: the words carry it, and the ⓘ keeps the details. - Not inside the frame: the admin fan-out's panel (
.plot_all()) stands below it, since its sessions are not this chat's. - One page for everyone (owner, 2026-11-06): the admin's chat looks and behaves exactly like a visitor's, so the owner never misses what a visitor meets. The page detects the admin and only picks the API it calls; the limits differ on the server, and the fan-out to every library is the one admin feature. Every admin extra sits behind one admin-only element, the admin menu (
AdminMenu.tsx): a dropdown chip like the library and language ones,admin ▾(admin · as visitor ▾in the visitor's view,admin · expired ▾after an expired admin session), at the header row's right end, where the⋯menu stood. Its popover holds# headingsections: the fan-out toggleall 15 libraries(on, the library chip readsall 15 libraries ▾),.save()with its data box, the toggleview as visitor, and the admin's diagnostics; after an expired session it holds the line that says so, with the link to/debug. Later admin extras (the progress grid, the meters) join as further sections, never elsewhere on the page. The notice, the feedback form's consent line, its unticked data box and every error text are the same for both;AgentChatPage.parity.test.tsxfails on any other difference.
Restrained. Five categories.
Hero box rises from 8px below with opacity transition:
@keyframes rise {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}Staggered via animation-delay:
- Eyebrow (
— the open plot catalogue): 0s - H1 line 1 (
any.plot()): 0.1s - H1 line 2 (
— any library.script): 0.1s - Subtitle (
one spec · every library · always current.): 0.15s - Intro paragraph: 0.2s
- Tagline (TypewriterText, typed at ~68ms/char; cursor fades 2.5s after done): 0.25s
- CTAs: 0.3s
- Plot-of-the-day terminal panel (right column): 0.3s
Total initial cascade: ~0.8s, plus the tagline types for another ~1.4s. The TypewriterText effect on the tagline is what makes the hero feel like code being written out; no per-line triplet animation anymore.
The block cursor at the end of make it yours._▌ blinks indefinitely:
.hero .cursor {
display: inline-block;
width: 0.6em;
height: 1em;
background: var(--imprint-green);
vertical-align: -0.1em;
animation: blink 1s steps(2) infinite;
}
@keyframes blink { 50% { opacity: 0; } }Use steps(2) rather than a smooth ease — terminal cursors are square-wave, not sinusoidal. A fading cursor reads as decorative; a square-blinking one reads as terminal.
For the page-load sequence, the triplet lines may type themselves character-by-character at ~40 chars/second, with the cursor following the typed character. Disable on prefers-reduced-motion and on subsequent visits (cookie-flag the first visit). If implemented, the staggered delays in §8.1 are replaced by sequential typing completion.
@media (prefers-reduced-motion: reduce) {
.hero .typed { animation: none !important; }
.hero .cursor { animation: none; opacity: 1; }
}All transitions use 0.15–0.3s with cubic-bezier(0.4, 0, 0.2, 1). Never use default ease — it's too linear for modern motion.
The vertical plot stacks flanking the hero box on ultrawide viewports auto-scroll continuously:
.hero-flank {
display: none; /* hidden by default */
height: 100vh;
overflow: hidden;
mask-image: linear-gradient(to bottom, transparent, black 10%, black 90%, transparent);
}
@media (min-width: 1600px) {
.hero-flank { display: block; }
}
.hero-flank__track {
display: flex;
flex-direction: column;
gap: 16px;
animation: flank-scroll 60s linear infinite;
}
@keyframes flank-scroll {
from { transform: translateY(0); }
to { transform: translateY(-50%); } /* track contains plots×2; loops seamlessly */
}
.hero-flank:hover .hero-flank__track { animation-play-state: paused; }- Two stacks, one each side of the hero box
- Each stack: 8–12 plot thumbnails sampled randomly from the catalog at page load
- Top/bottom 10% fade to transparent (mask-image) so plots dissolve in/out rather than hard-cutting
- Hover anywhere on a stack pauses the scroll (so the user can click through to a plot)
- Disabled on
prefers-reduced-motion
The palette strip at the bottom responds to hover: normal state is even distribution, :hover .sw shrinks all colors to 50%, then .sw:hover expands the specific hovered color to 300%. Playful detail that invites interaction without being necessary.
.palette-strip .sw { flex: 1; transition: flex 0.3s; }
.palette-strip:hover .sw { flex: 0.5; }
.palette-strip .sw:hover { flex: 3; }- First series = brand color (
#009E73). Always. This is the single most important consistency rule. - Neutral (semantic anchor) is reserved for aggregates, totals, reference lines. Don't use it for a normal category — it sits outside the categorical pool by design.
- Light-bg WCAG caveat: lavender, ochre, cyan, lime, and amber all fall below WCAG 2.1 SC 1.4.11's 3:1 minimum against cream
#FAF8F1. Add a 1px ink-color stroke on affected series when the chart is small or accessibility-strict — see the outline pattern in palette-variants/v3/decision-rationale.md.
imprint is a categorical palette — it's for distinct categories, not ordered or continuous data. For other data types, anyplot ships two palette-derived cmaps:
imprint_seq(sequential): brand green → blue. Use for single-polarity continuous data (intensity, magnitude, density, single-polarity heatmaps).imprint_div(diverging): matte red ↔ near-neutral ↔ blue. Use when the data has a meaningful midpoint (correlations, residuals, signed deviations). The midpoint flips per theme —#FAF8F1on cream bg,#1A1A17on dark bg.
Don't reach for the categorical palette on continuous data — a categorical palette on continuous data produces misleading banding artifacts. And don't substitute viridis/cividis/BrBG/jet/hsv/rainbow either; palette identity is part of the brand.
Plot containers use --bg-surface with a 1px border (rgba(0,0,0,0.08) on light, rgba(255,255,255,0.08) on dark) and 12–24px border-radius depending on context. The subtle elevation differentiates plots from page content without harsh boundaries.
Every palette change should be validated against:
- Deuteranopia (red-green confusion, most common CVD, ~6% of men)
- Protanopia (red-weak, ~1% of men)
- Tritanopia (blue-yellow confusion, rare but important)
Recommended tools:
- Viz Palette — interactive CVD simulator for palettes
- Coblis — upload a plot image, see all three simulations
- Sim Daltonism — macOS system-wide CVD simulator
- Colour Science for Python — programmatic access to Machado et al. (2009) simulation matrices
For algorithmic palette generation beyond imprint, see Petroff (2021) — a more recent palette optimized with numerical solvers in the CAM02-UCS perceptual color space.
The palette is exposed in the Python library as:
from core.palette import palette, IMPRINT
# full categorical pool as list
IMPRINT # ["#009E73", "#C475FD", "#4467A3", "#BD8233", ...]
# by hue name
palette.green # "#009E73" (slot 0, brand)
palette.lavender # "#C475FD" (slot 1)
palette.blue # "#4467A3" (slot 2)
palette.red # "#AE3030" (slot 4, deferred semantic anchor)
# ...
# semantic anchors outside the categorical pool
palette.amber # "#DDCC77" (warning / caution)
palette.neutral("light") # "#1A1A17" (totals / baseline — adaptive)
palette.neutral("dark") # "#F0EFE8"
palette.muted("light") # "#6B6A63" (other / rest — adaptive)
palette.muted("dark") # "#A8A79F"
# semantic-role aliases
palette.semantic.bad # → red
palette.semantic.warning # → amber
palette.semantic.info # → cyan
# matplotlib + cmap registration
from core.palette import imprint_seq, register_with_matplotlib
register_with_matplotlib() # registers imprint_seq + imprint_div_light/darkFor CSS:
:root {
--imprint-green: #009E73; /* slot 0 — brand */
--imprint-lavender: #C475FD; /* slot 1 */
--imprint-blue: #4467A3; /* slot 2 */
--imprint-ochre: #BD8233; /* slot 3 */
--imprint-red: #AE3030; /* slot 4 — semantic anchor */
--imprint-cyan: #2ABCCD; /* slot 5 */
--imprint-rose: #954477; /* slot 6 */
--imprint-lime: #99B314; /* slot 7 */
/* semantic anchors */
--imprint-amber: #DDCC77; /* warning (fixed) */
/* neutral + muted are aliased to --ink and --ink-muted which flip per theme */
}- Gradients: especially purple-to-blue SaaS gradients. The palette is categorical; continuous gradients would undermine its logic.
- Glass morphism / backdrop-blur: too trendy, breaks the paper metaphor.
- Tech-startup illustrations: isometric figures, 3D renders, hand-drawn doodles — all wrong register.
- Parallax effects or hero videos: too heavy for a content-first site. Layout should feel instantly complete.
- Badge-heavy UI: "NEW!" badges, notification dots, progress indicators competing for attention. Reserve attention for plots.
- Pure white or pure black backgrounds: harsh, removes the warmth, makes plots look awkward against them.
- Branded loading spinners: a plain loading indicator is fine. A branded animated logo is overkill. The site's own loader,
LoaderSpinner(three dots: a resting pair in the warm gray of muted text,--ink-muted, which follows the theme, and a runner that turns imprint green while it travels across them and grows; green and ochre until 2026-11-04), is that plain indicator: the plots gallery shows it while it loads, and since 2026-11-04 the agent chat shows its inline size beside the words of a wait that takes seconds (the session's opening, the thread's in-progress line, a plot run's progress, a parse or a file read); its dots stand still underprefers-reduced-motion, the runner green beside the gray pair. - Generic stock imagery: no isometric illustrations, no people high-fiving, no laptop-on-desk hero shots. Plots are our imagery.
- A second typeface: no Space Grotesk, no Poppins, no Fraunces, no Inter. MonoLisa is the only face; italic +
ss01is the only "second voice." Reaching for a serif for warmth or a sans for legal text undoes the single-face discipline.
- Overclaiming: "The best plotting catalogue." We're not. We're a plotting catalogue. Let readers decide if it's best.
- Tech-hype vocabulary: "Supercharge", "unlock", "revolutionary", "game-changing", "AI-powered." All out.
- Fake urgency: "Don't miss out!" "Get started today!" — we're not selling anything.
- Inconsistent casing: headings that alternate between Title Case and lowercase within the same context look like nobody decided.
- Testimonials from non-existent users: we don't need social proof theatre.
- Cookie banners beyond what's legally required: we don't track users. The privacy footer says so.
- Lavender (
#C475FD), cyan (#2ABCCD), rose (#954477), or lime (#99B314) in UI chrome. These are plot-only colors. Using them in navigation or buttons breaks the color hierarchy. - Brand green in backgrounds, body text emphasis, or non-logo icons. Reserve
#009E73for the seven approved contexts (§4.4). - Categorical palettes on continuous data. Use
imprint_seq(single-polarity) orimprint_div(diverging) instead — see §9.2.
The design system is implemented across:
- HTML reference (full mockup):
mockups/landing.html— single-file reference with all sections, SVG plots, and animations - Theme tokens (frontend):
app/src/theme/tokens.ts(design tokens: colors, typography, spacing, headingStyle, subheadingStyle, textStyle, tableStyle, codeBlockStyle) andapp/src/theme/create-theme.ts(MUI theme composition); both re-exported viasrc/theme - Palette (Python library):
anyplot.palette— see §9.5
Reference CSS skeleton:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>anyplot.ai — any library. one plot.</title>
<link rel="preconnect" href="https://storage.googleapis.com" crossorigin>
<!-- MonoLisa Code preloads, as in app/index.html: Basic Latin upright, Basic Latin italic (the script voice, ss01), Latin-1 Supplement (ä ö ü ß) -->
<link rel="preload" href="https://storage.googleapis.com/anyplot-static/fonts/v3/code/0-MonoLisaCode-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="https://storage.googleapis.com/anyplot-static/fonts/v3/code/1-MonoLisaCode-italic.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="https://storage.googleapis.com/anyplot-static/fonts/v3/code/2-MonoLisaCode-normal.woff2" as="font" type="font/woff2" crossorigin>
<style>
:root {
/* imprint palette — 8 categorical hues + 3 semantic anchors */
--imprint-green: #009E73; /* slot 0 — brand */
--imprint-lavender: #C475FD; /* slot 1 */
--imprint-blue: #4467A3; /* slot 2 */
--imprint-ochre: #BD8233; /* slot 3 */
--imprint-red: #AE3030; /* slot 4 — semantic anchor */
--imprint-cyan: #2ABCCD; /* slot 5 */
--imprint-rose: #954477; /* slot 6 */
--imprint-lime: #99B314; /* slot 7 */
--imprint-amber: #DDCC77; /* warning anchor (fixed) */
/* Surfaces — warm off-white, not pure #fff */
--bg-page: #F5F3EC;
--bg-surface: #FAF8F1;
--bg-elevated: #FFFDF6;
/* Warm-tinted grays — all pass WCAG 2.1 AA on the surfaces above */
--ink: #1A1A17;
--ink-soft: #4A4A44;
--ink-muted: #6B6A63;
--rule: rgba(26, 26, 23, 0.15);
/* Typography — MonoLisa Code only. serif/sans aliased for compatibility. */
--serif: 'MonoLisa Code', 'MonoLisa Fallback', Consolas, Menlo, Monaco, monospace;
--sans: 'MonoLisa Code', 'MonoLisa Fallback', Consolas, Menlo, Monaco, monospace;
--mono: 'MonoLisa Code', 'MonoLisa Fallback', 'JetBrains Mono', Consolas, monospace;
/* Layout */
--gutter: 24px;
--max: 1240px;
}
/* Dark theme — toggled by .theme-dark on <body> */
.theme-dark {
--bg-page: #121210;
--bg-surface: #1A1A17;
--bg-elevated: #242420;
--ink: #F0EFE8;
--ink-soft: #B8B7B0;
--ink-muted: #A8A79F;
--rule: rgba(240, 239, 232, 0.15);
}
/* Global: enable ss01 so any italic text renders as the script variant. */
html { font-feature-settings: "ss01"; }
body {
font-family: var(--mono);
background: var(--bg-page);
color: var(--ink);
font-size: 16px;
line-height: 1.5;
transition: background 0.3s, color 0.3s;
}
</style>
</head>
<body>
<!-- Content goes here. See full reference file for complete markup. -->
</body>
</html>Website. Voice as described in §3.4–3.6 — precise, understated, lowercase default. The landing page reads more like a journal essay than a product pitch.
GitHub README. Slightly more technical. Use lowercase headings for a consistent feel. Include: logo at top (SVG), one-paragraph description, installation instructions (first thing below the description), quick-start code example, link to documentation, license and citation. Do not include badges for every CI status; that's noise. Only include badges that carry real information.
Documentation (docs.anyplot.ai). More formal than the marketing site. Follow the same visual style but with denser information layouts. Code blocks dominate. Each function has: one-line description, parameters table, at least one example, related functions (see-also).
Social media. Rare. We're not a company that needs to post daily. Use social channels for: announcing new library support, pointing to particularly interesting examples, linking to related technical writing. Tone: informational, not promotional. Include a plot image whenever possible. Avoid self-reference ("check out our new...") — state the thing itself ("matplotlib now supports...").
Academic citation:
anyplot.ai. (2026). A catalogue of Python plotting examples
across multiple libraries. https://anyplot.ai
Eventually, once the project is mature, consider writing a JOSS (Journal of Open Source Software) paper so there's a formal citation target.
Non-web visual identity:
- GitHub profile images: square variant of the logo:
any.plot()in MonoLisa Bold, centered, on warm off-white background (#FAF8F1). Green dot at#009E73. Maintain 1em padding from edges. - OG / Twitter card images: 1200×630px. Top-left:
anyplot.ailogo in MonoLisa Bold. Center-left: page title in MonoLisa — bold upright for the subject, italic + ss01 script for one accent word. Center-right: representative plot screenshot or palette strip. Bottom: minimal meta in mono. - Presentations: black or warm off-white backgrounds. MonoLisa throughout — upright for body, italic-script for section dividers. Brand green only for emphasis. Every plot in the deck uses imprint.
- T-shirts / merch (if ever): dark t-shirt,
any.plot()in white with green dot, centered on chest. No taglines, no URLs, no GitHub handles. One variant only.
Aesthetic directions we drew from:
- observablehq.com — scientific, plot-centered, monospace details
- arXiv preprints — the "considered paper" feel we're targeting
- Wes Anderson film posters — for the principle of warm neutrals with sparing, saturated color accents
- The New York Times' interactive features — for the editorial layout rhythm
- Vercel's documentation — for the clean monospace-heavy technical pages
What we explicitly didn't borrow from:
- Generic SaaS landing pages (Stripe, Linear, Notion)
- AI startup aesthetics (Anthropic site, OpenAI site, HuggingFace)
- Tech-product marketing pages (Vercel's homepage, specifically — too dense with features)
-
Okabe, M. & Ito, K. (2008). Color Universal Design (CUD): How to make figures and presentations that are friendly to colorblind people. Jikei Medical School / University of Tokyo. https://jfly.uni-koeln.de/color/
-
Wong, B. (2011). Points of view: Color blindness. Nature Methods 8, 441. https://doi.org/10.1038/nmeth.1618 — extends Okabe-Ito with publication-specific usage guidelines.
-
Machado, G. M., Oliveira, M. M. & Fernandes, L. A. F. (2009). A physiologically-based model for simulation of color vision deficiency. IEEE Transactions on Visualization and Computer Graphics 15(6), 1291–1298. — the simulation model used by most modern CVD validators.
-
Petroff, M. A. (2021). Accessible Color Sequences for Data Visualization. arXiv:2107.02270. — modern alternative using CAM02-UCS optimization; relevant if green is not a required anchor.
-
Brewer, C. A. (2003). ColorBrewer in Print: A Catalog of Color Schemes for Maps. Cartography and Geographic Information Science 30(1). https://colorbrewer2.org — the gold standard for cartographic color schemes, useful for reference beyond categorical palettes.
-
van der Walt, S. & Smith, N. (2015). A Better Default Colormap for Matplotlib. SciPy 2015. — the paper behind
viridis, the recommended sequential colormap.
This document is the canonical design system reference. Deviations should be discussed and either:
- Rejected (system holds)
- Accepted as one-off exceptions (documented inline in code with a comment)
- Accepted as new standards (this document is updated)
Option 3 is the healthy path for small refinements. This is a living document, not a contract.
Last updated: 2026.