Skip to content
Madhav

Madhav

Design system v2.1.0. Professional enough for recruiters, fun enough for kids.

Color modes

Two modes, one system

Pages follow the visitor's system setting by default. The round sun or moon button in the header switches scheme and remembers the choice; switching back to match the device clears it, so the page follows the device again. Both modes are shown here side by side, whatever this page is set to.

Daybreak

Daybreak

Light. Cool slate-white ground, coral and teal accents, Fredoka headings.

Tag
bg#F6F7F9
bg-alt#ECEFF3
card#FFFFFF
ink#151B2B
ink-muted#525C6E
line#DCE1E8
edge#151B2B
ledge#151B2B
coral#EE7A63
teal#4FB3A6
sun#F2B544
sky#4FB3A6
plum#EE7A63
coral-strong#B1432C
teal-strong#0E6B63
sun-strong#7C5300
sky-strong#0E6B63
plum-strong#B1432C
on-accent#151B2B
focus= sky-strong
scrimrgba(21,27,43,.45)
media-black#000000
media-ink#FFFFFF
cat-red#F27A85
cat-orange#F5A05A
cat-yellow#F2CB4E
cat-green#74C487
cat-blue#7FB0F0
cat-purple#BC9AF0
cat-red-strong#B42335
cat-orange-strong#9C4A00
cat-yellow-strong#755A00
cat-green-strong#1E7038
cat-blue-strong#1D5DB5
cat-purple-strong#6A45B5
bolt-body#4FB3A6
bolt-screen#E9FBF8
bolt-accent#EE7A63
bolt-belly#F2B544
bolt-eye#FFFFFF
bolt-line#151B2B
Deep Space

Deep Space

Dark. Calm slate night, soft coral and teal accents, Space Grotesk headings.

Tag
bg#0F1218
bg-alt#151A22
cardrgba(27,33,43,.88)
ink#E9EDF3
ink-muted#A2ACBC
line#2A313D
edge#7D889B
ledge#3A4252
coral#F08C77
teal#5CC7B8
sun#F2C062
sky#5CC7B8
plum#F08C77
coral-strong#F59A86
teal-strong#6ED3C4
sun-strong#F2C062
sky-strong#6ED3C4
plum-strong#F59A86
on-accent#0F1218
focus= sky-strong
scrimrgba(0,0,0,.62)
media-black#000000
media-ink#FFFFFF
cat-red#F58E98
cat-orange#F7AE6E
cat-yellow#F4D06A
cat-green#86D198
cat-blue#8DBAF5
cat-purple#C6A8F5
cat-red-strong#F7A0A8
cat-orange-strong#F8B87E
cat-yellow-strong#F4D06A
cat-green-strong#93D8A4
cat-blue-strong#9DC4F7
cat-purple-strong#CFB5F7
bolt-body#4FB3A6
bolt-screen#E9FBF8
bolt-accent#EE7A63
bolt-belly#F2B544
bolt-eye#FFFFFF
bolt-line#151B2B

How it works: tokens.css sets Daybreak on :root, switches to Deep Space under prefers-color-scheme: dark, and lets <html data-theme="light|dark"> override both. Add class="mode-daybreak" or class="mode-deep-space" to pin any block to one mode.

Categories

Category colours

Six distinct hues for things that need telling apart: belts, categories, legend keys. Fills carry on-accent text; the -strong shades are for text and marks. Every pairing meets AA in both schemes.

Daybreak
RedOrangeYellowGreenBluePurple
RedOrangeYellowGreenBluePurple
Deep Space
RedOrangeYellowGreenBluePurple
RedOrangeYellowGreenBluePurple
Typography

Type scale

Each style is a token: font: var(--text-title) plus letter-spacing: var(--text-title-tracking), or the class md-text-title. Headings use Fredoka in Daybreak and Space Grotesk in Deep Space through var(--display). Body is Nunito Sans; code is JetBrains Mono.

display-xl--text-display-xl70px / 1.1 / 600Hero name only. clamp(2.6rem, 6vw, 4.4rem). Swap the family to display-space in Deep Space.
Hi, I'm Madhav
display-l--text-display-l53px / 1.1 / 600Page titles below the hero size: app and report heroes, the h1 of any page that is not the home page. clamp(2.1rem, 5vw, 3.3rem).
Speed tricks for everyone
heading--text-heading43px / 1.1 / 600Section titles. clamp(1.9rem, 4vw, 2.7rem).
Think first, then talk
stat--text-stat35px / 1.1 / 700Big numbers in stat tiles and scoreboards.
2 → 25
title--text-title24px / 1.1 / 600Panel and result headings.
Capital One
card-title--text-card-title20px / 1.15 / 600Card headings and section sub-heads.
Agentic AI
lede--text-lede19px / 1.6 / 400Hero and section intros, in ink-muted.
I teach computers how to listen, think, and help people.
body--text-body17px / 1.6 / 400Default paragraph text.
Pick your building blocks, stay within budget, then launch.
small--text-small15px / 1.5 / 400Card descriptions, post-mortem lines.
Background processing kept uploads instant.
nav--text-nav0.95rem / 1.6 / 600Header nav links and app tabs (the shared header).
Brain Lab
label--text-label14px / 1.4 / 700Buttons, chips, toggles, nav links (nav at 15px/600).
Explain it like I'm 8
label-sm--text-label-sm13.5px / 1.2 / 700Compact controls: small buttons, switches, the Music pill, segmented controls.
Kid mode
kicker--text-kicker12.5px / 1.3 / 700Uppercase labels above stats and lists, in ink-muted.
BUGS FIXED
code--text-code14px / 1.6 / 500Agent thoughts, code backdrops, costs, demo disclaimers.
{ route: "billing", confidence: 0.93 }
Spacing

