Mind site design system

Brain-centered and dark-native — a palette sampled from the hand-vectored gear-brain, with teal as the only living color. The gear-brain itself is a frozen signature surface; this spec documents it and never restyles it.

Every value here is read from the site’s own source when the site is built, so the page and the code never disagree.

Principles

  • RULE ZERO: the palette is EXTRACTED from the brain SVG, not invented.
  • The teal is the ONLY living color; it comes from attention, not the metal. … "The mind is silver; attention makes it glow."
  • Motion: exactly three named eases. Use NOTHING else.
  • Spacing rhythm (modular ~1.5 scale). Tight within, generous between.
  • The brain is pristine hand-vectored art … Adaptation layer only. Never simplify or regenerate the SVG.
  • The visual language of this site is BRAIN-CENTERED, not the sibling site's ocean motif
  • Anuja leads every creative decision; engineering waits on the artist.

Brand and voice

ModeDark-native — the mind floats in space. No light mode; the palette itself is the decision.
AccentSynapse teal #14b8a6 — attention only, never chrome decoration.
TypeHanken Grotesk for display and body (weight carries hierarchy); JetBrains Mono for mono caps; Noto Sans Devanagari.
GlassBanned — one waiver: the glass-pill Button borrowed from the portfolio for kinship.
EdgesCommit to a defined edge OR soft elevation — never a hairline plus a wide shadow.
Motionsettle (signature) · enter · drift — at 240 / 640 / 1500ms, the gears’ harmony beat.

Foundations

Color

Every swatch paints with the live token. The notes are the token file’s own words; the source is the hand-vectored brain below.

The resting gear-brain, this site's front door: silver gears in a brain silhouette on deep black
Shown as a still. The brain itself lives, untouched, at the center of the mind.

Surfaces

  • --space#010204the dark the brain floats on
  • --graphite#26282bSVG dark cluster (#242424/#2d2d2d), normalized

Text

  • --white#ffffffpure white — room placard title + description (her call)
  • --silver#e2e8f0light metal (45 shapes) — primary text
  • --slate#94a3b8most-used metal (47 shapes)
  • --steel#64748bdeep gear-metal gradient stop

Borders

  • --synapse-line#475569

Accent

  • --synapse#14b8a6the lit teal — the one living color
  • --teal-link#078B9Cher portfolio's accent teal — legible on white; used for clickable links on light surfaces

Semantic

None by design. Rule Zero admits only colors sampled from the brain, so state is carried by the one teal, by weight and by words.

Charts

None. This world holds artwork, not data.

Roles

RoleMaps toUsed for
--bg--space
--ink--silverprimary text on space
--ink-quiet--slatesecondary text / metadata
--ink-faint--steeltertiary / disabled
--edge--graphitedividers, hairlines
--glow--synapseattention only — never chrome decoration

Typography

Hanken Grotesk

300400500600700

--font-display · --font-bodyDisplay and body; weight carries the hierarchy.

JetBrains Mono

Live demo · Read the story

--font-monoThe button voice and technical labels, the thread to anujaharsha.com.

तुम हो तो मैं हूँ

--font-devanagariNoto Sans Devanagari, for the Hindi poems.
  • A mind you can walk through--text-3xl2.9rem · 46.4px
  • A mind you can walk through--text-2xl2.1rem · 33.6px
  • A mind you can walk through--text-xl1.6rem · 25.6px
  • A mind you can walk through--text-lg1.25rem · 20px
  • A mind you can walk through--text-base1rem · 16px
  • A mind you can walk through--text-sm0.875rem · 14px

Reading measure: --measure 68ch.

Spacing

A rhythm, never one uniform value: tight inside a group, generous between.

  • --space-2xs0.25rem · 4px
  • --space-xs0.5rem · 8px
  • --space-sm0.75rem · 12px
  • --space-md1.25rem · 20px
  • --space-lg2rem · 32px
  • --space-xl3.25rem · 52px
  • --space-2xl5.25rem · 84px
  • --space-3xl8.5rem · 136px

