# threesided.com — design system

> A reference for anyone (human or model) extending this site.
> Pair this with the live style guide at `/style-guide`.

---

## 1 · Purpose

threesided.com is a contract web-dev showcase. Every visual choice on the
site is *itself* a demo of the kind of work being sold. That gives the design
system one prime directive:

> **The site is the receipt.** When in doubt, do the more interesting thing.

What that means in practice:

- Vanilla CSS (custom properties + cascade) — no UI library, no Tailwind.
- Astro 6 minimal scaffold — content collections for projects, a TS data
  file for the resume-style experience timeline.
- A 3D hero via three.js, scroll-reveals, gradient text, and themed SVG
  "screens" inside each work-history entry — all intentional and all
  lazy-loaded where possible.

---

## 2 · Brand essentials

**Name:** threesided
**Tagline:** *Bleeding-edge web work, shipped without the bleeding.*
**Motif:** three sides — triangles, dice, three-stop gradients, three accents.

### Voice rules

| Rule | Example |
|---|---|
| Lead with the user benefit | "Sub-second loads" ✓  ·  "Vite + SWC bundler" ✗ |
| One adjective max per noun phrase | "Custom interactions" ✓  ·  "Custom, novel, delightful interactions" ✗ |
| Sass with substance | "Names on request" ✓  ·  "The cool kids do it this way" ✗ |
| Mono only in technical contexts | Eyebrows, tech tags, code, file names |
| Sentence case in headings | "The receipts" ✓  ·  "The Receipts" ✗ |

**Tone overall:** playful/weird but never lazy. Confident without bravado.
Specific over generic. Don't apologize, don't oversell.

---

## 3 · Color tokens

All colors live in `src/styles/global.css` under `:root` as CSS custom
properties. Use the tokens, not the hex values.

### Core palette

| Token | Hex | Role |
|---|---|---|
| `--void` | `#0d0a1e` | Background — primary |
| `--void-deep` | `#050316` | Background — recessed (image wells, footer) |
| `--void-soft` | `#1a1530` | Background — raised (elevated cards) |
| `--ghost` | `#f0f0ff` | Foreground — primary text |
| `--hot` | `#ff006e` | Accent — primary (CTAs, intensity) |
| `--cyan` | `#00d9ff` | Accent — secondary (links, info, focus) |
| `--acid` | `#fff95b` | Accent — tertiary (highlights, warnings) |

### Semantic tokens

| Token | Maps to | Use for |
|---|---|---|
| `--bg` | `--void` | Page background |
| `--fg` | `--ghost` | Body text |
| `--muted` | `#b8b8d0` | Secondary text (12.5 : 1 contrast — AAA) |
| `--dim` | `#7a7a95` | Tertiary text (5.6 : 1 — AA only) |
| `--border` | `rgba(255,255,255,0.12)` | Visible borders |
| `--border-soft` | `rgba(255,255,255,0.06)` | Subtle dividers |

### Contrast ratios (vs `--void`)

Calculated against `#0d0a1e` per WCAG 2.1 relative-luminance formula.
Verify with a real tool (e.g. WebAIM Contrast Checker) before quoting publicly.

| Color | Ratio | WCAG |
|---|---|---|
| `--ghost` | 17.1 : 1 | AAA |
| `--acid` | 17.4 : 1 | AAA |
| `--cyan` | 11.2 : 1 | AAA |
| `--muted` | 10.0 : 1 | AAA |
| `--hot` | 5.0 : 1 | AA |
| `--dim` | 4.6 : 1 | AA (normal text — just over the 4.5 : 1 line) |

**Rule:** `--hot` is the lowest-contrast accent. It still passes AA for
normal body text, but for anything below 14px keep it bold or invert
(dark text on a `--hot` background). `--dim` is borderline — don't
shrink it below 14px and don't use it for anything load-bearing.

### Accent rotation

Where there are three of something (services, side-by-side cards), rotate
through the three accents in the order **hot → cyan → acid**. Don't pick at
random — keep it consistent so the system reads as intentional.

---

## 4 · Typography

**Sans (display + body):** Space Grotesk
**Mono (technical):** IBM Plex Mono
**Loaded via Google Fonts preconnect** (see `src/styles/global.css` and
`src/layouts/BaseLayout.astro`).

### Scale

| Class | Size | Weight | Use |
|---|---|---|---|
| `.h1` | `clamp(48px, 7.5vw, 112px)` | 700 | Page headlines (one per page) |
| `.h2` | `clamp(32px, 4.5vw, 64px)` | 700 | Section headers |
| `.h3` | `clamp(22px, 2vw, 28px)` | 600 | Card titles, sub-section heads |
| `.body-lg` | `clamp(18px, 1.4vw, 22px)` | 400 | Section subheads, summaries |
| `.body` | `16px` | 400 | Standard reading text |
| `.eyebrow` | `12px mono` | 600 | Above-headline labels |

### Composition rule

The default section pattern is:

```
[eyebrow]    "// section eyebrow"
[h2]         "Section heading goes here."
[body-lg]    "One-sentence subhead, ~56ch."
[content]    cards / grid / list / etc.
```

If you skip the eyebrow or the subhead, the rhythm of the page breaks.
Add them.

---

## 5 · Components

All components live in `src/components/`. Most have their styles scoped via
Astro's `<style>` block.

### Tags (`.tag`, `.tag--hot/cyan/acid`)

Pill-shaped, monospace, 14px / 600. Used for short labels (≤ 4 words).
**Not for sentences** — if the content is a full sentence (like an
accomplishment), use a bullet list instead.