Spacing

space-14px
space-1-56px
space-28px
space-2-510px
space-312px
space-3-514px
space-416px
space-4-518px
space-520px
space-624px
space-728px
space-948px
space-section72px
Shape

Corners

radius-2xs4px
radius-xs8px
radius-sm12px
radius-md16px
radius-bubble18px
radius-lg22px
radius-xl28px
radius-pill999px
radius-round50%
Tokens

Depth, layers and motion

Shadows and effects have a value per scheme; sizes, layers and motion are the same in both.

Shadows and effects

TokenUse (a value per scheme, see tokens.json)
--shadow-cardCards and panels. A soft lift in both modes.
--shadow-ledgeRaised controls (buttons, chips, game boards use 5px). Hover: 5px and translateY(-2px). Active: 1px and translateY(2px).
--shadow-ledge-hoverRaised control on hover, with translateY(-2px).
--shadow-ledge-pressRaised control while pressed, with translateY(2px).
--shadow-modalDialogs, sheets, toasts and popovers: anything floating above the page.
--shadow-glowA soft amber ring around the current or selected item (the current level, a hinted key). Decorative: never the only sign of state.
--bolt-glowfilter for Bolt and space illustrations: none in Daybreak, a faint coral glow in Deep Space.

Sizes

TokenValueUse
--header-height66pxHeight of the shared site header row.
--control-height40pxRound icon buttons, the Explore button, the theme button.
--control-height-sm36pxThe same controls on phones (600px and narrower).

Layers (z-index)

TokenValueUse
--z-base1Page content above the space backdrop.
--z-header50The sticky site header.
--z-popover60Menus and panels that open from the header (the music panel).
--z-overlay100Non-modal overlays and sheets that are not a <dialog>.
--z-toast120The toast stack, above overlays.
--z-skip400The skip link, above everything.

Motion

TokenValueUse
--duration-fast.12sPress and lift of raised controls.
--duration-medium.25sSwitches, fades, small reveals.
--duration-slow.4sThe theme button spin, dialogs and toasts entering.
--ease-springcubic-bezier(.5,1.6,.5,1)Springy UI: switch knobs, the theme button, level nodes.
--ease-outcubic-bezier(.2,.8,.2,1)Things entering: dialogs, toasts, sections fading up.
Components

Components

Live, from components.css. The header at the top of this page is the shared site header. Switch scheme with the sun or moon to see each one change.

Button

Chip

Tag

AboutBrain LabBug HuntDesign LabJourney

Switch

Stat

Years building
17

Since 2009

AI squad
2 → 25

Engineers, QA, design

Bubble

Hi, I'm Bolt. I route requests so the big model does not have to.

Raised tile

Text input

Leaderboards only see this name.

Music pill

Round icon button and link button

Progress stays here.

Progress bar

80%

Dialog and toast

Sutra of the day

By one more than the one before. A dialog has a scrim behind it, a modal shadow and a close button.

Segmented control and kicker

Kicker on its own

Screen-reader-only text sits here: read aloud, never shown.

Icons

Icons and mascot

32 by 32 line icons with 2.4px round strokes. Files live in icons/. Shown on a Daybreak ground in both schemes, since they are drawn in navy.

agent
bank
book
briefcase
bug
code
coin
hardware
health
house
idea
interface
mystery
shield
sneaker
sound
star
voice
Bolt the robot
bolt
Guidelines

How to use it

The design system behind sbmadhav.com: professional enough for recruiters, fun enough for kids. Every new page should feel like part of the same site, in both color schemes, in both grown-up and kid mode.

Start a new page

  1. Copy template.html to the new page's folder (for example /projects/index.html).
  2. Keep its <head> as is: the theme script, the font preloads, then the pinned /design-system/v/<version>/tokens.css and components.css, in that order. Never add a Google Fonts link or @font-face rules; tokens.css already declares the self-hosted faces.
  3. Keep the shared header between <!-- md-header --> and <!-- /md-header -->. Put the page's own parts in its slots (brand, nav, music, end) and run python3 compose.py header.html page.html with the pinned header.html (see Header below). Load the pinned header.js with defer.
  4. Write every piece of copy twice: <span class="pro">…</span><span class="kid">…</span>.
  5. Build from the components below; add page-specific styles with tokens only (var(--coral), var(--space-6), var(--radius-lg), font: var(--text-title)), never raw hex or raw font sizes.
  6. Add the page to the Explore menu: one entry in PAGES at the top of /assets/explore.js in the sbmadhav.github.io repo (path, icon, grown-up and kid title and one-line description).
  7. Check both schemes (sun/moon button), kid mode, a 390px-wide phone, and reduced motion before publishing.