Radius

--radius12pxcards top out here (anti-slop); pills are for tags/buttons only
999pxThe full pill, for buttons and chips only.

Elevation

Flat by design. Depth comes from the stage’s own light and from space between things; the one soft shadow belongs to the glass Button, which floats over imagery.

Glass and scrim

Glass appears in exactly one place, the Button, by a logged waiver. The theater view lays a dark gradient scrim behind a placard’s words. Neither is used as decoration.

Motion

The eases share the 1500ms beat, so only the curve differs; the durations share --ease-settle.

  • --ease-settlecubic-bezier(0.22, 1, 0.36, 1)the signature: answering -> inhabiting
  • --ease-entercubic-bezier(0.16, 1, 0.30, 1)wing reveals, door openings
  • --ease-driftcubic-bezier(0.37, 0, 0.63, 1)ambient sway, breathing, loops
  • --dur-quick240ms
  • --dur-reveal640ms
  • --dur-settle1500msthe gears' synced harmony beat — PRD §3.3

Contrast

Measured live from the tokens the browser is running, against WCAG AA.

PairSampleRatioWCAG AA
Primary text on page--silver on --spaceAa 12316.84:1Pass ≥ 4.5
Placard title on page--white on --spaceAa 12320.76:1Pass ≥ 4.5
Secondary text on page--slate on --spaceAa 1238.10:1Pass ≥ 4.5
Text on the darkest metal--silver on --graphiteAa 12311.99:1Pass ≥ 4.5
Primary label on the glass pill--synapse on --spaceAa 1238.34:1Pass ≥ 4.5
Link hover on page--teal-link on --spaceAa 1235.12:1Pass ≥ 4.5
Large text on page--steel on --space--steel is a gear-metal tone first; as text it is held to the large-text bar.Aa 1234.36:1Pass ≥ 3, large text
Focus ring vs page--synapse on --space8.34:1Pass ≥ 3, non-text

Components

The site’s own parts, rendered from the same files the rooms use. Hover, focus and pressed are forced side by side from the components’ real CSS.

Button

The one clickable action, in the glass-pill, mono-caps recipe shared with anujaharsha.com. Color is the only difference between variants.

Use when

  • A deliberate action or a step forward
  • Moving to another place (it renders a link when given an href)
  • Leaving the site (external opens a new tab)

Don’t

  • Text inside a sentence (use a plain link)
  • More than one primary per moment
  • Decoration: it is the one place glass is allowed

States × variants

VariantDefaultHoverFocusPressedDisabled
primary
Not in the system
secondary
Not in the system
ghost
Not in the system

More versions

Back to the mindAs an internal link
Meet AnujaAs an external link
With an icon
Over imagery: the glass at work

Live, try it

Hover, tab to, or press a button.
View code
import Link from "next/link";
import type { ReactNode } from "react";
import styles from "./Button.module.css";

/**
 * Button — borrowed from the sibling site (anujaharsha.com) so the two feel
 * related: the glass-pill, mono-caps, wide-tracking "whispering intensity"
 * recipe. Re-skinned to the mind's extracted tokens. Color is the only thing
 * that differs between variants.
 *
 * Polymorphic: next/link for internal href, <a target=_blank> for external,
 * <button> otherwise.
 */
type Variant = "primary" | "secondary" | "ghost";

type Common = {
  variant?: Variant;
  icon?: ReactNode;
  className?: string;
  children?: ReactNode;
  "aria-label"?: string;
};

type Props =
  | (Common & { href: string; external?: boolean })
  | (Common & { href?: undefined; onClick?: () => void });