### Buttons (`.cta`, `.cta--ghost`)

Primary is always `--hot` with a glow shadow. Pair with a `.cta--ghost`
when there are two paths (primary + secondary). Never use color-only to
indicate state — pair color with icon or label.

### Project card (`src/components/ProjectCard.astro`)

Used in the homepage work grid. Each card has:
- a small favicon block (auto-fetched from `url` field, or explicit
  `favicon:` override in markdown frontmatter)
- accent border + corner-dot marker
- title + summary + tech pills
- a date footer + arrow that animates on hover

### Experience card (`src/components/ExperienceCard.astro`)

Timeline-style. Each entry has a stylized SVG "screen" themed to the
industry (`data` / `security` / `martech` / `fintech` / `planning` /
`docs` / `agency` / `media` / `edtech` / `lending` / `defense`).

**Adding a new entry:** edit `src/data/experience.ts`. Choose a `visual`
that doesn't repeat within the same screen — if a visual would repeat,
add a new SVG branch in `ExperienceCard.astro` rather than reusing.

### Client chip (`src/components/ClientChip.astro`)

Smaller card for past client work. Grayscale + low-opacity thumbnail at
rest, full color on hover.

### Hero3D (`src/components/Hero3D.astro`)

Three nested wireframe tetrahedra (hot / cyan / acid). Lazy-loaded on
viewport entry. Mouse-reactive. Respects `prefers-reduced-motion`. Pauses
when the tab is hidden. `aria-hidden` + `tabindex="-1"` — keyboard users
skip past it.

---

## 6 · Motion

**Defaults:** every interactive element transitions ~200ms on hover.
**Easing:** `--ease-out` (cubic-bezier(0.16, 1, 0.3, 1)) for arrivals,
`--ease-snap` for state changes.

### Scroll-reveal

Anything that should fade up on scroll gets `data-reveal`. The
IntersectionObserver in `BaseLayout.astro` handles the rest with a small
stagger (60ms per sibling, capped at 6).

### `prefers-reduced-motion`

Honored globally. All transitions zero out, scroll-reveals reveal
immediately, the 3D scene renders a single static frame.

**Rule:** every new animation must check reduced-motion. The CSS media
query already disables CSS transitions globally — but JS-driven animations
(IntersectionObserver, three.js, etc.) need an explicit guard.

---

## 7 · Accessibility

We target **WCAG 2.1 AA**, exceed it in most places.

### Mandatory

- Semantic HTML: `<main id="main" tabindex="-1">` on every page; one `<h1>`.
- Skip-to-content link on every page (focus the URL bar, tab once).
- `:focus-visible` outline (2px cyan, 3px offset) on every interactive element.
- Decorative SVGs: `aria-hidden="true"` on the parent.
- Decorative images: `alt=""` (NOT missing alt).
- Three.js canvas: `aria-hidden="true"` + `tabindex="-1"`.

### Things to check when adding content

- [ ] Heading order doesn't skip levels.
- [ ] Color isn't the only carrier of meaning.
- [ ] Interactive elements work with keyboard alone.
- [ ] Anything that moves has a reduced-motion fallback.
- [ ] Alt text describes information, not appearance.

---

## 8 · Adding a new section

Quick checklist for adding a new section to the homepage:

1. **Eyebrow → h2 → subhead pattern.** See § 4.
2. **Match the section padding cadence** (`.section` class — clamp 80–140px block padding).
3. **Add a `border-top: 1px solid var(--border-soft)`** if it follows another section.
4. **Pick one accent for the section's eyebrow** — rotate through hot/cyan/acid.
5. **Stagger any cards** with `data-reveal` (the IO handles the rest).
6. **Test reduced-motion** — refresh with the OS toggle on.

## 9 · Adding a new page

1. New file under `src/pages/`. Wrap in `<BaseLayout title="..." description="...">`.
2. Use `<main id="main" tabindex="-1">` so the skip link works.
3. Set page title and description as props.
4. One `<h1>`. Hierarchy from there.
5. Use the existing tokens — don't introduce new colors or sizes ad hoc.
6. If introducing a new pattern, document it here.

---

## 10 · What NOT to do

- Don't add a UI framework. The hand-built CSS is the point.
- Don't pull external icon libraries for the experience SVGs — design new ones in-component.
- Don't use plain `&rsquo;` / `&mdash;` in TypeScript strings — they will render as escaped text. Use real Unicode `'` and `—`.
- Don't use `--hot` (`#ff006e`) for small body text without bold weight. Contrast fails.
- Don't add a new animation without checking `prefers-reduced-motion`.
- Don't name companies in the experience section unless the role was freelance/contract. The standing rule is categorical descriptions only.
- Don't break the eyebrow → h2 → subhead rhythm. It's the page's heartbeat.

---

## 11 · Stack reference

| Layer | Choice |
|---|---|
| Framework | Astro 6 (static output) |
| Styling | Vanilla CSS, CSS custom properties, container queries where useful |
| JS | Minimal; vanilla in `is:inline` scripts; three.js as a dynamic import |
| Fonts | Google Fonts (Space Grotesk + IBM Plex Mono) with preconnect |
| Content | Astro content collections (`projects`); TS data files (`experience`, `clients`) |
| Hosting | Netlify (Node 22, npm run build → dist) |
| Source | github.com/adamaragon/Threesided (private) |

---

*Last updated when this file was committed. Check git history for the latest revision.*