Versions

Every release is published twice: at /design-system/<file> (the latest, which changes with every release) and at /design-system/v/<version>/<file>, which never changes once published. The current version is 2.1.0; versions.json lists every version.

  • Pin a version in every page: /design-system/v/2.1.0/tokens.css, components.css, header.html, header.js. A design-system release then changes nothing on a page until that page moves to the new version on purpose, in its own change, and each site can roll back alone.
  • Pinned files per version: tokens.css, tokens.json, components.css, header.html, music.html, header.js and compose.py.
  • Version numbers follow semver: a new token, class or component is a minor version; a fix that changes nothing a page relies on is a patch; renaming or removing a token or class is a major version. Existing token names and classes keep working within a major version.
  • The unversioned paths stay published for older pages, and the main site's worker precaches them. New pages should not use them.

The decision and its alternatives are in docs/adr/0001-versioned-releases-and-shared-header.md in the repo.

Files

File What it is
tokens.json The source of truth: every color (both schemes), type style, spacing step, radius, shadow, size, layer and motion token, each with a usage note. Its version is the release version.
tokens.css The same tokens as CSS custom properties, with Daybreak as default and Deep Space for prefers-color-scheme: dark or data-theme="dark". Generated by build.py.
components.css The md- components, the shared header, page basics (md-page, md-wrap, md-section, md-band, md-grid), type classes (md-text-*) and the kid-mode .pro / .kid swap.
header.html The shared site header as markup with slots. Pages compose it in at build time.
header.js The header's color scheme button (and nothing else).
music.html The Music pill's markup, to put in the header's music slot.
compose.py Puts header.html into a page and keeps it in step: python3 compose.py header.html page.html. Standard library only.
template.html A starter page wired up with the shared header, the theme button and kid mode.
index.html The reference page at /design-system/: color modes side by side, category colours, type, spacing, tokens, live components, icons and these guidelines. Generated.
v/<version>/, versions.json The published, immutable copy of each release, and the list of releases. Generated.
build.py Regenerates tokens.css, index.html, the header in template.html and v/<version>/, then checks contrast, token coverage and raw colours: python3 build.py.
icons/ The 18 line icons and Bolt the mascot as SVG files.

This repo

The design system lives in its own repo, sbmadhav/design-system, and GitHub Pages publishes it as a project site. Under the user site's custom domain that is sbmadhav.com/design-system/, the same URLs every page already links. _config.yml keeps README.md, CLAUDE.md and build.py out of the published site; tokens.json is published so other repos can read it.

  • Build. pip install markdown, then python3 build.py. Commit the generated files (tokens.css, index.html, template.html, v/<version>/, versions.json) with the change that produced them.
  • Release. Any change to a published file needs a new version in tokens.json (semver, see Versions). build.py writes v/<version>/; CI fails if a version already on main would gain, lose or change a file (python3 build.py --check-immutable origin/main). Unit tests for that check and for compose.py live in tests/: python3 -m unittest discover -s tests.
  • Checks. build.py fails if a category colour misses AA in either scheme, if a token is missing from a scheme block of tokens.css, or if a hand-written file uses a raw colour. A pre-commit hook in .githooks/ and the workflow in .github/workflows/build.yml both run it and fail if it changed a file. Enable the hook once per clone: git config core.hooksPath .githooks.
  • Shared files from the site. Fonts (/assets/fonts/), the profile photo, the app icon, the Explore menu (/assets/explore.js) and analytics (/assets/analytics.js) are served by the sbmadhav.github.io repo. Keep those paths there.
  • Preview locally. Absolute /assets/ and /design-system/ paths need the whole site. Run node e2e/local-server.mjs and open http://127.0.0.1:8123/design-system/: it serves this checkout and takes everything else from the live site. The end-to-end tests run against it with BASE_URL=http://127.0.0.1:8123.
  • The site's color check. The home page inlines its colors for a fast first paint. sbmadhav.github.io keeps a pinned copy of tokens.json and compares against it; after changing a color here, refresh that copy there with python3 scripts/check-tokens.py --refresh.

The interactive version of this system, with live previews of every component, is kept privately in Claude.

Principles

  • Two audiences, one page. Every piece of copy has a grown-up version and a kid version. The layout never changes between them; only the words do.
  • Two schemes, two personalities. Daybreak (light) is a clean, cool slate-white page. Deep Space (dark) is a calm slate night sky. They should surprise people with how different they feel, while every component stays in the same place.
  • Friendly, never childish. Rounded shapes, chunky borders and a solid "ledge" under controls make things feel touchable. Restraint in copy and layout keeps it professional.
  • Play is opt-in. Music, motion and games never start on their own. Everything respects reduced-motion settings.
  • Show, then tell. Prefer an interactive demo (Brain Lab, Design Lab) over a paragraph that explains an idea.

