diff --git a/design.md b/design.md new file mode 100644 index 0000000..a6813c8 --- /dev/null +++ b/design.md @@ -0,0 +1,110 @@ +# Design System — Ryan Moore (ryantmoore.ca) + +**Style:** Swiss / International Typographic Style +**Principle:** One typeface, expressed through weight and size only. Flush-left grid. Generous whitespace. Restrained brand-blue accent. + +--- + +## Typeface + +**Archivo Variable** (`/public/fonts/archivo-variable-latin.woff2`) +Self-hosted. Latin subset. Weight axis: 100–900. Loaded via `@font-face` in `global.css`, preloaded in `Base.astro`. + +One family across the entire site — display, body, labels, UI. No secondary typefaces. + +```css +--display: "Archivo", -apple-system, BlinkMacSystemFont, sans-serif; +--sans: "Archivo", -apple-system, BlinkMacSystemFont, sans-serif; +--label: "Archivo", -apple-system, BlinkMacSystemFont, sans-serif; +``` + +--- + +## Type Scale + +1.25 ratio (Major Third), anchored at 1rem / 16px. +`size = 1rem × 1.25ⁿ` + +| Token | Value | Pixels | Usage | +|---------------|-------------|---------|------------------------------------------| +| `--text-2xs` | 0.64rem | 10.24px | Diagram sub-labels, micro tags | +| `--text-xs` | 0.8rem | 12.8px | Kickers, nav links, meta labels, tags | +| `--text-sm` | 0.875rem | 14px | Secondary body, button text | +| `--text-base` | 1rem | 16px | Primary body copy, form inputs | +| `--text-lg` | 1.25rem | 20px | Card headings, service titles, timeline | +| `--text-xl` | 1.5625rem | 25px | Sub-headings, pull-quote minimum | +| `--text-2xl` | 1.953rem | 31.25px | Section h2 minimum, post titles | +| `--text-3xl` | 2.441rem | 39px | Section h2 maximum, case study headings | +| `--text-4xl` | 3.052rem | 48.8px | Hero baseline, h1 minimum | +| `--text-5xl` | 3.815rem | 61px — Hero mid, case study title max | +| `--text-6xl` | 4.768rem | 76.3px | Hero headline maximum | + +**Rule:** Never hardcode a `font-size`. Use a scale token or `clamp(token, vw, token)`. + +--- + +## Weights + +Restrained set — four stops only: + +| Weight | Use | +|--------|------------------------------------------------------------| +| 400 | Body copy, prose paragraphs | +| 500 | Nav links, kicker labels, button text | +| 700 | Headings (h1–h3), section titles, card headings | +| 800 | Hero headline, large display numerics (price, metrics) | + +--- + +## Leading & Tracking + +| Context | `line-height` | `letter-spacing` | +|---------------------|---------------|-------------------------| +| Body copy | 1.6 | default (0) | +| Prose paragraphs | 1.75 | default (0) | +| Display headings | 0.96–1.02 | −0.025em to −0.04em | +| Kickers / labels | default | +0.22em (tracked caps) | +| Nav / UI labels | default | +0.03–0.05em | + +--- + +## Grid & Layout + +- **Max width:** `--wrap: 1180px`, centered with `margin-inline: auto` +- **Narrow column:** `820px` for prose, lab posts, case study body +- **Gutter:** `--gut: clamp(1.25rem, 4vw, 3rem)` — both sides +- **Default alignment:** flush-left / ragged-right everywhere +- **Center alignment:** only for pull-quotes (`.quotes`) — deliberate typographic pause + +--- + +## Colour + +One accent, always `--accent: #1f4fff`. + +| Token | Value | Role | +|-----------------|------------|------------------------------------| +| `--ink` | `#f7f5f0` | Page background | +| `--ink-2` | `#fffefb` | Raised panels, cards | +| `--ink-3` | `#efece4` | Hover / alt fill | +| `--paper` | `#14140f` | Primary text | +| `--paper-dim` | `#51504a` | Secondary text | +| `--paper-faint` | `#6d6b60` | Tertiary text (min WCAG AA on ink) | +| `--accent` | `#1f4fff` | Brand blue — the one signal colour | +| `--accent-2` | `#1a3fd6` | Accent hover / pressed | +| `--accent-hi` | `#5a86ff` | Accent on dark hero | + +--- + +## Pages Covered + +Every page and component in the site inherits from `global.css` via `Base.astro`. The type system applies to: + +- **Home (`/`)** — hero, work rows, projects grid, services, timeline, audit section, contact form +- **Case studies (`/work/[slug]`)** — hero, meta table, metrics, prose body, next-study footer +- **Lab index (`/lab/`)** — header, post list rows +- **Lab posts (`/lab/[slug]`)** — prose body, post meta +- **404** — error page +- **Nav** — brand mark, name, links, CTA +- **Footer** — meta, links +- **Schematic** (`Schematic.astro`) — SVG labels inherit `--label` / `--display` tokens; px sizes intentionally kept for SVG coordinate space diff --git a/public/fonts/archivo-variable-latin.woff2 b/public/fonts/archivo-variable-latin.woff2 new file mode 100644 index 0000000..716d4ab Binary files /dev/null and b/public/fonts/archivo-variable-latin.woff2 differ diff --git a/public/fonts/hanken-grotesk-latin.woff2 b/public/fonts/hanken-grotesk-latin.woff2 deleted file mode 100644 index 518e2c4..0000000 Binary files a/public/fonts/hanken-grotesk-latin.woff2 and /dev/null differ diff --git a/src/components/Schematic.astro b/src/components/Schematic.astro index b76c23e..4989294 100644 --- a/src/components/Schematic.astro +++ b/src/components/Schematic.astro @@ -4,6 +4,14 @@ // Ryan integrates orbit it and exchange data along glowing cables. The story is "I build // the product at the centre AND wire your whole stack into it", not "we run a platform". +// AGENTIC LOOP — Direction B (July 2026 redesign): +// Each agent action is a VISIBLE ROUND-TRIP: a packet leaves the core → reaches a tool +// → a return packet traces a bowed return arc back on the OPPOSITE side of the cable, +// so out+return form a lasso/eye shape. The cycle is in the motion itself, grounded in +// the real tool stack (HubSpot, Stripe, Twilio, etc.) that already orbits the core. +// A small circular-arrow glyph on the core marks where observations land. +// No abstract decorative ring; no Plan/Act/Observe text labels. + // Monochrome brand glyphs, tinted via CSS (fill: currentColor inherits .sch-glyph). // simple-icons live in a 24-unit box; Twilio's mark is a 64-unit box (ring + 4 dots). const GLYPHS: Record = { @@ -31,14 +39,7 @@ const C = { x: 600, y: 300 }; const R_sun = 64; // cables meet the sun's body here const ORBIT = { inner: { rx: 300, ry: 118 }, outer: { rx: 480, ry: 172 } }; -// Services that orbit the product. `logo` -> a GLYPHS key. Each sits on a tilted -// elliptical orbit at angle `deg` (0°=right, 90°=front/bottom); `s` is the depth scale -// (front-of-orbit nodes read larger/brighter/nearer). `bow` curves its cable. Asymmetric -// on purpose — a loose constellation, not a clock face. -// `dir` is which way this service's data packets travel: most feed 'in' to the product, -// but the services the hero outcomes name as destinations — bookings out to the calendar, -// orders out to the books, texts out via Twilio — flow 'out', so the diagram shows the -// two-way conversation the lede promises ("so they finally talk to each other"). +// Services that orbit the product. const sources = [ { name: 'HubSpot', logo: 'hubspot', orbit: 'outer', deg: 145, s: 1.12, bow: 20, dir: 'in' }, { name: 'Stripe', logo: 'stripe', orbit: 'outer', deg: 35, s: 1.05, bow: -18, dir: 'in' }, @@ -57,7 +58,6 @@ type Node = (typeof sources)[number]; function geom(n: Node) { const o = ORBIT[n.orbit as 'inner' | 'outer']; const t = (n.deg * Math.PI) / 180; - // the anchor sits exactly ON the orbit ellipse — the planet's true position const ax = C.x + o.rx * Math.cos(t); const ay = C.y + o.ry * Math.sin(t); const s = n.s; @@ -68,8 +68,6 @@ function geom(n: Node) { const padL = 16 * s, gap = 11 * s, padR = 18 * s; const textW = n.name.length * font * 0.6; const w = padL + markerW + gap + textW + padR; - // the label card hangs just OUTSIDE its anchor (radially away from the sun), so the - // glowing anchor node stays visible on the orbit line with the card attached to it const vx = ax - C.x, vy = ay - C.y; const vl = Math.hypot(vx, vy) || 1; const ux = vx / vl, uy = vy / vl; @@ -78,21 +76,19 @@ function geom(n: Node) { const cy = ay + uy * off; const x = cx - w / 2; const y = cy - h / 2; - // nearer nodes read brighter; further ones recede into the field const depth = +(0.55 + 0.45 * ((s - S_MIN) / (S_MAX - S_MIN))).toFixed(3); const labelX = x + padL + markerW + gap; return { ...n, ax, ay, cx, cy, s, font, h, w, x, y, markerW, padL, gap, depth, hasLogo, labelX }; } const N = sources.map(geom); -// each cable runs from the planet's orbit anchor inward to the sun's body, gently -// bowed so the constellation reads as cables rather than rigid spokes. -// packets travel anchor -> sun for dir:'in' cables, sun -> anchor for dir:'out'. +// cable() — the outbound path from tool anchor to core surface. +// bow > 0 curves left-of-centre; bow < 0 curves right-of-centre. function cable(n: ReturnType) { const dx = C.x - n.ax, dy = C.y - n.ay; const dist = Math.hypot(dx, dy) || 1; const ux = dx / dist, uy = dy / dist; - const sx = n.ax + ux * 9, sy = n.ay + uy * 9; // leave the anchor dot crisp + const sx = n.ax + ux * 9, sy = n.ay + uy * 9; const ex = C.x - ux * R_sun, ey = C.y - uy * R_sun; const mx = (sx + ex) / 2, my = (sy + ey) / 2; const px = -(ey - sy), py = ex - sx; @@ -102,8 +98,24 @@ function cable(n: ReturnType) { return `M${r(sx)},${r(sy)} Q${r(qx)},${r(qy)} ${r(ex)},${r(ey)}`; } -// the sun's rays — hand-varied angle / length / width / opacity so the corona reads -// as a radiant star, not a uniform gear. R_ray is where rays begin (just off the body). +// returnCable() — the return arc from core back to the tool, bowed on the OPPOSITE side. +// Together with cable() this forms a lasso/eye shape: act→observe per interaction. +function returnCable(n: ReturnType) { + const dx = C.x - n.ax, dy = C.y - n.ay; + const dist = Math.hypot(dx, dy) || 1; + const ux = dx / dist, uy = dy / dist; + const sx = n.ax + ux * 9, sy = n.ay + uy * 9; + const ex = C.x - ux * R_sun, ey = C.y - uy * R_sun; + const mx = (sx + ex) / 2, my = (sy + ey) / 2; + const px = -(ey - sy), py = ex - sx; + const len = Math.hypot(px, py) || 1; + // Opposite bow: creates the return arc on the other side of the cable + const qx = mx - (px / len) * n.bow, qy = my - (py / len) * n.bow; + const r = (v: number) => v.toFixed(1); + // Return path runs core→tool (reversed start/end from cable()) + return `M${r(ex)},${r(ey)} Q${r(qx)},${r(qy)} ${r(sx)},${r(sy)}`; +} + const R_ray = 72; const rays = [ { ang: 5, len: 34, w: 4, o: 0.80 }, @@ -124,10 +136,13 @@ const rays = [ { ang: 338, len: 26, w: 3, o: 0.40 }, ]; -// One source of truth per cable — base line, drawn-in current line, and the GSAP packet -// all reference the same id/d. The path always runs service -> sun; `dir` tells the -// packet which way to ride it. -const cables = N.map((n, i) => ({ id: `cbl-${i}`, d: cable(n), dir: n.dir })); +const cables = N.map((n, i) => ({ + id: `cbl-${i}`, + retId: `ret-${i}`, + d: cable(n), + retD: returnCable(n), + dir: n.dir, +})); const orbits = [ORBIT.inner, ORBIT.outer]; --- @@ -135,7 +150,7 @@ const orbits = [ORBIT.inner, ORBIT.outer]; class="schematic" viewBox="0 64 1200 488" role="img" - aria-label="A solar-system diagram: a glowing core represents the client's website and app at the centre, with the services Ryan integrates (HubSpot, Stripe, Drupal, Twilio, Shopify, WordPress, QuickBooks, Google Calendar, Workiz and Jobber) orbiting it and exchanging data along glowing cables." + aria-label="An orchestration diagram: a glowing core represents the client's website and app at the centre; the services Ryan integrates (HubSpot, Stripe, Drupal, Twilio, Shopify, WordPress, QuickBooks, Google Calendar, Workiz and Jobber) orbit it and exchange data along glowing cables — each interaction is a visible round-trip, action packet out and observation packet back, showing the agentic act-observe loop grounded in the real tool stack." > @@ -147,13 +162,16 @@ const orbits = [ORBIT.inner, ORBIT.outer]; - + + + + @@ -166,6 +184,10 @@ const orbits = [ORBIT.inner, ORBIT.outer]; + + + + @@ -175,21 +197,24 @@ const orbits = [ORBIT.inner, ORBIT.outer]; {orbits.map((o) => )} - + {cables.map((c) => )} + {cables.map((c) => )} - + {cables.map((c) => )} + + {cables.map((c) => )} - + {cables.map((c) => )} + + {cables.map((c) => )} - + {N.map((s) => )} - + {N.map((s) => ( @@ -242,6 +267,15 @@ const orbits = [ORBIT.inner, ORBIT.outer]; + + YOUR WEBSITE & APPS @@ -250,8 +284,7 @@ const orbits = [ORBIT.inner, ORBIT.outer];