export default function Button(props: Props) {
  const { variant = "primary", icon, className = "", children } = props;
  const cls = `${styles.pill} ${styles[variant]} ${className}`.trim();
  const inner = (
    <span className={styles.inner}>
      {icon}
      {children}
    </span>
  );

  if ("href" in props && props.href) {
    if (props.external) {
      return (
        <a
          href={props.href}
          target="_blank"
          rel="noopener noreferrer"
          className={cls}
          aria-label={props["aria-label"]}
        >
          {inner}
        </a>
      );
    }
    return (
      <Link href={props.href} className={cls} aria-label={props["aria-label"]}>
        {inner}
      </Link>
    );
  }

  return (
    <button
      type="button"
      onClick={(props as { onClick?: () => void }).onClick}
      className={cls}
      aria-label={props["aria-label"]}
    >
      {inner}
    </button>
  );
}
/* The shared glass pill — mirrors the sibling site's Button surface, re-skinned
   to mind tokens. Mono caps, wide tracking, rounded-full, subtle blur, press. */
.pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  white-space: nowrap;
  border-radius: 999px;
  padding: 0.72rem 1.35rem;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-size: 0.72rem;
  line-height: 1;
  background: color-mix(in srgb, var(--space) 55%, transparent);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  box-shadow: 0 12px 44px -14px rgba(0, 0, 0, 0.6);
  border: 1px solid var(--edge);
  color: var(--ink-quiet);
  cursor: pointer;
  transition: color var(--dur-quick) var(--ease-enter),
    border-color var(--dur-quick) var(--ease-enter),
    background-color var(--dur-quick) var(--ease-enter),
    transform var(--dur-quick) var(--ease-enter);
}
.pill:active {
  transform: scale(0.97);
}
.pill:focus-visible {
  outline: 2px solid var(--synapse);
  outline-offset: 2px;
}