Content fundamentals

  • Voice: first person, warm, plain words, dry humor in grown-up mode ("Zero-defect release. Principal engineer energy."), wonder in kid mode ("Whoa! We just blasted into deep space!").
  • Never use em dashes. Use a period, comma or colon instead.
  • Short. Ledes are one or two sentences. Card descriptions fit in two lines.
  • Grown-up copy names real things: titles, metrics, technologies. Kid copy names what it does: "builds robot helpers for a really big bank."
  • Sentence case for headings and buttons. Uppercase only for kickers (kicker style).
  • Numbers in stats are big and specific: 17, 2 → 25, +25%, $70M+.

Color

Use tokens, never raw hex. Each token has a Daybreak and a Deep Space value; switching scheme swaps every value at once. Every pairing below meets WCAG 2.1 AA in both modes: 4.5:1 for text, 3:1 for control borders, focus rings, icons and chart marks.

Role Token Daybreak Deep Space
Page ground bg cool slate-white near-black slate
Section bands bg-alt pale slate deep slate
Cards card white dark slate, slightly translucent (blur behind)
Text ink / ink-muted navy / slate soft white / cool gray
Hairlines (decorative) line light gray slate
Control border edge navy slate gray
Control ledge ledge (in shadow-ledge) navy dark slate
Signature fill coral toned coral soft coral
Success fill teal teal aqua
Highlight fill sun amber soft amber
Info fill sky same as teal same as teal
Play fill plum same as coral same as coral
Text, icons, marks coral-strong, teal-strong, sun-strong, sky-strong, plum-strong deep shades lighter shades of the fill
Text on accent fills on-accent navy near-black
Focus ring focus alias of sky-strong alias of sky-strong
Behind dialogs scrim navy at 45% black at 62%
Behind video and media media-black, media-ink fixed black, fixed white the same
Categories and belts cat-red, cat-orange, cat-yellow, cat-green, cat-blue, cat-purple (fills) and cat-*-strong mid tones / deep shades soft tones / light shades
Bolt the mascot bolt-body, bolt-screen, bolt-accent, bolt-belly, bolt-eye, bolt-line fixed the same (fixed)

The palette is called Harbor: three hues. Coral leads, deep teal supports, and amber (sun) is only for highlights. sky shares teal's values and plum shares coral's; they keep their own names so a page can still say what a color means (thinking, play) and the five tag and fill classes keep working. Because they look the same, never use sky and teal (or plum and coral) to tell two things apart: use the category colours.

Category colours

Six distinct hues for anything that must be told apart at a glance: belts, categories, balloon or tile colours in games, legend keys. They are the one place a page may show more than three hues.

  • Fill (cat-red to cat-purple): belts, tiles, chips, legend swatches. Always with on-accent text, and with a 2px edge border when the fill is the only boundary.
  • Strong (cat-*-strong): text, icons, chart lines and marks on bg, bg-alt or card.
  • Belts: white and black stay as the page's own fixed colours; yellow, orange, green, blue and purple use cat-yellow, cat-orange, cat-green, cat-blue and cat-purple.
  • Always pair colour with a word or shape (a belt name, a legend label). Colour alone never carries meaning.

Measured contrast (Daybreak / Deep Space). build.py and the end-to-end tests fail if any falls under 4.5, or if two of the six get too alike (CIE76 distance under 20).

Colour on-accent on the fill -strong on its worst ground
red 6.5 / 8.2 5.6 / 8.3
orange 8.2 / 10.0 5.4 / 9.6
yellow 11.0 / 12.6 5.7 / 11.1
green 8.2 / 10.4 5.3 / 9.9
blue 7.7 / 9.4 5.5 / 9.2
purple 7.4 / 9.2 5.8 / 9.1

Page background is a gradient, not a flat fill: a faint amber glow top right and a faint teal glow left in Daybreak; faint coral, aqua and amber haze in Deep Space. It ships as --page in tokens.css; use background: var(--page); background-attachment: fixed; on body.

How the colors stay accessible

The system follows the approach Stripe describes in Designing accessible color systems: every hue gets the same few lightness levels, so any color at a given level has the same contrast as every other hue at that level. Pick a level by job, not by hue, and contrast takes care of itself.

Level Tokens Job Rule
Fill coral, teal, sun, sky, plum Buttons, tags, chips, highlights, tile backgrounds Always carries on-accent text (at least 6.2:1). Never text on bg.
Strong *-strong Text, links, icons, chart marks, status labels At least 4.5:1 on bg, bg-alt and card.
Soft color-mix(in srgb, var(--x) 14%, var(--card)) Tinted panels, badges, icon tiles Text on it is ink or the matching *-strong.

Measured contrast (Daybreak / Deep Space, on bg):

Pairing Daybreak Deep Space
ink 16.0 16.0
ink-muted 6.3 8.2
coral-strong 5.3 8.8
teal-strong 5.9 10.5
sun-strong 6.3 11.2
sky-strong 5.9 10.5
plum-strong 5.3 8.8
edge (controls) 16.0 5.2
focus 5.9 10.5
on-accent on the lightest fill / darkest fill 9.4 / 6.2 11.2 / 7.8

The tightest pairs are on bg-alt: coral-strong 4.9 in Daybreak and edge 4.6 on card in Deep Space.

State rules - Hover: buttons lift (transform and a deeper ledge); colors stay the same. Chips fill with sun and switch text to on-accent. Links keep their color and gain an underline. - Pressed or selected: fill with ink, text flips to bg (16.0:1 / 16.0:1). - Focus: 3px focus outline with a 3px offset, on every interactive element. - Disabled: 45% opacity. Disabled controls are exempt from contrast rules, but never rely on disabled styling alone to explain why something is unavailable.

Accent rules - Fills carry on-accent text; text, icons and chart marks use *-strong. White text on any fill fails and is not allowed. - Deep Space is calm by default: accents are soft coral and aqua, not neon glows or gradients. - Three hues in one composition. Coral leads, teal supports, amber only highlights. - Section tags take one accent each: sun for About, sky for Brain Lab, coral for Bug Hunt, plum for games, teal for Journey. - line is decorative. A control's boundary must come from edge (or a fill with at least 3:1 against its ground).

Typography

Four families, self-hosted as WOFF2 in /assets/fonts/ so pages make no third-party font requests. tokens.css declares every @font-face (generated by build.py), so linking it is enough. Do not repeat the @font-face rules in a page. Preload the two faces that render first:

<link rel="preload" href="/assets/fonts/fredoka-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/nunito-sans-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
Family File Weights
Fredoka fredoka-latin-600-normal.woff2, fredoka-latin-700-normal.woff2 600, 700
Space Grotesk space-grotesk-latin-wght-normal.woff2 300 to 700 (variable)
Nunito Sans nunito-sans-latin-wght-normal.woff2 200 to 1000 (variable)
JetBrains Mono jetbrains-mono-latin-500-normal.woff2 500 only: never ask for 600 or 700, the browser fakes the bold

To add a weight, put the WOFF2 file in /assets/fonts/ (in the sbmadhav.github.io repo) and add it to FONT_FACES in build.py.

  • Display is Fredoka in Daybreak and Space Grotesk in Deep Space. tokens.css swaps it through one variable, --display; every heading uses font-family: var(--display). This font swap is a big part of why the two schemes feel so different.
  • Nunito Sans for all body text, buttons and labels.
  • JetBrains Mono for agent thoughts, code, costs and disclaimers.
  • Headings are weight 600, tracked in slightly: --tracking-heading is -0.01em in Daybreak and -0.025em in Deep Space, applied by components.css to .md-page headings. Body is 17px at 1.6.
  • Highlight one word in a hero heading with a sun underline band (Daybreak) or a coral-to-teal gradient fill (Deep Space). Use it once per page.

Type tokens

Every style in the scale is a set of tokens, so a page never writes its own font size, and a change here reaches every page:

Token What it holds
--text-<style> The whole font shorthand: weight, size, line height and family. Use font: var(--text-title);
--text-<style>-size, -weight, -leading, -tracking Each part on its own, for when only one changes (font-size: var(--text-small-size)).
.md-text-<style> A class that sets the font and the tracking (and uppercase for kicker).

Styles: display-xl (hero name), display-l (page titles below the hero size, new in 2.1), heading (section titles), stat, title, card-title, lede, body, small, nav (header links and app tabs), label, label-sm (compact controls), kicker, code. Aliases by role: --text-display is display-xl, --text-h1 is display-l, --text-h2 is heading, --text-h3 is title, --text-h4 is card-title.

display-xl, display-l and heading are fluid (clamp()), so they shrink on phones without a media query. Set letter-spacing from the matching -tracking token: the font shorthand does not include it. Kickers also need text-transform: uppercase (the md-kicker and md-text-kicker classes do both).

Layout and spacing

  • Content max width content-max (1120px), gutter space-6 (24px), space-4 (16px) on phones.
  • Every section: space-section (72px) top and bottom padding; alternate plain sections with bg-alt bands that have a 1px line top and bottom.
  • Section header: a tag, a heading, then a lede in ink-muted at most 40em wide, then space-7 (28px) before content.
  • Cards in grids: 4 across on desktop, 2 under 980px, 1 under 560px.
  • No horizontal scroll at 390px (or 320px). The shared header hides its section links under 1180px and keeps Explore, the scheme button, music and the end controls.
Token Value Token Value
space-1 4px space-1-5 6px
space-2 8px space-2-5 10px
space-3 12px space-3-5 14px
space-4 16px space-4-5 18px
space-5 20px space-6 24px
space-7 28px space-9 48px
space-section 72px content-max 1120px