.inner {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

/* secondary — silver border + slate text (the default). */
.secondary {
  border-color: color-mix(in srgb, var(--silver) 16%, transparent);
  color: var(--slate);
}
.secondary:hover {
  border-color: color-mix(in srgb, var(--silver) 30%, transparent);
  background: color-mix(in srgb, var(--space) 72%, transparent);
  color: var(--silver);
}

/* primary — synapse-teal border + teal text (the forward action). */
.primary {
  border-color: color-mix(in srgb, var(--synapse) 40%, transparent);
  color: var(--synapse);
}
.primary:hover {
  border-color: color-mix(in srgb, var(--synapse) 65%, transparent);
  background: color-mix(in srgb, var(--space) 72%, transparent);
  color: var(--silver);
}

/* ghost — bare mono-caps text link for tertiary actions. */
.ghost {
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
  border: none;
  padding: 0.25rem 0;
  color: var(--slate);
}
.ghost:hover {
  color: var(--silver);
}

Gear

The recurring motif, carried from the brain into every room. In a room, a turning gear marks a work still being made.

Use when

  • A work in progress, held in its place
  • A quiet mark of the gear world beside a heading or an action

Don’t

  • As a button on its own (it is decorative and hidden from assistive tech)
  • Recolored or glowing: teeth stay --steel, body and hub --slate

States × variants

VariantTurningReduced motion
cw
ccw

More versions

24
44 (default)
72, sixteen teeth
8 teeth

Live, try it

View code
import styles from "./Gear.module.css";

/**
 * A clean vector gear — the site's recurring motif, carried from the brain into
 * every wing. Turns slowly on its own axis (CSS, reduced-motion aware). Purely
 * decorative unless labelled.
 */
export default function Gear({
  size = 44,
  teeth = 12,
  spin = "cw",
  className,
}: {
  size?: number;
  teeth?: number;
  spin?: "cw" | "ccw";
  className?: string;
}) {
  const cx = 50;
  const cy = 50;
  const rOuter = 46;
  const rInner = 37;
  const rBody = 33;
  const rHub = 12;
  const half = 360 / teeth / 2;

  // Build the toothed outline as a single path around the gear.
  const pt = (r: number, deg: number) => {
    const a = (deg * Math.PI) / 180;
    return `${(cx + r * Math.cos(a)).toFixed(2)},${(cy + r * Math.sin(a)).toFixed(2)}`;
  };
  let d = "";
  for (let i = 0; i < teeth; i++) {
    const a = (360 / teeth) * i;
    d += `${i === 0 ? "M" : "L"}${pt(rInner, a - half)} L${pt(rOuter, a - half * 0.55)} L${pt(
      rOuter,
      a + half * 0.55
    )} L${pt(rInner, a + half)} `;
  }
  d += "Z";

  return (
    <svg
      className={`${styles.gear} ${className ?? ""}`}
      data-spin={spin}
      width={size}
      height={size}
      viewBox="0 0 100 100"
      aria-hidden="true"
      focusable="false"
    >
      <path d={d} className={styles.teeth} />
      <circle cx={cx} cy={cy} r={rBody} className={styles.body} />
      <circle cx={cx} cy={cy} r={rHub} className={styles.hub} />
    </svg>
  );
}
.gear {
  display: block;
  transform-origin: center;
  animation: gearTurn 26s linear infinite;
}
.gear[data-spin="ccw"] {
  animation-direction: reverse;
}

.teeth {
  fill: var(--steel);
}
.body {
  fill: none;
  stroke: var(--slate);
  stroke-width: 3;
  opacity: 0.7;
}
.hub {
  fill: var(--slate);
  opacity: 0.55;
}

@keyframes gearTurn {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .gear {
    animation: none;
  }
}

Placard link

The call to action on a room placard, the 30 of every 70/30 spotlight. The placard runs in museum order: title + subtext right. No eyebrows, anywhere, ever.

Use when

  • The one next step a spotlight offers (a live demo, a visit)
  • Leaving to another of her sites

Don’t

  • More than one per placard
  • Above the title: it always sits after the words

States × variants

VariantDefaultHoverFocusPressedDisabled
internal
Not in the system
external
Not in the system

More versions

A placard title

One or two lines of description sit beneath the title, never above it.

Live demo ↗
In a full placard

Live, try it

View code
function SpotCtaLink({ cta }: { cta: NonNullable<RoomData["spots"][number]["cta"]> }) {
  const isMail = cta.href.startsWith("mailto:");
  // mailto + external open outside the router (plain anchor); ↗ only for the web
  if (isMail || cta.external) {
    return (
      <a
        className={styles.cta}
        href={cta.href}
        {...(isMail ? {} : { target: "_blank", rel: "noopener noreferrer" })}
      >
        {cta.label}
        {isMail ? "" : " ↗"}
      </a>
    );
  }
  return (
    <Link className={styles.cta} href={cta.href}>
      {cta.label}
    </Link>
  );
}
.cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  margin-top: var(--space-lg);
  padding: 0.55em 1.2em;
  border: 1px solid color-mix(in srgb, var(--synapse) 45%, transparent);
  border-radius: 999px;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  font-size: 0.66rem;
  color: var(--ink);
  transition: border-color 220ms var(--ease-enter), background 220ms var(--ease-enter);
}

.cta:hover,
.cta:focus-visible {
  border-color: var(--synapse);
  background: color-mix(in srgb, var(--synapse) 12%, transparent);
}

Language chip

Switches a poem between its own script, English and Korean, without leaving the placard.

Use when

  • A poem that exists in more than one language

Don’t

  • A single-language poem (the switch is not rendered)
  • Navigation or filters elsewhere

States × variants

VariantDefaultHoverFocusPressedSelected
अ
EN
한

Live, try it

View code
<div className={styles.langSwitch} role="group" aria-label="Poem language">
  {opts.map((o) => (
    <button
      key={o.key}
      type="button"
      className={styles.langChip}
      data-active={cur === o.key ? "" : undefined}
      aria-pressed={cur === o.key}
      lang={o.htmlLang}
      onClick={() => setLangBy((m) => ({ ...m, [i]: o.key }))}
    >
      {o.label}
    </button>
  ))}