The half steps (space-1-5 to space-4-5, new in 2.1) cover the 6, 10, 14 and 18px gaps that dense controls need. Prefer the whole steps for layout.

Sizes: header-height (66px), control-height (40px: round icon buttons, Explore, the theme button) and control-height-sm (36px, the same controls on phones).

Shape and depth

  • Radius ladder: radius-2xs 4, radius-xs 8, radius-sm 12, radius-md 16, radius-bubble 18 (speech bubbles), radius-lg 22, radius-xl 28, radius-pill, and radius-round (50%, circles).
  • Raised controls (buttons, chips, tiles, palette blocks, game boards) get a 2px edge border and a solid shadow-ledge. They lift on hover (translateY(-2px) with shadow-ledge-hover) and press down on click (translateY(2px) with shadow-ledge-press).
  • Cards and panels get a 1px line border, shadow-card, and lift 4px with a tiny tilt (-0.4deg) on hover.
  • Anything floating above the page (dialogs, sheets, toasts, popovers) gets shadow-modal. A soft amber ring around the current or selected item is shadow-glow.
  • Deep Space adds backdrop-filter: blur(8px) (--glass) to cards and controls so the starfield shows through softly.

Layers

Use the z-index tokens, never a number: z-base (1, content above the space backdrop), z-header (50), z-popover (60, panels that open from the header), z-overlay (100, sheets that are not a <dialog>), z-toast (120) and z-skip (400, the skip link). A <dialog> opened with showModal() sits in the top layer and needs none.

Motion

  • Gentle and looping: Bolt bobs (3.4s), planets drift (45 to 130s per crossing), stars twinkle, the hero blob morphs (12s).
  • Springy UI: toggles, the theme button and level nodes use --ease-spring (cubic-bezier(.5,1.6,.5,1)).
  • Durations: --duration-fast (.12s, press and lift), --duration-medium (.25s, switches and fades), --duration-slow (.4s, the theme button, dialogs and toasts entering). Things entering use --ease-out.
  • Sections fade up 18px on first scroll into view.
  • prefers-reduced-motion: reduce turns all of it off. Nothing essential may depend on motion.
  • Pause motion in the Explore menu does the same on every page (WCAG 2.2.2). It sets class="motion-paused" on <html> and remembers it; components.css stops CSS animation under it. Give every page-level reduced-motion rule an html.motion-paused twin, and have scripts that animate decoration check window.siteMotion.paused() and listen for the sitemotion event on document.

Interaction patterns

  • Kid mode: write both versions side by side as <span class="pro">…</span><span class="kid">…</span>; body.kidmode swaps them. Remember the choice; ?kid in the URL starts in kid mode.
  • Color modes: Daybreak (light) and Deep Space (dark). Pages follow the visitor's system setting by default. The round sun/moon button in the header (md-theme, run by header.js) is the one theme control on every page: a click switches scheme; a choice that differs from the system is saved in localStorage theme (light or dark), and switching back to match the system clears it. Set data-theme from a tiny inline script in <head> to avoid a flash. Pin any block to one mode with class="mode-daybreak" or class="mode-deep-space". There is no System / Daybreak / Deep Space picker any more.
  • Explore menu: every page has an Explore button (Star map in kid mode) that opens a panel listing every page as a card, jump links to the home page's sections, and LinkedIn, GitHub and Install app. It slides in from the right on desktop and up from the bottom on phones. The page list lives in one place, /assets/explore.js.
  • Bolt (the robot mascot) reacts to actions in a speech bubble under him. A tap-hint pill and a halo show until the first click.
  • Music is generated in the browser, never auto-plays, and pauses when the tab is hidden. Every site uses the same Music pill in its header; each brings its own tracks and panel. Games may take over the music while they run and give it back after.

Iconography

  • Inline SVG <symbol> sprites on a 32×32 grid, 2.4px round-capped strokes, no fills except small solid details; stroke uses currentColor in code.
  • Shown on icon tiles: a 54px radius-md tile filled with the accent at 18% and the icon in the full accent.
  • Illustrations (planets, rockets, astronaut, Bolt) use flat fills with a 3 to 5px navy outline.
  • No emoji in the UI. No brand logos of other companies.

Accessibility

  • Text meets 4.5:1 in both schemes: ink and ink-muted on bg, bg-alt and card.
  • Focus: outline: 3px solid var(--focus); outline-offset: 3px.
  • Toggles use role="switch" and aria-checked; chips and toggle buttons use aria-pressed; live results use aria-live="polite".
  • Every game and animation has a still, keyboard-usable path.
  • Pages with repeated navigation start with <a class="md-skip" href="#main">Skip to content</a> and a <main id="main" tabindex="-1">.
  • Anything that scrolls sideways (code samples, wide tables) is focusable and named: tabindex="0" role="region" aria-label="…".
  • At 320px wide nothing scrolls sideways: header controls wrap to a second row, and a control whose label is hidden on phones keeps an aria-label.
  • Text that is only for screen readers uses md-sr-only. Toasts are announced from a role="status" stack and never block taps.

Components

Button

Classes: md-btn, md-btn--primary, md-btn--sm.

A pill with a 2px edge and a solid ledge underneath that lifts on hover and presses down on click.

  • Primary (md-btn md-btn--primary): one per view, the main call to action. Coral in both modes. Label in on-accent.
  • Secondary (md-btn): card background. Use for every other action.
  • Small (md-btn--sm): inside tools and games (Undo, Clear, Reset).
  • Labels are verbs in sentence case and can have kid and grown-up versions ("See how my agents think" / "Peek inside a robot brain").
  • Icons go before the label at 18px.
  • The consumer provides the label, the click handler and disabled when the action is not available.

Chip

Classes: md-chip with aria-pressed.

A pill-shaped choice for picking one option from a small set: Brain Lab questions, Design Lab scenarios.

  • Use aria-pressed="true" on the selected chip; it fills with ink and its text flips to bg.
  • Hover fills with sun and on-accent text.
  • Keep labels under about 30 characters; they wrap as a row with space-2 gaps.

Tag

Classes: md-tag, md-tag--sky|coral|plum|teal.

The small colored label that opens every section, above the heading.

  • One accent per section: sun (default), sky, coral, plum, teal. Pick by meaning: play is plum, debugging is coral, thinking is sky, career is teal.
  • Text is on-accent in the display face at 14px. It can have kid and grown-up versions ("Design Lab" / "Robot Maze").
  • One tag per section; never inline in body text.

Card

Classes: md-card, md-icon-tile.

The default container: white (Daybreak) or dark slate (Deep Space), 1px line border, radius-lg, shadow-card, space-5 padding.

  • Optional icon tile on top: 54px, radius-md, filled with the card's accent at 18% (color-mix(in srgb, var(--teal) 18%, transparent)) with the icon in the matching *-strong color.
  • Heading in card-title; description in small, ink-muted, two lines or fewer.
  • Lifts 4px and tilts -0.4deg on hover. Use in grids of 2 to 4.

Stat

Classes: md-stat with md-kicker, md-value, p.

A metric tile: kicker label, a big number in the stat style, and an optional one-line note.

  • Numbers are specific and short (17, 2 → 25, $70M+). Put the unit in the number, not the label.
  • Use 2 to 4 in a row. Scoreboards in games use the same tile.
  • The note is ink-muted at 14px; give kid mode its own wording.

Switch

Classes: md-switch with md-track and md-knob, role="switch".

The on/off pill used for kid mode, with a springy knob.

  • role="switch" and aria-checked carry the state; the track fills with sun when on.
  • The label names the mode, not the action. Shorten it on phones ("Kid mode").
  • Turning kid mode on swaps every .pro span for its .kid twin and gets a confetti burst.

Bubble

Classes: md-bubble.

Bolt's speech bubble: card background, 2px edge, 18px corners, ledge shadow and a pointer toward the speaker.

  • Place it under Bolt with the pointer up, never over content or photos.
  • One or two short sentences, in the current mode's voice. Auto-hide after about 4 seconds.
  • Announce it with role="status" and aria-live="polite".

Header

Files: header.html (markup), components.css (md-header and friends), header.js (the theme button).

The sticky bar at the top of every page on sbmadhav.com, the same everywhere: Bolt's head and the name (a link home), the page's own nav, the Explore button, the sun/moon theme button, the Music pill and the page's end controls. 66px tall, bg at 88% with a blur, a 1px line under it.

<!-- md-header -->
<!-- slot:nav --><nav class="md-nav" aria-label="Sections"><a href="#work">About</a><a href="#lab">Brain Lab</a></nav><!-- /slot:nav -->
<!-- slot:end --><button class="md-switch" id="kidBtn" role="switch" aria-checked="false" aria-label="Kid mode">…</button><!-- /slot:end -->
<!-- /md-header -->
  • Compose it at build time, never by hand (a malformed, duplicated or unclosed slot marker is an error, so page content is never dropped): python3 compose.py header.html page.html (with the pinned header.html) fills the block from the partial and keeps the page's slots. Run it again after moving to a new version; --check fails when a page's header differs from the partial, for CI.
  • Slots: brand (the name next to the logo, default "Madhav"), nav, music (the Music pill) and end (a kid mode switch, a Player chip). Everything outside the slots is fixed.
  • Nav: <nav class="md-nav"> with plain links; mark the current one aria-current="page" (or "true" for app routes) and it fills with ink. Section links hide under 1180px (the Explore menu lists them). For app tabs that must always show, add md-header--tabs to <header>: under 1180px the tabs move to their own rows and wrap.
  • Phones: under 600px the controls shrink to 36px and Explore and Music show their icon only; under 420px the end controls wrap to a second row; under 360px the name hides and the logo keeps the link's name.
  • No Shadow DOM: the header is plain markup, so kid mode, Pause motion and the fonts work in it like anywhere else.

Theme button

Classes: md-icon-btn md-theme (in the header).