</div>
.langSwitch {
  display: inline-flex;
  gap: 6px;
  align-self: flex-start;
  margin-top: var(--space-sm);
}

.langChip {
  min-width: 2.4em;
  padding: 0.4em 0.75em;
  background: none;
  border: 1px solid color-mix(in srgb, var(--slate) 30%, transparent);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-quiet);
  cursor: pointer;
  transition: border-color 200ms var(--ease-enter), color 200ms var(--ease-enter),
    background 200ms var(--ease-enter);
}

.langChip:hover,
.langChip:focus-visible {
  color: var(--ink);
  border-color: color-mix(in srgb, var(--synapse) 45%, transparent);
}

.langChip[data-active] {
  color: var(--white);
  border-color: color-mix(in srgb, var(--synapse) 60%, transparent);
  background: color-mix(in srgb, var(--synapse) 12%, transparent);
}

Story toggle

Opens the written story beneath a film, so a long read never needs a popup.

Use when

  • A long read that belongs to a piece already on screen

Don’t

  • Hiding anything essential to understanding the piece

States × variants

VariantDefaultHoverFocusPressed
closed
open

Live, try it

View code
<button
  type="button"
  className={styles.storyToggle}
  aria-expanded={on}
  onClick={() => setStoryOpen((m) => ({ ...m, [ei]: !m[ei] }))}
>
  {on ? "Hide the story" : "Read the story"}{" "}
  <span aria-hidden="true">{on ? "↑" : "↓"}</span>