The round sun (Daybreak) or moon (Deep Space) button. header.js handles the click, the saved choice, theme-color, and the button's name ("Switch to Deep Space (dark) scheme"). Pages can listen for the sitetheme event on document (to have Bolt say something), and set data-track="<area>" on the button to record the switch with siteTrack.

Music pill

Markup: music.html. Classes: md-music, md-music-btn, md-music-lbl, md-eq, md-music-next.

A pill with a note icon, the word "Music" and a three-bar equalizer, plus an optional next-track button. It never plays on its own.

  • Play and pause (the home page): the button has aria-pressed; "true" while playing fills the pill with ink and makes the equalizer dance.
  • Open a panel (Vedic Math): the button has aria-expanded and aria-controls; add the class is-on to .md-music while music plays.
  • On phones only the note shows; the button keeps its aria-label.

Raised tile

Classes: md-tile, md-tile-t, md-tile-s, md-tile--fill, md-tile--center.

A card-sized control with a button's edge, ledge, lift and press: grade and game pickers, mode cards, keypad keys, belts.

  • A <button> or <a>. Title in md-tile-t (card-title), an optional line in md-tile-s.
  • Selected: aria-pressed="true" (or aria-current) tints it with sun.
  • md-tile--fill fills it with --tile-fill (default sun) and switches text to on-accent: style="--tile-fill: var(--cat-blue)". A selected fill tile keeps its fill and shows an ink ring instead.

Text input

Classes: md-input (on <input>, <select> or <textarea>), md-label, md-hint.

  • Always with a visible md-label tied by for; put help in md-hint and link it with aria-describedby.
  • 2px edge border, radius-sm, 40px tall. aria-invalid="true" turns the border coral-strong; say what is wrong in words too.

Dialog

Classes: md-dialog on a <dialog> opened with showModal(), md-dialog-head, md-dialog-foot.

  • A scrim behind it, radius-xl, shadow-modal, solid card (never see-through). Name it with aria-labelledby pointing at its <h2>.
  • A round close button in the head (md-icon-btn md-icon-btn--sm, aria-label="Close"); Escape closes it too.

Progress bar

Classes: md-progress, md-progress--sm (6px), md-progress--lg (16px).

  • <div class="md-progress" role="progressbar" aria-label="…" aria-valuemin="0" aria-valuemax="100" aria-valuenow="60"><span style="--value:60%"></span></div>, or a native <progress class="md-progress">.
  • The track is line, the fill teal-strong; change it with --bar (for example --bar: var(--sun-strong) for a timer).

Toast

Classes: md-toasts (the stack), md-toast.

  • One stack per page: <div class="md-toasts" role="status" aria-live="polite">. Append a md-toast, remove it after about 3 seconds (add is-leaving first to fade it).
  • The stack and the toasts have pointer-events: none, so a toast never blocks a tap on the page under it. A link or button inside a toast still works.

Round icon button

Classes: md-icon-btn, md-icon-btn--sm.

A 40px circle (32px small) with one 20px icon: close, flip back, play. Always give it an aria-label.

Link button

Classes: md-link.

A <button> that reads as a link, for small inline actions ("Reset play mode"). Underlined in coral, so it is told apart by more than colour.

Segmented control

Classes: md-seg (or the older name md-modes) with <button aria-pressed>.

One choice from two to four: leaderboard periods, units. Not for the color scheme, which is the theme button.

Kicker and screen-reader-only text

Classes: md-kicker, md-sr-only.

  • md-kicker is the uppercase label (kicker style, ink-muted) and works anywhere, not only in a stat.
  • md-sr-only hides text visually but keeps it for screen readers.

Icons

The line icons from sbmadhav.com, exported from its inline SVG sprite. 32×32 grid, 2.4px round-capped strokes, drawn here in ink navy (#151B2B).

  • On the site they live in a <svg><defs><symbol> sprite and use currentColor, so they take the accent color of their tile or text. Copy the paths into a sprite rather than loading the files in icons/ with <img> when the color must change.
  • Pair each with a meaning, not a decoration: agent (agentic AI), voice, interface, shield (responsible AI), briefcase (consulting), hardware, sneaker, health, bank, mystery (what's next), bug, star (goal or current level).

Mascot

Bolt, the site's original robot mascot: teal body and arms, pale mint face, coral antenna and cheeks, amber chest, navy outlines.

  • The header logo is Bolt's head, drawn with the same tokens.
  • Bolt speaks through the Bubble component and reacts to what people do (switching schemes, playing music, winning games).
  • Keep Bolt's colors fixed in both schemes with the bolt-* tokens: bolt-body, bolt-screen, bolt-accent, bolt-belly, bolt-eye and bolt-line. Never white or card for his eyes (card is dark in Deep Space).
  • For the faint Deep Space glow, use filter: var(--bolt-glow) (or the class md-bolt): a soft coral drop shadow in Deep Space and nothing in Daybreak, whichever way the scheme was chosen.
  • Never put words on Bolt or change his shape; poses come from small transforms (bob, tilt, eyes following the pointer).