</button>
.storyToggle {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 0.7em 1.4em;
  background: none;
  border: 1px solid color-mix(in srgb, var(--slate) 40%, transparent);
  border-radius: 999px;
  font-family: var(--font-mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-size: 0.74rem;
  color: var(--ink);
  cursor: pointer;
  transition: border-color 220ms var(--ease-enter), color 220ms var(--ease-enter);
}

.storyToggle:hover,
.storyToggle:focus-visible {
  border-color: var(--teal-link, #078b9c);
  color: var(--teal-link, #078b9c);
}

Handwrite

Draws a word one glyph at a time, as if written by hand: each outline traces in on --ease-enter, then inks.

Use when

  • A single name or word that deserves a signature moment

Don’t

  • Sentences or body copy
  • More than one at a time on screen

States × variants

VariantBefore it starts (not yet drawn)Written
Latin
Hangul

Live, try it

View code
"use client";

import type { CSSProperties } from "react";
import type { HandGlyphs } from "@/data/hero-handwriting";
import styles from "./Handwrite.module.css";

/**
 * Handwrite — draws a word one glyph at a time, as if written by hand. Each
 * glyph's outline is traced (stroke-dashoffset) then inked in (fill), staggered
 * left→right. Paths are pre-generated in font-em units (baseline y=0), so the
 * SVG is sized in `em` and matches the surrounding font-size exactly. Under
 * reduced-motion it shows solid immediately (see the CSS).
 */
export default function Handwrite({
  data,
  start,
  ariaLabel,
  lang,
  baseDelay = 0,
  stagger = 70,
  duration = 900,
}: {
  data: HandGlyphs;
  start: boolean;
  ariaLabel: string;
  lang?: string;
  baseDelay?: number;
  stagger?: number;
  duration?: number;
}) {
  const { em, width, ascent, descent, glyphs } = data;
  const vbH = ascent - descent;
  const style = {
    width: `${width / em}em`,
    height: `${vbH / em}em`,
    "--hw-base": `${baseDelay}ms`,
    "--hw-stagger": `${stagger}ms`,
    "--hw-dur": `${duration}ms`,
  } as CSSProperties;

  return (
    <svg
      className={styles.svg}
      viewBox={`0 ${-ascent} ${width} ${vbH}`}
      style={style}
      role="img"
      aria-label={ariaLabel}
      lang={lang}
      data-draw={start ? "" : undefined}
    >
      {glyphs.map((d, i) => (
        <path
          key={i}
          d={d}
          pathLength={1}
          className={styles.stroke}
          style={{ "--hw-i": i } as CSSProperties}
        />
      ))}
    </svg>
  );
}
.svg {
  display: block;
  overflow: visible;
}

/* each glyph: a filled letter whose outline traces in (stroke draw), then inks
   (fill fades up). Stroke + fill are both currentColor, so there's no seam. */
.stroke {
  fill: currentColor;
  fill-opacity: 0;
  stroke: currentColor;
  stroke-width: 26; /* font-em units ≈ ~0.9px at the hero size */
  stroke-linejoin: round;
  stroke-linecap: round;
  stroke-dasharray: 1; /* pathLength normalised to 1 */
  stroke-dashoffset: 1;
}

/* only animate once the portrait is ready (data-draw set by the parent) */
.svg[data-draw] .stroke {
  animation: handwrite var(--hw-dur, 900ms) var(--ease-enter, ease) forwards;
  animation-delay: calc(var(--hw-base, 0ms) + var(--hw-i, 0) * var(--hw-stagger, 70ms));
}

@keyframes handwrite {
  0% {
    stroke-dashoffset: 1;
    fill-opacity: 0;
  }
  58% {
    stroke-dashoffset: 0;
    fill-opacity: 0;
  }
  100% {
    stroke-dashoffset: 0;
    fill-opacity: 1;
  }
}

/* reduced-motion: no drawing — the word is simply present and solid */
@media (prefers-reduced-motion: reduce) {
  .stroke {
    fill-opacity: 1;
    stroke-width: 0;
    stroke-dashoffset: 0;
    animation: none;
  }
}

The world

The gear-brain experience is a signature surface. It is documented here and never re-rendered, restyled or wrapped; to see it, step into the mind.

ComponentPurposeStates
BrainActorThe one persistent brain, morphed between states instead of remounting.center · corner · hidden
WorldShellThe one continuous world; never remounts across navigations.one
ConstellationThe nine options, always visible, arranged around the brain.one
CornerNavThe current room's frozen thumbnail answers where am I.one
ThumbnailsPhase-based mini-narratives, three to five seconds each.round · loop · frozen
GearCursorA dot with a trailing gear; touch and reduced motion keep the native cursor.one
PhoneGateThe world is desktop-first; phones get a warm gate.one

Rules

Do

  • All colors come from the token palette extracted from the brain SVG. No invented colors.
  • Animate transform/opacity only.
  • Spacing has rhythm: tight within groups, generous between sections — never one uniform value.
  • No nested cards; flatten with spacing/typography/dividers.
  • WCAG AA minimum everywhere (4.5:1 body, 3:1 large).

Don’t

  • No bounce/elastic easing on UI; ease-out smoothly (project's three named eases only).
  • No oversized italic serif hero, no eyebrow/pill kicker above the headline, no repeated tiny uppercase section labels.
  • No glassmorphism, frosted blur, or glow borders as decoration — only if solving a real layering problem.
  • Banned: glassmorphism, Geist/Inter/Space Grotesk/Instrument Serif as primary, gradient text, purple/violet gradients, side-tab card stripes, icon-in-tile feature cards, bounce/elastic easing.

Spec decisions

Values the shared spec template sets where the site has none, so shared components can render there. None of them is used on this site.

TokenValueWhy
--surface-1 / -2#0d0f12 / #1a1c1fThe site has no card ramp; steps between space and graphite so components can render. Not a request to add cards.
--positive / --caution / --dangergreen / amber / redThe mind has no semantic colors — these exist only so shared components render; they would break Rule Zero if used on the site.
--accent-strong / -soft#2dd4bf / 14%Hover and tint for the one teal.