diff --git a/.gitignore b/.gitignore index 939bab2..10ed0ed 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,10 @@ _site/ .jekyll-cache/ *.icloud archive/ + +# The full CV is deliberately not published here: the site's own position is +# "Fullständigt CV och case-beskrivningar på begäran". The complete design +# system, CV included, lives in the local skill at +# ~/.claude/skills/sandstream-design/. +design-system/assets/peter-cv-*.txt +design-system/assets/peter-cv-*.pdf diff --git a/design-system/CLAUDE_CODE_HANDOFF.md b/design-system/CLAUDE_CODE_HANDOFF.md new file mode 100644 index 0000000..7f6def5 --- /dev/null +++ b/design-system/CLAUDE_CODE_HANDOFF.md @@ -0,0 +1,304 @@ +# Claude Code — Handoff Brief + +> **Goal:** Rebuild `sandstre.am` (currently a Jekyll/Bootstrap "Grayscale" +> template) as a modern static site implementing this design system, +> with the case studies and bio captured in the prototype. + +This brief is meant to be opened by Claude Code in a fresh terminal, +pointed at this folder. Everything Claude Code needs to ship the site +is in this project. Read this brief first, then start. + +--- + +## 0 · Setup commands + +```bash +# Clone the existing site — it already has the right CNAME (www.sandstre.am) +# and a few historical demos under sub-paths. Don't start fresh; preserve git +# history and keep the live DNS. +git clone https://github.com/sandstream/sandstream.github.io.git +cd sandstream.github.io + +# Strip the Jekyll machinery. We're staying on GitHub Pages but going +# plain-static (no build step, no Jekyll). Move the old stuff to archive/ +# so URLs like sandstre.am/djr/ keep working. +mkdir archive +git mv djr ngcs ica lullabys.html archive/ 2>/dev/null || true +git mv _includes _layouts _site _config.yml "_config copy.yml" archive/_jekyll/ 2>/dev/null || mkdir -p archive/_jekyll +git mv css js dist node_modules package.json package-lock.json archive/_jekyll/ 2>/dev/null || true + +# Add an empty .nojekyll to opt out of GitHub's Jekyll build step entirely. +# Without this, GH Pages tries to interpret our static files as Jekyll input. +touch .nojekyll + +# Keep CNAME at root — it's already correct (www.sandstre.am). +``` + +**Stack choice — plain static HTML, no build step.** + +The prototype in `ui_kits/portfolio/` uses React via Babel-standalone +in the browser. For a 1-page portfolio with a few hundred visits/month, +**that setup is production-shippable as-is.** No build pipeline, no +GitHub Actions, no Vercel — push to `master`, GitHub Pages serves it. +Lighthouse will still hit ≥ 90 with the simple optimisations below. + +**Production file layout:** + +``` +sandstream.github.io/ +├── .nojekyll # opt out of Jekyll build +├── CNAME # www.sandstre.am — keep as-is +├── index.html # ported from ui_kits/portfolio/index.html +├── portfolio.css +├── colors_and_type.css # copy in from this design system +├── App.jsx +├── Header.jsx +├── Hero.jsx +├── About.jsx +├── Cases.jsx +├── Roles.jsx +├── Contact.jsx +├── FloatingPill.jsx +├── MetaStrip.jsx +├── sandstre/ # the phonetic-morph easter egg +│ └── index.html +├── assets/ +│ ├── peter-portrait.jpg +│ ├── logo-wordmark.svg +│ ├── logo-wordmark-light.svg +│ └── mark-favicon.svg +└── archive/ # old Jekyll site preserved under sub-paths + ├── djr/ + ├── ngcs/ + ├── ica/ + ├── lullabys.html + └── _jekyll/ # raw source kept for history +``` + +**When to consider a real build step** (later, not now): + +- You add a blog with 5+ posts and want markdown + MDX — switch to + Astro on Vercel. +- You add i18n routing and want `/en/` / `/sv/` as real paths instead + of a query-param toggle — switch to Astro on Vercel. +- React-via-CDN starts noticeably slowing things (e.g. you add 10+ + components and bundle hits ~200KB) — pre-bundle with Vite, push + the built `dist/` to GH Pages via Action. + +For the launch version: don't bother. + +--- + +## 1 · Design system source of truth + +| You want | Read this | +|---|---| +| Brand narrative + tone | `README.md` (root) | +| Color + type tokens | `colors_and_type.css` | +| Brand voice rules | `README.md` § "Content fundamentals" | +| Visual foundations | `README.md` § "Visual foundations" | +| Iconography rules | `README.md` § "Iconography" | +| Working hi-fi reference | `ui_kits/portfolio/index.html` | + +**Copy `colors_and_type.css` into the new project verbatim.** Every +hex value, every spacing token, every font reference is intentional. +Do not introduce new design tokens; if something feels missing, ask +before adding. + +--- + +## 2 · Component → file mapping + +Port these React components straight into the deploy target — they +already use React-via-CDN, which ships fine to GitHub Pages (see §0). +No framework migration needed; just clean filenames, then optionally +pre-compile JSX → JS for perf (see §8). + +| Source (in this design system) | Production target | +|---|---| +| `ui_kits/portfolio/index.html` | `index.html` (repo root) | +| `ui_kits/portfolio/portfolio.css` | `portfolio.css` (repo root) | +| `colors_and_type.css` | `colors_and_type.css` (repo root) | +| `ui_kits/portfolio/Header.jsx` | `Header.jsx` | +| `ui_kits/portfolio/Hero.jsx` | `Hero.jsx` | +| `ui_kits/portfolio/About.jsx` | `About.jsx` | +| `ui_kits/portfolio/Cases.jsx` | `Cases.jsx` + extract `CASES` to JSON | +| `ui_kits/portfolio/Roles.jsx` | `Roles.jsx` + extract `ROLES` to JSON | +| `ui_kits/portfolio/Contact.jsx` | `Contact.jsx` + `Footer` extract | +| `ui_kits/portfolio/FloatingPill.jsx` | `FloatingPill.jsx` | +| `ui_kits/portfolio/MetaStrip.jsx` | `MetaStrip.jsx` (currently unused — drop or wire up) | +| `ui_kits/portfolio/App.jsx` | `App.jsx` | +| `explorations/sandstre-intro.html` | `sandstre/index.html` (easter egg) | + +Extract `CASES` and `ROLES` arrays into JSON or YAML so future Peter +can edit content without touching JSX. The factual data inside them +came from `assets/peter-cv-2026.txt` (kept out of this public copy - see README). + +--- + +## 3 · Assets to use + +| File | What | Where to put it | +|---|---|---| +| `assets/peter-portrait.jpg` | Hero/About portrait | `public/peter.jpg` | +| `assets/logo-wordmark.svg` | Inline wordmark, ink | `src/components/Wordmark.astro` (inline) | +| `assets/logo-wordmark-light.svg` | Inline wordmark, bone | (same component, prop-driven) | +| `assets/mark-favicon.svg` | Favicon + app icon | `public/favicon.svg` | +| `assets/peter-cv-2026.txt` | Source of truth for bio facts | **Not in this public copy.** Local skill only. | + +--- + +## 4 · Fonts + +Use Google Fonts — same three families: + +```html + + + +``` + +If you self-host fonts later for performance, use `font-display: swap` +and preload `Instrument+Serif:ital,wght@1,400` (the italic is on the +critical path for the hero headline). + +--- + +## 5 · Things to NOT redesign + +These are intentional and locked. Do not "improve" them without +explicit approval: + +1. **One italic word per headline.** Every display headline has + exactly one word in ``. Never two. +2. **The bone background.** `#F4F2EC`, not `#FFFFFF`. +3. **The accent.** `#1E8A8A` only. No second accent colour. +4. **Sharp cards.** `border-radius: 0` on cards. 8px is only for buttons. +5. **Hairline borders, not shadows.** Default elevation is a 1px + `--border` inset hairline. The pop shadow exists for ONE element on + the page (the floating pill). +6. **No emoji.** Mono symbols only: `→ · ↗ ↺`. +7. **The mono metadata strips.** Uppercase, narrow tracking, mono. + They appear top of hero, bottom of hero, bottom of contact. They're + the brand's "tech-savvy" signal — keep them. + +--- + +## 6 · Specific requirements / open questions + +Ship these as proper engineering, not design: + +- [ ] **i18n.** Swedish primary, English secondary. Use a query param + (`?lang=en`) or subpath (`/en/`). Headline italicisation pattern + works in both languages — find the right noun per locale. +- [ ] **SEO.** Real ``, `<meta name="description">`, OG image + (use a 1200×630 export of the hero). Schema.org `Person` for + Peter on the homepage. +- [ ] **Analytics.** Use Plausible or Fathom (privacy-friendly, no + cookie banner). Avoid GA. +- [ ] **Calendly embed.** Currently a link to + `cal.eu/sandstream/30min` — confirm with Peter whether to + embed inline on the contact page or keep external. +- [ ] **Real Bolagsverket org-nr.** Hero + Contact + Footer use + placeholder `559123-0000`. Get the real number. +- [ ] **Email form.** `hello@sandstre.am` is mailto-only today. If + Peter wants a real form, add Resend or Plunk. +- [ ] **OG image.** Generate a 1200×630 image: bone background, the + `sandstream` wordmark large, "Tjugo år av ledarskap inom tech." + headline. Use the same fonts. +- [ ] **404 page.** Same rail + a mono "404 / page not found" set in + Instrument Serif italic. Link back home. + +--- + +## 7 · Things the user didn't decide yet — ASK before assuming + +1. ~~**Wordmark direction.**~~ **Decided: Option B (`sandstre.am` + domain-hack).** The `.` and `am` are set in Stream Teal `#1E8A8A`, + the `am` is italic. See `assets/logo-wordmark.svg` and + `preview/logo-light.html`. +2. ~~**Dark mode toggle?**~~ **Decided: ship a toggle, default LIGHT, + persist in localStorage.** Implemented in `Header.jsx` / + `portfolio.css` (the segmented pill in the rail). +3. **Game Day DJ stack.** Listed as iOS + Firebase + WebAudio. + Confirm before publishing. +4. **Ralph Inferno license + repo state.** Currently linking to + `github.com/sandstream/ralph-inferno`. Confirm public + ready. + +### 7a · Easter egg: `sandstre` route + +Peter's existing `index 2.html` (kept from the old repo, see § 0) +animates through phonetic spellings of his name: + +``` +sandström → sandstroem → sandstream → săndstrēm → sandstre.am +``` + +Set this up as a hidden route at **`/sandstre`** (or trigger via +Cmd-K from the main page). Re-implement the `fitter-happier-text` +behaviour with modern CSS — each spelling fits the viewport width, +crossfades to the next every ~1.6s, settles on `sandstre.am` (which +is set in the canonical wordmark style: italic `.am` in Stream Teal), +and fades into a "→ enter site" link back to `/`. + +Use the same Instrument Serif + bone background + grain overlay +treatment as the main site so it feels like a memory of the site, +not a separate page. Add a meta tag `<meta name="robots" content="noindex">` +so it stays an easter egg. + +--- + +## 8 · Deploy + +**Stay on GitHub Pages.** No new platform needed. + +1. Push to `master`. GitHub Pages serves it. +2. The `.nojekyll` file (added in § 0) tells Pages to skip the Jekyll + build step — files are served as-is. +3. `CNAME` is already correct (`www.sandstre.am`). Don't touch it. +4. Verify HTTPS is on in Pages settings → custom domain. +5. Add a redirect from `sandstream.github.io` to `www.sandstre.am` + (this happens automatically once CNAME is set). + +**Performance optimisations to apply before launch:** + +- Preload `Instrument Serif` italic — it's on the critical path for + the hero headline. +- Inline the critical CSS (`colors_and_type.css` first declaration + block + portfolio hero styles) into `<head>`; load the rest async. +- Lazy-load `assets/peter-portrait.jpg` — it's below the fold. +- The `*.jsx` files load via Babel-standalone. Babel transpiles in the + browser (~150KB). To trim this: + - Option A: keep as-is. Total payload still under 250KB. Acceptable. + - Option B: pre-compile JSX → JS once with `npx esbuild *.jsx + --jsx=automatic --outdir=js/`. Ship `.js` and drop Babel from + `<head>`. ~120KB lighter. No build pipeline; just a one-time step + when components change. **Recommended before launch.** + +Keep the existing `sandstream.github.io` repo as-is. The migration is +just a series of commits on `master`, not a new repo. + +--- + +## 9 · Definition of done + +- [ ] All five sections render exactly as the prototype on desktop +- [ ] Mobile (375px) gracefully collapses — rail becomes top nav, + headline reflows, case cards stack +- [ ] Lighthouse: ≥ 95 Performance, ≥ 100 Accessibility, ≥ 95 SEO +- [ ] No layout shift on font load (preload italic Instrument Serif) +- [ ] Both languages (sv default, en secondary) ship with content +- [ ] OG image + meta tags + favicon all set +- [ ] Real org-nr replaces placeholder +- [ ] Domain `sandstre.am` resolves, HTTPS, with redirect from + `www.sandstre.am` and the old `sandstream.github.io` + +--- + +## 10 · Need clarification? + +If something is ambiguous, **read `README.md` first**, then ask +Peter. Don't invent. The voice and visual rules are tight on purpose; +guessing leads to drift. + +— end — diff --git a/design-system/README.md b/design-system/README.md new file mode 100644 index 0000000..d7689d1 --- /dev/null +++ b/design-system/README.md @@ -0,0 +1,348 @@ +# Sandstream Design System + +A 2026 visual + verbal system for **Peter Sandström / Sandstream AB** — +Stockholm-based interim tech leader (CTO / CIO / CPO / CDO). The system +defines a tight, editorial, computationally-literate brand for the +portfolio site at `sandstre.am` and any adjacent decks or one-pagers. + +> _Interim ledning inom Tech._ +> _Strategic technology leadership for the spaces in between._ + +--- + +## What this public copy leaves out + +This repository is public and served at `sandstre.am`. Two things are +therefore deliberately absent, and the full versions live only in the +local Claude Code skill at `~/.claude/skills/sandstream-design/`: + +- **`assets/peter-cv-2026.txt`** — the full CV. The site's own position is + "Fullständigt CV och case-beskrivningar på begäran", so publishing it here + would contradict that. A `.gitignore` rule keeps it from being added by + accident. +- **The phone number in `ui_kits/portfolio/Contact.jsx`** — replaced with a + `[PHONE]` placeholder. `sandstre.am` publishes email and a booking link, + not a mobile number. + +Everything else is here. The portrait and favicon are already served from +the live site, so they add no new exposure. + +--- + +## Sources + +This system was derived from materials the user supplied: + +- **GitHub**: `sandstream/portfolio` — _empty at the time of authoring._ +- **GitHub**: `sandstream/sandstream.github.io` (`master`) — at the time of + authoring (May 2026) this was a Jekyll/Bootstrap portfolio site. It has + since been rebuilt as the plain-static site now live at `sandstre.am`, + which is the result of this design system. Used for: + - Brand identity, name, contact details (`_config.yml`) + - Swedish copy and service positioning + (`_includes/section*.html`, `header.html`, `contact.html`) + - Portrait photography (`img/peter-about.jpg`, copied to + `assets/peter-portrait.jpg`) +- **Brief**: "2026 insanely aesthetic and tech-savvy page." + +That earlier site was content-correct but visually dated (Bootstrap +Grayscale theme, opaque black panel overlays, mixed color accents). The +content was preserved and re-framed; the visuals were rebuilt from zero +with a clear aesthetic position. **That rebuild has shipped** — `sandstre.am` +now runs plain static HTML with these tokens inlined, no build step. + +--- + +## Brand essence + +**Name.** "Sandstream" — _sand_ + _stream_. Time falling in fine grain, +attention flowing, signal collecting. The brand mark holds this tension +without being literal: warm grain neutrals (sand), single sharp accent +(stream / signal), monospace timestamps (time falling). + +**Voice.** Bilingual Swedish/English. Calm authority. No hype, no +emoji, no "🚀 transforming the future of." Specific over general, +concrete over abstract. _Interim_ implies temporary and crucial — the +voice mirrors that: precise, useful, leaves. + +**Positioning.** Twenty-plus years across leadership and innovation +in tech. Interim leader for transitions, scale phases, turnarounds. +Stockholm-based, internationally fluent. + +--- + +## Index — files in this system + +| Path | What | +|---|---| +| `README.md` | This file. The narrative — read first. | +| `SKILL.md` | Agent-compatible skill manifest. | +| `colors_and_type.css` | Single source of truth for color + type tokens. | +| `assets/` | Logos (SVG), portrait photography, favicon mark, Peter's full CV. | +| `preview/` | Small HTML specimens used as cards in the Design System tab. One concept per card. | +| `ui_kits/portfolio/` | The full 2026 portfolio site as a working hi-fi React prototype. | + +There are no slide templates yet — none were provided. Ask if you want +deck templates derived from this system. + +--- + +## Content fundamentals + +### Tone + +**Calm, specific, useful.** The voice is that of an experienced +operator who has been in the room when difficult decisions were made, +and who is comfortable not embellishing. Reads like a senior +consultant's briefing note, not a sales page. + +**Bilingual by default.** Primary surfaces (sandstre.am, decks for +Swedish prospects) lead with **Swedish**. English is the secondary +locale for international engagements. The two are visually +distinguished — Swedish in display serif, English in mono — when shown +together on a page. + +### Casing + +- **Section labels** — `lowercase mono with wide tracking`, often + prefixed with a slash or section number, e.g. + `/01 — interim ledning` or `01 / services`. +- **Display headlines** — sentence case in Instrument Serif, often + with one word italicised for emphasis, e.g. + _Tjugo år av ledarskap i_ tech. +- **Body** — sentence case, no underlines, no bold inside paragraphs. +- **ALL CAPS** is reserved for the wordmark and a single hero call + per page. Don't sprinkle. + +### Person & pronouns + +- **Third person** for Peter on marketing surfaces — "Peter has led…", + "His engagements typically…". This reads as a professional bio, not + a self-promo. +- **Direct second person** for CTAs only — _"Boka ett möte"_, + _"Tell us about the transition"_. +- Company is **"Sandstream"** (one word, capital S). Legal entity is + **"Sandstream AB"**, used only in footers and contractual contexts. + +### Emoji & decorative glyphs + +**No emoji.** Ever. Use mono symbols when a glyph is needed: +`→ / · / ↗ / ◯ / ▮ / ↺`. The arrow `→` is the brand's punctuation — +it appears in links, CTAs, lists. The middle dot `·` separates inline +metadata. The slash `/` prefixes section numbers. + +### Specimens + +> **Headline (SV):** +> _Tjugo år av ledarskap inom_ tech. _Stockholm-baserat, internationellt +> uppdragsbart._ +> +> **Headline (EN):** +> _Two decades of technical leadership._ Stockholm-based, available +> worldwide. + +> **Subhead (SV):** Strategisk översikt och praktisk teknisk insikt +> för digital transformation, interimsuppdrag och skalresa. + +> **Bio sentence:** Peter har varit Managing Director, CPO, CTO, CIO +> och CDO. Just nu mellan uppdrag. + +> **CTA:** Boka 30 min → · Email · LinkedIn + +> **Meta strip (mono):** +> `STOCKHOLM, 59.33°N` · `AVAILABLE Q3 2026` · `v2026.05` + +### Things the voice never does + +- Hype words: "revolutionary", "game-changing", "world-class", "10×" +- Filler verbs: "leverage", "unlock", "supercharge", "synergize" +- Bullet salad — prose first, lists only when items are genuinely + parallel +- Emoji, caps lock for emphasis, exclamation marks, em-dashes between + every clause + +--- + +## Visual foundations + +### The system in one sentence + +> A warm-cool bone neutral page, an editorial serif headline with one +> italicised word, a precise sans body, mono metadata strips, and one +> stream-teal signal accent. No more colours. + +### Colour + +**Three layers, one accent.** + +- **Bone** (`#F4F2EC → #CFC8B5`) — warm-cool neutral page and surfaces. + Always slightly off-white with a faint green-grey cast; never pure + `#FFFFFF`. The page _feels_ like recycled paper, not bleach. +- **Ink** (`#0E1A1A → #2C3838`) — near-black text and dark mode + surfaces. Slight teal cast lines up with the signal accent so dark + surfaces feel like the same family, not a different brand. +- **Stream teal** (`#1E8A8A`) — the single signal accent. Used for + primary CTAs, marks, the wordmark dot, link hover. **One signal per + view.** If you find yourself reaching for a second accent colour, you + are wrong; restate the hierarchy first. +- **Warm second** (`#C77B3A`) — a warm copper reserved exclusively for + data visualisation, charts, and one-off "heat" indicators where teal + alone can't differentiate categories. Never decorative. + +The old site's palette (`#40E0D0`, `#FF1493`, `#9400D3`) is **retired.** + +### Typography + +Three families, no more. + +| Role | Family | Source | +|---|---|---| +| Display | **Instrument Serif** | Google Fonts | +| UI / body | **Geist** | Google Fonts | +| Mono | **JetBrains Mono** | Google Fonts | + +Instrument Serif's italic is a _signature move_ — it carries one word +per headline and nothing else. The combination gives Sandstream a +voice that reads "editorial weekly" and "technical bulletin" at the +same time. + +> _Font substitution note._ No proprietary fonts are licensed in the +> existing brand, so the system uses freely-available Google Fonts +> across the board. If Peter wants to commission a proprietary type +> license (e.g. ABC Diatype, Söhne, or PP Editorial Old), the system's +> token names map cleanly to that swap. **Ask Peter if a brand-type +> upgrade is desired.** + +### Spacing + +A 4px base scale (4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 / 96 / 128 / +192). Most layouts use **only** 16, 24, 32, 64, 96. Don't fill space — +let bone breathe. + +### Layout + +**Asymmetric editorial grids.** 12-column at the page level, but most +content occupies an off-centre 7-col block aligned to a vertical rule. +Metadata sits in the margin as mono captions — never centred. + +A **fixed vertical edge** holds the wordmark + nav on desktop. The +content scrolls past it. + +### Backgrounds + +- Default: **flat bone neutral.** No gradients, no patterns. +- Hero / dark sections: **flat ink** with `--grain` overlay at 50% + opacity. The grain is the brand's "sand." +- Cards: hairline 1px border in `--border`, **no shadow** in light + mode. Cards stand on the page by typography weight, not elevation. +- Imagery: warm, slightly desaturated. The Peter portrait is on flat + black — preserve that crop and treat the black background as part + of the composition. +- **No bluish-purple gradients. No emoji backgrounds. No mesh + gradients. No glassmorphism.** + +### Hover, press, focus + +- **Link hover:** colour shifts from `--fg` to `--accent`. Underline + thickens from 1px to 2px. Duration 140ms, ease-out. +- **Button hover:** background darkens to `--accent-hover`, + translate-y 0px (no lift). Sandstream is grounded, not bouncy. +- **Press:** scale `0.98` for 90ms with `--ease-spring`. Subtle. +- **Focus:** 2px outline in `--accent`, offset 3px. Never disable + focus rings. +- **Cards:** on hover, the hairline border darkens from `--border` to + `--ink-700`. No translate, no shadow. + +### Borders, radii, shadows + +- **Default radius is zero.** Cards, sections, dividers are sharp. +- **8px** for buttons and inputs. +- **Full pill** only for tags, status chips, and the floating + schedule pill. +- **Shadows are rare.** Most surfaces use the inset hairline + (`--shadow-hairline`). The pop shadow (`--shadow-pop`) is used + exactly once per page — on the floating "schedule a call" pill. + +### Motion + +- **Default ease:** `cubic-bezier(0.22, 1, 0.36, 1)` — confident + out-curve. No bounces in functional UI. +- **Spring ease** (`0.34, 1.32, 0.64, 1`) is reserved for press + feedback only. +- **Page entry:** content fades in over 420ms with a 16px upward + translate. Stagger headlines by 60ms. +- **Hero numbers and time stamps** count up on first paint. This is + the only "animated number" pattern in the system. +- **Reduced motion:** all entry animations collapse to a 140ms + opacity fade, no translate. + +### Transparency, blur + +- **No backdrop-blur** anywhere by default. Sandstream is opaque. +- Exception: the floating bottom-right schedule pill on dark hero + uses 24px blur over `rgba(10,10,12,0.4)`. Once per page maximum. + +### Cards + +The card is a near-naked surface: bone fill, 1px hairline border, +generous internal padding (`--space-6` to `--space-7`), no shadow. +Typography does the work. Cards have a `data-tag` corner label in +mono that names them. + +### Imagery treatment + +- **Portrait:** the Peter photo lives on its native flat black + background; the photo's edge is the composition. Never crop to a + circle. A subtle warm gradient overlay (`rgba(30,138,138,0.04)`) is + acceptable when integrating into bone surfaces. +- **Generic imagery:** warm, slightly desaturated, never cool. If a + photo reads "cool/blue" it's wrong for this brand. +- **No stock photography of handshakes, glass office towers, hands + typing on laptops, or rooftop city panoramas.** Use specific Peter + imagery only. + +--- + +## Iconography + +Sandstream avoids iconography for decoration. Function calls for +glyphs in three places, and only three: + +1. **Mono symbols inline** — `→ · ↗ ↺ ▮ ◯` set in JetBrains Mono. + These cover ~90% of icon needs. +2. **Lucide icons via CDN** — for the handful of true UI affordances + (menu, close, copy, external-link, calendar). Lucide's stroke + weight (1.5px) and squared geometry match the system. Linked from: + `https://unpkg.com/lucide@latest/dist/umd/lucide.js` +3. **The wordmark dot** — a single tangerine `·` between "sand" and + "stream" in the wordmark when emphasis is needed. + +> _Substitution note._ The existing site uses **Font Awesome 4** for a +> couple of social/calendar icons. That entire dependency is dropped +> in favour of Lucide via CDN. Lucide is stroked and modern; Font +> Awesome's filled, soft 2013-era forms don't fit the 2026 voice. + +**Never:** +- Emoji in any product surface. +- Coloured icons. Icons are `currentColor` and inherit text colour. +- Icon-only buttons without an accompanying label or aria-label. +- Hand-drawn SVG illustrations. The brand has no illustration + language. Photography and typography carry visual interest. + +--- + +## Quick start for designers / agents + +To produce a design in this system: + +1. Link `colors_and_type.css` and Google Fonts in the document head. +2. Reach for **semantic tokens** (`--bg`, `--fg`, `--accent`) not raw + palette values. +3. Pair a serif display headline with one italicised word, a sans + subhead, and a mono metadata strip. That's the brand voice. +4. Use bone (`#F4F2EC`) or ink (`#0E1A1A`) as the page; never another + colour as the page. +5. One signal accent per view. If the design feels flat, restate the + typographic hierarchy — do not add a second colour. + +See `ui_kits/portfolio/index.html` for a working reference. diff --git a/design-system/SKILL.md b/design-system/SKILL.md new file mode 100644 index 0000000..a2e8c48 --- /dev/null +++ b/design-system/SKILL.md @@ -0,0 +1,58 @@ +--- +name: sandstream-design +description: Use this skill to generate well-branded interfaces and assets for Peter Sandström / Sandstream AB, either for production or throwaway prototypes/mocks/etc. Contains essential design guidelines, colors, type, fonts, assets, and UI kit components for prototyping. +user-invocable: true +--- + +# Sandstream Design — Skill + +Read the `README.md` file within this skill first; it contains the +brand voice, content fundamentals, visual foundations and iconography +rules. Then explore the other files as needed: + +- `colors_and_type.css` — single source of truth for colour + type tokens +- `assets/` — wordmark SVGs, favicon mark, the portrait of Peter, his CV +- `preview/` — small specimen cards showing each token / pattern in use +- `ui_kits/portfolio/` — the working 2026 portfolio site as a hi-fi + React reference (Hero, About, Cases, Roles, Contact, FloatingPill) + +## Use + +If creating visual artifacts (slides, mocks, throwaway prototypes, +landing pages, decks), copy assets out and create static HTML files +for the user to view. Reach for the design system's semantic tokens +(`--bg`, `--fg`, `--accent`) and the three families (Instrument Serif +display, Geist sans, JetBrains Mono). + +If working on production code, you can copy assets and read the rules +to become an expert in designing within Sandstream. + +If the user invokes this skill without other guidance, ask them what +they want to build (a new section on the portfolio site? a one-pager +for an engagement? a deck for a prospect?), ask some clarifying +questions, and act as an expert designer who outputs HTML artifacts +or production code, depending on the need. + +## Five things this brand always does + +1. **One italic word per headline.** The signature move. Set a noun + in Instrument Serif italic; never the whole phrase. +2. **One signal accent per view.** Stream teal `#1E8A8A`. If you want + a second colour, restate the typographic hierarchy instead. +3. **Mono metadata strips.** Uppercase, narrow tracking, with a + coloured dot prefix for status. The brand's "tech-savvy" tell. +4. **Sharp by default.** Cards, sections, panels have `radius: 0` and + a 1px hairline border — no shadow. +5. **Bone, not white.** Pages live on `#F4F2EC`. Never pure `#FFFFFF`. + +## Five things this brand never does + +1. **No emoji.** Use mono symbols (`→ · ↗ ↺ ▮`). +2. **No bluish-purple or mesh gradients.** Flat colour or one-direction + warm overlays only. +3. **No glassmorphism.** Sandstream is opaque (one exception: the + floating schedule pill). +4. **No hype words.** "leverage", "10×", "revolutionary", "unlock" are + banned. The voice is a senior consultant's briefing note. +5. **No hand-drawn SVG illustrations.** Photography (Peter's portrait) + and typography do the visual work. diff --git a/design-system/assets/logo-wordmark-light.svg b/design-system/assets/logo-wordmark-light.svg new file mode 100644 index 0000000..db742fd --- /dev/null +++ b/design-system/assets/logo-wordmark-light.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 80" role="img" aria-label="sandstre.am"> + <text x="0" y="60" font-family="Instrument Serif, serif" font-size="64" fill="#F4F2EC">sandstre</text> + <text x="246" y="60" font-family="Instrument Serif, serif" font-size="64" fill="#1E8A8A">.</text> + <text x="262" y="60" font-family="Instrument Serif, serif" font-size="64" font-style="italic" fill="#1E8A8A">am</text> +</svg> \ No newline at end of file diff --git a/design-system/assets/logo-wordmark.svg b/design-system/assets/logo-wordmark.svg new file mode 100644 index 0000000..87af4a5 --- /dev/null +++ b/design-system/assets/logo-wordmark.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 80" role="img" aria-label="sandstre.am"> + <text x="0" y="60" font-family="Instrument Serif, serif" font-size="64" fill="#0E1A1A">sandstre</text> + <text x="246" y="60" font-family="Instrument Serif, serif" font-size="64" fill="#1E8A8A">.</text> + <text x="262" y="60" font-family="Instrument Serif, serif" font-size="64" font-style="italic" fill="#1E8A8A">am</text> +</svg> \ No newline at end of file diff --git a/design-system/assets/mark-favicon.svg b/design-system/assets/mark-favicon.svg new file mode 100644 index 0000000..3a352f2 --- /dev/null +++ b/design-system/assets/mark-favicon.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" role="img" aria-label="sandstre.am"> + <rect width="80" height="80" fill="#0E1A1A"></rect> + <text x="14" y="56" font-family="Instrument Serif, serif" font-size="48" fill="#F4F2EC">s</text> + <text x="36" y="56" font-family="Instrument Serif, serif" font-size="48" fill="#1E8A8A">.</text> + <text x="44" y="56" font-family="Instrument Serif, serif" font-size="48" font-style="italic" fill="#1E8A8A">a</text> +</svg> \ No newline at end of file diff --git a/design-system/assets/peter-portrait.jpg b/design-system/assets/peter-portrait.jpg new file mode 100644 index 0000000..9e597be Binary files /dev/null and b/design-system/assets/peter-portrait.jpg differ diff --git a/design-system/colors_and_type.css b/design-system/colors_and_type.css new file mode 100644 index 0000000..029a579 --- /dev/null +++ b/design-system/colors_and_type.css @@ -0,0 +1,291 @@ +/* ============================================================= + SANDSTREAM — colors_and_type.css + The foundation: color tokens, type tokens, semantic styles. + v1.0 — May 2026 + ============================================================= */ + +/* Fonts — load from Google Fonts in any consuming HTML: + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> +*/ + +:root { + /* ----------------------------------------------------------- + COLOR — RAW PALETTE + A warm bone/sand neutral spine, a near-black ink, and ONE + signal accent (tangerine) pulled from the brand name's + warm-sand register. Restraint is the move. + ----------------------------------------------------------- */ + + /* Neutrals (warm-cool) — Palette B, slightly cooler than original */ + --bone-50: #F4F2EC; /* page background, lightest */ + --bone-100: #EAE7DE; /* default surface */ + --bone-200: #DFDBCE; /* hairline backgrounds, cards on bone */ + --bone-300: #CFC8B5; /* dividers, soft borders */ + --bone-400: #9E9982; /* tertiary text, captions */ + --bone-500: #5A6464; /* secondary text — cool grey-green */ + + /* Inks — cooler, teal-leaning warm-black */ + --ink-900: #0E1A1A; /* deepest, full bleed dark mode */ + --ink-800: #131F1F; /* dark surface */ + --ink-700: #1C2828; /* dark hover/raised */ + --ink-600: #2C3838; /* dark border */ + --ink-500: #475252; /* dark secondary text */ + --ink-400: #76807F; /* dark tertiary */ + --ink-300: #A6ADAB; + --ink-200: #D4D8D6; + --ink-100: #EBEDEC; + + /* Signal — Stream teal. The brand-name literal read. */ + --signal: #1E8A8A; /* teal — primary action, marks */ + --signal-hover: #166E6E; + --signal-tint: #D9EDED; /* signal background fill */ + --signal-ink: #062626; /* signal-on-signal text */ + + /* Secondary signal — used sparingly for data viz / one-off heat */ + --stream-warm: #C77B3A; + --stream-tint: #F5E6D5; + + /* Semantic status (kept narrow on purpose) */ + --status-ok: #2C7A4B; + --status-warn: #B5651D; + --status-err: #B23B2E; + + /* ----------------------------------------------------------- + COLOR — SEMANTIC TOKENS (light mode default) + Always reference these in components, not raw palette. + ----------------------------------------------------------- */ + --bg: var(--bone-50); + --surface: var(--bone-100); + --surface-2: var(--bone-200); + --border: var(--bone-300); + --border-strong: var(--ink-700); + + --fg: var(--ink-900); + --fg-2: var(--bone-500); + --fg-3: var(--bone-400); + --fg-on-dark: var(--bone-50); + + --accent: var(--signal); + --accent-hover: var(--signal-hover); + --accent-tint: var(--signal-tint); + --accent-ink: var(--signal-ink); + + /* ----------------------------------------------------------- + TYPE — FAMILIES + Three families. No more. + Display: Instrument Serif — high contrast, gorgeous italics + Sans: Geist — precise, technical neo-grotesque + Mono: JetBrains Mono — metadata, code, version stamps + ----------------------------------------------------------- */ + --font-display: "Instrument Serif", "Times New Roman", Georgia, serif; + --font-sans: "Geist", -apple-system, BlinkMacSystemFont, "Inter", "Helvetica Neue", Arial, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + + /* ----------------------------------------------------------- + TYPE — RAW SCALE (fluid via clamp where appropriate) + ----------------------------------------------------------- */ + --fs-xs: 11px; + --fs-sm: 13px; + --fs-base: 15px; + --fs-md: 17px; + --fs-lg: 20px; + --fs-xl: 28px; + --fs-2xl: 40px; + --fs-3xl: 56px; + --fs-4xl: 80px; + --fs-5xl: clamp(96px, 12vw, 168px); /* hero display */ + + --lh-tight: 1.02; + --lh-snug: 1.15; + --lh-normal: 1.45; + --lh-loose: 1.6; + + --tracking-tight: -0.02em; + --tracking-snug: -0.01em; + --tracking-normal: 0; + --tracking-wide: 0.08em; + --tracking-mono: 0.02em; + + /* ----------------------------------------------------------- + SPACING — 4px base scale + ----------------------------------------------------------- */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + --space-7: 48px; + --space-8: 64px; + --space-9: 96px; + --space-10: 128px; + --space-11: 192px; + + /* ----------------------------------------------------------- + RADII + Cards and surfaces are sharp; only inputs/pills get rounding. + ----------------------------------------------------------- */ + --radius-0: 0; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 14px; + --radius-pill: 999px; + + /* ----------------------------------------------------------- + SHADOWS — used SPARINGLY. Most surfaces use a 1px hairline. + ----------------------------------------------------------- */ + --shadow-hairline: inset 0 0 0 1px var(--border); + --shadow-card: 0 1px 0 rgba(10,10,12,0.04), 0 12px 32px -16px rgba(10,10,12,0.18); + --shadow-pop: 0 2px 0 rgba(10,10,12,0.05), 0 24px 48px -24px rgba(10,10,12,0.28); + + /* ----------------------------------------------------------- + MOTION + ----------------------------------------------------------- */ + --ease-out: cubic-bezier(0.22, 1, 0.36, 1); + --ease-spring: cubic-bezier(0.34, 1.32, 0.64, 1); + --dur-fast: 140ms; + --dur-base: 220ms; + --dur-slow: 420ms; + + /* ----------------------------------------------------------- + GRAIN — a subtle tactile texture overlay; used on heroes + and dark sections, never on body text. + ----------------------------------------------------------- */ + --grain: url("data:image/svg+xml;utf8,<svg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)' opacity='0.5'/></svg>"); +} + +/* ============================================================= + DARK MODE — invert tokens; the same components work. + ============================================================= */ +[data-theme="dark"] { + --bg: var(--ink-900); + --surface: var(--ink-800); + --surface-2: var(--ink-700); + --border: var(--ink-600); + --border-strong: var(--bone-300); + + --fg: var(--bone-50); + --fg-2: var(--ink-300); + --fg-3: var(--ink-400); + --fg-on-dark: var(--bone-50); +} + +/* ============================================================= + SEMANTIC TYPE CLASSES + Use these directly. Don't reach for raw scale tokens in + components — these encapsulate family + size + tracking. + ============================================================= */ + +.t-eyebrow { + font-family: var(--font-mono); + font-size: var(--fs-xs); + font-weight: 400; + text-transform: uppercase; + letter-spacing: var(--tracking-wide); + color: var(--fg-2); +} + +.t-meta { + font-family: var(--font-mono); + font-size: var(--fs-sm); + font-weight: 400; + letter-spacing: var(--tracking-mono); + color: var(--fg-2); +} + +.t-display-xl { + font-family: var(--font-display); + font-size: var(--fs-5xl); + font-weight: 400; + line-height: var(--lh-tight); + letter-spacing: var(--tracking-tight); + color: var(--fg); +} + +.t-display-lg { + font-family: var(--font-display); + font-size: var(--fs-4xl); + font-weight: 400; + line-height: var(--lh-tight); + letter-spacing: var(--tracking-tight); + color: var(--fg); +} + +.t-display-md { + font-family: var(--font-display); + font-size: var(--fs-3xl); + font-weight: 400; + line-height: var(--lh-snug); + letter-spacing: var(--tracking-snug); + color: var(--fg); +} + +.t-h1 { + font-family: var(--font-sans); + font-size: var(--fs-2xl); + font-weight: 500; + line-height: var(--lh-snug); + letter-spacing: var(--tracking-tight); + color: var(--fg); +} + +.t-h2 { + font-family: var(--font-sans); + font-size: var(--fs-xl); + font-weight: 500; + line-height: var(--lh-snug); + letter-spacing: var(--tracking-snug); + color: var(--fg); +} + +.t-h3 { + font-family: var(--font-sans); + font-size: var(--fs-lg); + font-weight: 500; + line-height: var(--lh-snug); + letter-spacing: var(--tracking-snug); + color: var(--fg); +} + +.t-body { + font-family: var(--font-sans); + font-size: var(--fs-md); + font-weight: 400; + line-height: var(--lh-loose); + color: var(--fg); + text-wrap: pretty; +} + +.t-body-sm { + font-family: var(--font-sans); + font-size: var(--fs-base); + font-weight: 400; + line-height: var(--lh-normal); + color: var(--fg); +} + +.t-caption { + font-family: var(--font-sans); + font-size: var(--fs-sm); + font-weight: 400; + line-height: var(--lh-normal); + color: var(--fg-2); +} + +.t-code { + font-family: var(--font-mono); + font-size: var(--fs-sm); + font-weight: 400; + letter-spacing: var(--tracking-mono); + color: var(--fg); + background: var(--surface-2); + padding: 2px 6px; + border-radius: var(--radius-sm); +} + +/* Italic display — a SIGNATURE move. Use sparingly. */ +.t-italic { + font-style: italic; +} diff --git a/design-system/explorations/sandstre-intro.html b/design-system/explorations/sandstre-intro.html new file mode 100644 index 0000000..d65d54c --- /dev/null +++ b/design-system/explorations/sandstre-intro.html @@ -0,0 +1,359 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<meta name="robots" content="noindex"> +<title>sandstre.am + + + + + + + + +
+ SANDSTRE.AM + /ˈsanˌstreːm/ +
+ +
+ +
+ +
+ +
+ v2026.05 + 01 / 05 +
+ +
+ enter site +
+ + + + + + + diff --git a/design-system/preview/_preview.css b/design-system/preview/_preview.css new file mode 100644 index 0000000..584210a --- /dev/null +++ b/design-system/preview/_preview.css @@ -0,0 +1,56 @@ +/* Shared scaffolding for preview cards. Each card is rendered standalone + at 700px wide, intended for the Design System tab. */ + +@import url("../colors_and_type.css"); +@import url("https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"); + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--fg); + font-family: var(--font-sans); + font-size: var(--fs-md); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.card { + padding: 24px 28px; + min-height: 100vh; + display: flex; + flex-direction: column; + gap: 16px; +} + +.card-row { + display: flex; + align-items: center; + gap: 12px; +} + +.card-grid { + display: grid; + gap: 12px; +} + +.tag { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--fg-3); +} + +.tag::before { + content: ""; + width: 6px; + height: 6px; + background: var(--accent); + border-radius: 50%; +} diff --git a/design-system/preview/buttons.html b/design-system/preview/buttons.html new file mode 100644 index 0000000..704df4d --- /dev/null +++ b/design-system/preview/buttons.html @@ -0,0 +1,80 @@ + + + + +Buttons + + + + +
+
+ Buttons — primary · secondary · ghost · link + radius 8px · pill for CTA +
+
+
+ primary + + +
+
+ secondary + + + +
+
+
+ + diff --git a/design-system/preview/cards.html b/design-system/preview/cards.html new file mode 100644 index 0000000..05706f8 --- /dev/null +++ b/design-system/preview/cards.html @@ -0,0 +1,92 @@ + + + + +Cards + + + + +
+
+ Cards — hairline border, no shadow + hover darkens border +
+
+
+ / 01 — interim +
Interim ledarskap.
+
CTO, CIO, CPO, CDO eller MD inom tech. Sex till tjugofyra månader.
+ Read more → +
+
+ / 02 — advisory +
Strategic advisory.
+
Board, investor and exec advisory for digital transformation programmes.
+ Read more → +
+
+
+ + diff --git a/design-system/preview/colors-bone.html b/design-system/preview/colors-bone.html new file mode 100644 index 0000000..3d8e9c0 --- /dev/null +++ b/design-system/preview/colors-bone.html @@ -0,0 +1,106 @@ + + + + +Colors · Bone + + + + +
+
+ Bone — warm neutrals + page · surface · borders +
+
+
+ bone / 50 + #F4F2EC + Default page background. + --bg +
+
+ bone / 100 + #EAE7DE + Surface · raised panels. + --surface +
+
+ bone / 200 + #DFDBCE + Secondary surface. + --surface-2 +
+
+ bone / 300 + #CFC8B5 + Hairline dividers + borders. + --border +
+
+ bone / 400 + #9E9982 + Tertiary text · captions. + --fg-3 +
+
+ bone / 500 + #5A6464 + Secondary body text. + --fg-2 +
+
+
+ + diff --git a/design-system/preview/colors-ink.html b/design-system/preview/colors-ink.html new file mode 100644 index 0000000..10a3997 --- /dev/null +++ b/design-system/preview/colors-ink.html @@ -0,0 +1,117 @@ + + + + +Colors · Ink + + + + +
+
+ Ink — cool warm-black scale + text · dark surfaces · borders +
+
+
+ ink / 900 + #0E1A1A + Primary text · dark page. + --fg +
+
+ ink / 800 + #131F1F + Dark surface · raised panel. + dk --surface +
+
+ ink / 700 + #1C2828 + Dark hover · raised surface. + dk --surface-2 +
+
+ ink / 600 + #2C3838 + Dark borders + dividers. + dk --border +
+
+ ink / 500 + #475252 + Dark secondary text. + dk --fg-2 +
+
+ ink / 400 + #76807F + Dark tertiary · captions. + dk --fg-3 +
+
+ ink / 300 + #A6ADAB + Inverse muted. + +
+
+ ink / 200 + #D4D8D6 + Inverse hairline. + +
+
+
+ + diff --git a/design-system/preview/colors-semantic.html b/design-system/preview/colors-semantic.html new file mode 100644 index 0000000..072f400 --- /dev/null +++ b/design-system/preview/colors-semantic.html @@ -0,0 +1,83 @@ + + + + +Colors · Semantic + + + + +
+
+ Semantic tokens — reference these, not raw palette + light · dark +
+
+
+
Light
+
--bgbone-50
+
--surfacebone-100
+
--borderbone-300
+
--fgink-900
+
--fg-2bone-500
+
+
+
Accent
+
--accentsignal
+
--accent-hoversignal/h
+
--accent-tintsignal/t
+
--accent-inksignal/i
+
+
+
Dark
+
--bgink-900
+
--surfaceink-800
+
--borderink-600
+
--fgbone-50
+
--fg-2ink-300
+
+
+
+ + diff --git a/design-system/preview/colors-signal.html b/design-system/preview/colors-signal.html new file mode 100644 index 0000000..71d15ac --- /dev/null +++ b/design-system/preview/colors-signal.html @@ -0,0 +1,128 @@ + + + + +Colors · Signal + + + + +
+
+ Signal — the one accent + one accent per view +
+
+
+ / SIGNAL · PRIMARY +
Tangerine.
+ +
+
+
+ hover + Hover + #166E6E +
+
+ tint + Tint + #D9EDED +
+
+ ink-on-signal + Ink + #062626 +
+
+
+
+ + diff --git a/design-system/preview/colors-status.html b/design-system/preview/colors-status.html new file mode 100644 index 0000000..159e9fc --- /dev/null +++ b/design-system/preview/colors-status.html @@ -0,0 +1,82 @@ + + + + +Colors · Status + + + + +
+
+ Stream + status — secondary signals + data viz + system feedback +
+
+
+ stream · teal +
+
Stream
+ #1E8A8A +
+
+
+ status · ok +
+
OK
+ #2C7A4B +
+
+
+ status · warn +
+
Warn
+ #B5651D +
+
+
+ status · err +
+
Error
+ #B23B2E +
+
+
+
+ + diff --git a/design-system/preview/iconography.html b/design-system/preview/iconography.html new file mode 100644 index 0000000..6eda668 --- /dev/null +++ b/design-system/preview/iconography.html @@ -0,0 +1,77 @@ + + + + +Iconography + + + + + +
+
+ Iconography — mono symbols + Lucide + no emoji, ever +
+
+
+ Mono glyphs~90% of needs +
+ link + extl + ·sep + /section + refresh + play + status +
+
+
+ LucideCDN · 1.5 stroke +
+ + + + + + + + +
+
+
+
+ + + diff --git a/design-system/preview/inputs.html b/design-system/preview/inputs.html new file mode 100644 index 0000000..3614630 --- /dev/null +++ b/design-system/preview/inputs.html @@ -0,0 +1,81 @@ + + + + +Form inputs + + + + +
+
+ Form inputs — fields + focus + states + focus ring · tangerine +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + + Not a valid email address. +
+
+
+ + diff --git a/design-system/preview/logo-dark.html b/design-system/preview/logo-dark.html new file mode 100644 index 0000000..7aa82b6 --- /dev/null +++ b/design-system/preview/logo-dark.html @@ -0,0 +1,160 @@ + + + + +Logo · Dark + + + + +
+
+ Wordmark — option B · dark + grain + noise@0.14 +
+ +
+ sandstre.am +
+ +
+
+ NAV · 26PX + sandstre.am +
+
+ STACKED · WITH ROLE +
+ sandstre.am + Peter Sandström · Interim Tech +
+
+
+ FAVICON · APP ICON +
+ s.a + mark-favicon.svgsquare crop · 80×80 +
+
+
+
+ + diff --git a/design-system/preview/logo-light.html b/design-system/preview/logo-light.html new file mode 100644 index 0000000..9916ed0 --- /dev/null +++ b/design-system/preview/logo-light.html @@ -0,0 +1,156 @@ + + + + +Logo · Light + + + + +
+
+ Wordmark — option B · domain hack + locked · light surfaces +
+ +
+ sandstre.am +
+ +
+
+ NAV · 26PX + sandstre.am +
+
+ STACKED · WITH ROLE +
+ sandstre.am + Peter Sandström · Interim Tech +
+
+
+ FAVICON · APP ICON +
+ s.a + mark-favicon.svgsquare crop · 80×80 +
+
+
+
+ + diff --git a/design-system/preview/meta-strip.html b/design-system/preview/meta-strip.html new file mode 100644 index 0000000..8620a95 --- /dev/null +++ b/design-system/preview/meta-strip.html @@ -0,0 +1,74 @@ + + + + +Meta strip + + + + +
+
+ Meta strip — recurring brand pattern + mono · uppercase · narrow tracking +
+
+
+ STOCKHOLM 59.33°N · 18.07°E + v2026.05 + LAST EDIT 2026-05-13 14:22 UTC +
+
+ STATUS AVAILABLE Q3 '26 + BOOKED 14 / 26 WEEKS + RESPONSE < 24h +
+
+
+ + diff --git a/design-system/preview/motion.html b/design-system/preview/motion.html new file mode 100644 index 0000000..43bb164 --- /dev/null +++ b/design-system/preview/motion.html @@ -0,0 +1,222 @@ + + + + +Motion · easing + + + + +
+
+ Motion — easing + duration + confident · never bouncy in UI +
+ +
+ +
+
+ ease-out — default + cubic-bezier(0.22, 1, 0.36, 1) · 220ms + Used for everything functional — page entry, menu reveal, modal in. Confident exit curve, no overshoot. +
+ +
+ SPECIMEN — slide-in +
+
+
+ + +
+
+ spring — press only + cubic-bezier(0.34, 1.32, 0.64, 1) · 140ms + Reserved for tactile press feedback. The only "bouncy" curve in the system. Never use on layout or page transitions. +
+ +
+ SPECIMEN — button press +
Boka 30 min →
+
+
+ + +
+
+ linear — data & counters + linear · 600–1200ms + Counting up hero numbers, progress bars, status pulses. Anything time-truthful where curvature would lie about the data. +
+ +
+ SPECIMEN — count-up +
+
+
+
+
+ + diff --git a/design-system/preview/radii.html b/design-system/preview/radii.html new file mode 100644 index 0000000..b90fc78 --- /dev/null +++ b/design-system/preview/radii.html @@ -0,0 +1,63 @@ + + + + +Radii + + + + +
+
+ Radii — sharp by default + 0 · 4 · 8 · 14 · pill +
+
+
--radius-00px · cards
+
--radius-sm4px · code
+
--radius-md8px · buttons
+
--radius-lg14px · panels
+
--radius-pillfull · tags · CTA
+
+
+ + diff --git a/design-system/preview/shadows.html b/design-system/preview/shadows.html new file mode 100644 index 0000000..c95be36 --- /dev/null +++ b/design-system/preview/shadows.html @@ -0,0 +1,59 @@ + + + + +Shadows + elevation + + + + +
+
+ Elevation — hairline first, shadow rarely + hairline · card · pop +
+
+
--shadow-hairlineinset 1px border · default
+
--shadow-cardcard hover · rare
+
--shadow-popfloating CTA · once per page
+
+
+ + diff --git a/design-system/preview/spacing-scale.html b/design-system/preview/spacing-scale.html new file mode 100644 index 0000000..6cccf2e --- /dev/null +++ b/design-system/preview/spacing-scale.html @@ -0,0 +1,53 @@ + + + + +Spacing scale + + + + +
+
+ Spacing scale — 4px base + 4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 / 96 +
+
+
--space-14px
+
--space-28px
+
--space-312px
+
--space-416px
+
--space-524px
+
--space-632px
+
--space-748px
+
--space-864px
+
--space-996px
+
--space-10128px
+
+
+ + diff --git a/design-system/preview/tags-status.html b/design-system/preview/tags-status.html new file mode 100644 index 0000000..790cc3a --- /dev/null +++ b/design-system/preview/tags-status.html @@ -0,0 +1,78 @@ + + + + +Tags + status + + + + +
+
+ Tags + status chips + pill · mono · dot prefix +
+
+
+ default + STOCKHOLM + INTERIM + 2026 +
+
+ solid + AVAILABLE Q3 '26 + NEW ENGAGEMENT +
+
+ status + SHIPPED + IN REVIEW + BLOCKED +
+
+
+ + diff --git a/design-system/preview/type-display.html b/design-system/preview/type-display.html new file mode 100644 index 0000000..bd4ee8d --- /dev/null +++ b/design-system/preview/type-display.html @@ -0,0 +1,55 @@ + + + + +Type · Display + + + + +
+
+ Display — Instrument Serif + regular + italic +
+
+
Sandstream.
+
+ Family Instrument Serif + Weight 400 + Tracking −0.02em + Use hero · headline · pull-quote +
+
+
+ + diff --git a/design-system/preview/type-headline-pattern.html b/design-system/preview/type-headline-pattern.html new file mode 100644 index 0000000..bcf1b19 --- /dev/null +++ b/design-system/preview/type-headline-pattern.html @@ -0,0 +1,50 @@ + + + + +Type · Headline pattern + + + + +
+
+ Headline pattern — one italic word + signature move +
+
+
Tjugo år av ledarskap inom tech.
+
Strategy for the spaces in between.
+
Set one word — usually a noun — in italic. Never two. Never the whole phrase.
+
+
+ + diff --git a/design-system/preview/type-mono.html b/design-system/preview/type-mono.html new file mode 100644 index 0000000..0af8db5 --- /dev/null +++ b/design-system/preview/type-mono.html @@ -0,0 +1,50 @@ + + + + +Type · Mono + + + + +
+
+ Mono — JetBrains Mono + metadata · timestamps · code +
+
+
/01 · v2026.05 · sandstre.am
+
+ STOCKHOLM 59.33°N · 18.07°E + STATUS AVAILABLE Q3 '26 + LAST EDIT 2026-05-13 14:22 UTC +
+
+
+ + diff --git a/design-system/preview/type-sans.html b/design-system/preview/type-sans.html new file mode 100644 index 0000000..20404f7 --- /dev/null +++ b/design-system/preview/type-sans.html @@ -0,0 +1,50 @@ + + + + +Type · Sans + + + + +
+
+ Sans — Geist + UI · body · subheads +
+
+
Tjugo år av ledarskap inom tech.
+
light · 300Strategisk översikt och praktisk teknisk insikt.
+
regular · 400Strategisk översikt och praktisk teknisk insikt.
+
medium · 500Strategisk översikt och praktisk teknisk insikt.
+
semibold · 600Strategisk översikt och praktisk teknisk insikt.
+
bold · 700Strategisk översikt och praktisk teknisk insikt.
+
+
+ + diff --git a/design-system/preview/type-scale.html b/design-system/preview/type-scale.html new file mode 100644 index 0000000..3da1930 --- /dev/null +++ b/design-system/preview/type-scale.html @@ -0,0 +1,54 @@ + + + + +Type · Scale + + + + +
+
+ Type scale — display + sans + 11 → 168px +
+
+
5xl96–168pxSandstream
+
4xl80pxDisplay large
+
2xl40pxHeading one
+
lg20pxHeading three
+
md17pxBody paragraph copy
+
sm13pxCaption / meta
+
+
+ + diff --git a/design-system/preview/voice-specimen.html b/design-system/preview/voice-specimen.html new file mode 100644 index 0000000..efe71e4 --- /dev/null +++ b/design-system/preview/voice-specimen.html @@ -0,0 +1,52 @@ + + + + +Brand voice + + + + +
+
/ voice — headline + meta + sub
+
+
Tjugo år av ledarskap inom tech.
+
Stockholm-baserat, internationellt uppdragsbart. Strategisk översikt och praktisk teknisk insikt för digital transformation, interimsuppdrag och skalresa.
+
+
+ + diff --git a/design-system/ui_kits/portfolio/About.jsx b/design-system/ui_kits/portfolio/About.jsx new file mode 100644 index 0000000..875adb7 --- /dev/null +++ b/design-system/ui_kits/portfolio/About.jsx @@ -0,0 +1,48 @@ +// About.jsx — portrait + bio + stats. The "om Peter" section. + +function About() { + return ( +
+
+ / 01   om peter + +
+ +
+
+ Peter Sandström +
PORTRAIT · 2025
+
+ +
+

+ Strategi för det mellanrum där tech möter affär. +

+

+ Peter är en erfaren teknologiledare med över 20 års erfarenhet av IT-ledning och digital transformation. Han har byggt och lett tekniska team genom komplexa förändringar — från dynamiska startups till etablerade nordiska koncerner. +

+

+ Senaste åren har fokus skiftat mot affärsnära AI-implementation: generativ AI, agentiska arbetsflöden, vector embeddings, och praktiska automationer som faktiskt går i produktion. Just nu AI & Innovation Lead på Frontwalker, samtidigt som Sandstream AB tar interimsuppdrag och advisory. +

+ +
+
+
20+
+
Years in tech leadership
+
+
+
35
+
Largest team led · Devoteam
+
+
+
4
+
CXO + MD interim roles
+
+
+
+
+
+ ); +} + +window.About = About; diff --git a/design-system/ui_kits/portfolio/App.jsx b/design-system/ui_kits/portfolio/App.jsx new file mode 100644 index 0000000..5751586 --- /dev/null +++ b/design-system/ui_kits/portfolio/App.jsx @@ -0,0 +1,59 @@ +// App.jsx — composes the page. Tracks active section via IntersectionObserver +// for nav highlighting. Smooth-scroll on nav clicks. + +const { useState, useEffect, useCallback, useRef } = React; + +function App() { + const [active, setActive] = useState("top"); + + const scrollTo = useCallback((id) => { + const el = document.getElementById(id); + if (!el) return; + const top = el.getBoundingClientRect().top + window.scrollY; + window.scrollTo({ top, behavior: "smooth" }); + }, []); + + const openSchedule = useCallback(() => { + window.open("https://cal.eu/sandstream/quick-chat", "_blank", "noopener,noreferrer"); + }, []); + + useEffect(() => { + const sections = ["top", "about", "work", "roles", "contact"] + .map((id) => document.getElementById(id)) + .filter(Boolean); + if (!sections.length) return; + const obs = new IntersectionObserver( + (entries) => { + // Pick the entry closest to the top with majority visibility + const visible = entries + .filter((e) => e.isIntersecting) + .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); + if (visible.length) setActive(visible[0].target.id); + }, + { rootMargin: "-30% 0px -55% 0px", threshold: 0 } + ); + sections.forEach((s) => obs.observe(s)); + return () => obs.disconnect(); + }, []); + + // Re-render Lucide icons after mount + useEffect(() => { + if (window.lucide) window.lucide.createIcons(); + }, []); + + return ( +
+
+
+ + + + + +
+ +
+ ); +} + +ReactDOM.createRoot(document.getElementById("root")).render(); diff --git a/design-system/ui_kits/portfolio/Cases.jsx b/design-system/ui_kits/portfolio/Cases.jsx new file mode 100644 index 0000000..6022cf6 --- /dev/null +++ b/design-system/ui_kits/portfolio/Cases.jsx @@ -0,0 +1,121 @@ +// Cases.jsx — the "selected work" section. The 4 references the user asked for. + +const CASES = [ + { + num: "/01", + name: ["Hunter", "Matcher"], + italic: 1, // index of italicised word + tagline: "AI-driven consultant matching · built by a team of two.", + desc: "End-to-end backoffice for consulting firms — ingests inbound RFPs from email, parses each mission, embeds requirements against a multilingual consultant base, and ranks every match in real time. Founded the platform inside Frontwalker AIDA; built the AI pipeline, the integrations and the frontend.", + meta: [ + { l: "ROLE", v: "Co-founder · AI lead" }, + { l: "STACK", v: "React 19 · Convex · Vertex AI" }, + { l: "SCALE", v: "180 consultants · 13 kr/mo each" }, + { l: "YEAR", v: "2025–26" }, + ], + href: "https://huntermatcher.vercel.app/", + hrefLabel: "huntermatcher.vercel.app", + }, + { + num: "/02", + name: ["Kjorre"], + italic: 0, + tagline: "International QR-kiosk payment platform.", + desc: "Cross-border self-service payment kiosks with QR-first UX. Multi-tenant SaaS architecture, payment provider integrations and an admin portal for venue operators. Built the technical strategy and the platform foundations end-to-end.", + meta: [ + { l: "ROLE", v: "Founding tech · architecture" }, + { l: "STACK", v: "Next.js · Stripe · multi-tenant" }, + { l: "SECTOR", v: "Fintech · self-service retail" }, + { l: "YEAR", v: "2024–26" }, + ], + href: "https://www.kjorre.com", + hrefLabel: "kjorre.com", + }, + { + num: "/03", + name: ["Game Day", "DJ"], + italic: 1, + tagline: "Real-time arena sound for amateur sports.", + desc: "Mobile app that turns a coach or game-day volunteer into a stadium DJ — cue-based audio playback for moments in a basketball or hockey game, with offline-first audio packs and live event triggers. Built the iOS app, the curated sound library and the live-event sync.", + meta: [ + { l: "ROLE", v: "Product · iOS · audio" }, + { l: "STACK", v: "iOS · Firebase · WebAudio" }, + { l: "SECTOR", v: "Sports tech · live audio" }, + { l: "YEAR", v: "2024–26" }, + ], + href: "https://gamedaydj.app", + hrefLabel: "gamedaydj.app", + }, + { + num: "/04", + name: ["Ralph", "Inferno"], + italic: 1, + tagline: "AI-driven autonomous development workflow.", + desc: "Open-source orchestration for an inner-loop agentic coding workflow — Ralph plans, edits, runs tests and reviews while you sleep. LLM-agnostic engine, designed for long-horizon autonomous code generation with explicit guardrails and verification gates.", + meta: [ + { l: "ROLE", v: "Creator · open source" }, + { l: "STACK", v: "Node · LLM-agnostic · CLI" }, + { l: "SECTOR", v: "Developer tooling" }, + { l: "YEAR", v: "2025–26" }, + ], + href: "https://github.com/sandstream/ralph-inferno", + hrefLabel: "github.com/sandstream/ralph-inferno", + }, +]; + +function Case({ data }) { + const renderName = () => + data.name.map((word, i) => ( + + {i > 0 && " "} + {i === data.italic ? {word} : word} + + )); + + return ( +
+
{data.num}
+
+
{renderName()}
+
{data.tagline}
+
+
+
{data.desc}
+
+ {data.meta.map((m, i) => ( + {m.l} {m.v} + ))} +
+ + {data.hrefLabel} + +
+
+ ); +} + +function Cases() { + return ( +
+
+ / 02   selected work + +
+ +
+

+ Four cases. One method. +

+

+ Recent work that captures the shape of how Peter operates: small teams, modern AI tooling, deep domain understanding, and a bias toward shipping something real in weeks — not roadmapping it for quarters. +

+
+ +
+ {CASES.map((c) => )} +
+
+ ); +} + +window.Cases = Cases; diff --git a/design-system/ui_kits/portfolio/Contact.jsx b/design-system/ui_kits/portfolio/Contact.jsx new file mode 100644 index 0000000..98fa492 --- /dev/null +++ b/design-system/ui_kits/portfolio/Contact.jsx @@ -0,0 +1,53 @@ +// Contact.jsx — full-width dark contact panel + Footer. + +function Contact({ onSchedule }) { + return ( + +
+
+ / 04   let's talk + +
+ +

+ Behöver ni en interim i tech? +

+ +
+ + + hello@sandstre.am + +
+ +
+
+ Email + hello@sandstre.am + Svar inom 24 timmar, vardagar. +
+
+ Telefon + [PHONE] + SMS preferred. Stockholm timezone. +
+
+ Calendar + cal.eu/sandstream + 30 min · ingen kostnad · no slides. +
+
+
+ +
+ sandstre.am + STOCKHOLM · ORG.NR 559123-0000 · © 2026 + LINKEDIN ↗ +
+
+ ); +} + +window.Contact = Contact; diff --git a/design-system/ui_kits/portfolio/FloatingPill.jsx b/design-system/ui_kits/portfolio/FloatingPill.jsx new file mode 100644 index 0000000..85eb709 --- /dev/null +++ b/design-system/ui_kits/portfolio/FloatingPill.jsx @@ -0,0 +1,13 @@ +// FloatingPill.jsx — the single floating CTA pill, bottom-right. + +function FloatingPill({ onClick }) { + return ( + + ); +} + +window.FloatingPill = FloatingPill; diff --git a/design-system/ui_kits/portfolio/Header.jsx b/design-system/ui_kits/portfolio/Header.jsx new file mode 100644 index 0000000..491a1f1 --- /dev/null +++ b/design-system/ui_kits/portfolio/Header.jsx @@ -0,0 +1,150 @@ +// Header.jsx — fixed left rail with wordmark + nav + theme toggle. +// Clicking the wordmark plays a phonetic morph through Peter's name +// variants before scrolling back to top. + +const { useState, useEffect, useRef, useCallback } = React; + +// The phonetic variants the wordmark cycles through. Last one IS the +// canonical wordmark; the morph "settles" there. The .am is rendered +// with italic teal styling baked into the HTML strings so the final +// state matches the static wordmark exactly. +const PHONETIC_VARIANTS = [ + "sandström", + "sandstroem", + "sandstream", + "săndstrēm", + 'sandstre.am', +]; + +// Plays a sequence of in-place morphs on a single element. +// Returns a Promise that resolves when the morph settles. +function playMorph(el, variants, holdMs = 280) { + return new Promise((resolve) => { + let i = 0; + function tick() { + el.classList.add("morph-out"); + setTimeout(() => { + el.innerHTML = variants[i]; + el.classList.remove("morph-out"); + el.classList.add("morph-in"); + // Force reflow so the .morph-in transition triggers + void el.offsetWidth; + el.classList.remove("morph-in"); + i++; + if (i >= variants.length) { + // Mark settled so the wordmark stays styled correctly + el.classList.add("morph-settled"); + resolve(); + return; + } + setTimeout(tick, holdMs); + }, 180); + } + tick(); + }); +} + +function ThemeToggle() { + const [theme, setTheme] = useState(() => { + if (typeof window === "undefined") return "light"; + return localStorage.getItem("sandstream-theme") || "light"; + }); + + useEffect(() => { + document.documentElement.setAttribute("data-theme", theme); + localStorage.setItem("sandstream-theme", theme); + }, [theme]); + + const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light")); + + return ( + + ); +} + +function Header({ active, onNav }) { + const navItems = [ + { id: "about", num: "01", label: "Bio" }, + { id: "work", num: "02", label: "Selected work" }, + { id: "roles", num: "03", label: "Roles" }, + { id: "contact", num: "04", label: "Contact" }, + ]; + + const wordmarkRef = useRef(null); + const morphingRef = useRef(false); + + const handleWordmarkClick = useCallback(async (e) => { + e.preventDefault(); + if (morphingRef.current) return; + morphingRef.current = true; + + const el = wordmarkRef.current; + if (!el) { morphingRef.current = false; return; } + + // Always: morph in place, then scroll to top if not already there + const canonical = PHONETIC_VARIANTS[PHONETIC_VARIANTS.length - 1]; + const atTop = window.scrollY < 8; + if (!atTop) { + // Start scrolling to top in parallel with the morph so they finish + // around the same time. The morph is the visible feedback. + window.scrollTo({ top: 0, behavior: "smooth" }); + } + await playMorph(el, PHONETIC_VARIANTS, 240); + el.innerHTML = canonical; + morphingRef.current = false; + }, []); + + return ( + + ); +} + +window.Header = Header; diff --git a/design-system/ui_kits/portfolio/Hero.jsx b/design-system/ui_kits/portfolio/Hero.jsx new file mode 100644 index 0000000..ed8d8f8 --- /dev/null +++ b/design-system/ui_kits/portfolio/Hero.jsx @@ -0,0 +1,52 @@ +// Hero.jsx — the editorial hero. Mono meta strip top, big serif headline, +// supporting sub, two CTAs, mono meta strip bottom. + +function Hero({ onSchedule }) { + return ( +
+
+
+ STOCKHOLM · 59.33°N · 18.07°E + SANDSTREAM AB · ORG.NR 559123-0000 +
+
+ + + AVAILABLE Q3 '26 + + RESPONSE < 24h +
+
+ +
+

+ Tjugo år av ledarskap inom tech. +

+

+ Peter Sandström — interim CTO, CIO, CPO och MD för bolag i transitions- och skalfas. Strategisk översikt och praktisk teknisk insikt för digital transformation, AI-implementation och skalresa. +

+
+ + + hello@sandstre.am + +
+
+ +
+
+ NOW AI & Innovation Lead · Frontwalker + FOUNDED Sandstream AB · 2023 +
+
+ LANG Svenska · English + BASED Stockholm · remote-first +
+
+
+ ); +} + +window.Hero = Hero; diff --git a/design-system/ui_kits/portfolio/MetaStrip.jsx b/design-system/ui_kits/portfolio/MetaStrip.jsx new file mode 100644 index 0000000..dd1cabd --- /dev/null +++ b/design-system/ui_kits/portfolio/MetaStrip.jsx @@ -0,0 +1,21 @@ +// MetaStrip.jsx — recurring mono-metadata strip used in hero and contact + +function MetaStrip({ items, dark }) { + return ( +
+ {items.map((group, i) => ( +
+ {group.lines.map((line, j) => ( + + {line.label && {line.label}} + {line.label && " "} + {line.value} + + ))} +
+ ))} +
+ ); +} + +window.MetaStrip = MetaStrip; diff --git a/design-system/ui_kits/portfolio/README.md b/design-system/ui_kits/portfolio/README.md new file mode 100644 index 0000000..de133fe --- /dev/null +++ b/design-system/ui_kits/portfolio/README.md @@ -0,0 +1,54 @@ +# Portfolio · `sandstre.am` (2026) + +A complete single-page replacement for the existing Jekyll site at +`https://www.sandstre.am`. Built as a hi-fi interactive prototype using +the Sandstream design system tokens directly. + +## What's in here + +| File | Role | +|---|---| +| `index.html` | Entry — loads fonts, design-system CSS, components | +| `portfolio.css` | Page-specific styles (rail, hero, cases, contact) | +| `App.jsx` | Composes the page · IntersectionObserver for active nav | +| `Header.jsx` | Fixed left rail · wordmark + numbered nav | +| `Hero.jsx` | Editorial hero with mono meta strips | +| `About.jsx` | Portrait + bio + 3 stats | +| `Cases.jsx` | Four references — HunterMatcher, Kjorre, Game Day DJ, Ralph Inferno | +| `Roles.jsx` | 20-year leadership timeline pulled from CV | +| `Contact.jsx` | Dark contact panel + footer | +| `FloatingPill.jsx` | The single floating CTA (bottom-right) | +| `MetaStrip.jsx` | Reusable mono metadata strip primitive | + +## Sections + +1. **Hero** — `Tjugo år av ledarskap inom tech.` Mono meta strips top and bottom (location, org-nr, current role, languages). Two CTAs. +2. **Om Peter** — Portrait + bio + 3 stats (20+ years, 35 max team, 4 interim CXO roles). +3. **Selected work** — Four case studies (HunterMatcher / Kjorre / Game Day DJ / Ralph Inferno) on the dark grain background. +4. **Roles** — Timeline of 9 selected positions from 2005 to now, drawn straight from Peter's CV. +5. **Contact** — Email · phone · Calendly, dark with grain. + +## Content authority + +All numbers, dates, role titles, achievements and case-study facts come +from `assets/peter-cv-2026.txt` (Peter's own CV, not published in this repo) and the +`team-summary.pptx` deck he supplied (HunterMatcher cost numbers, stack, +scale). No content is invented. Where a fact wasn't available — e.g. +the precise org-nr for Sandstream AB — a placeholder is used and +flagged in this README for Peter to fill in. + +## Known placeholders + +- **`ORG.NR 559123-0000`** — placeholder. Replace with real Bolagsverket number in `Hero.jsx` and `Contact.jsx`. +- **Game Day DJ stack** — listed as iOS + Firebase + WebAudio. If the real stack differs, edit `Cases.jsx`. +- **Calendly URL** — `cal.eu/sandstream/30min` carried over from the old site; confirm still valid. + +## How to iterate + +This file is the design canvas — edit JSX components directly. The +component split was chosen so common edits stay small: + +- Change colours / type → edit `colors_and_type.css` (root) +- Change case copy → edit `Cases.jsx` `CASES` array +- Change roles → edit `Roles.jsx` `ROLES` array +- Change hero copy → edit `Hero.jsx` strings diff --git a/design-system/ui_kits/portfolio/Roles.jsx b/design-system/ui_kits/portfolio/Roles.jsx new file mode 100644 index 0000000..84357e6 --- /dev/null +++ b/design-system/ui_kits/portfolio/Roles.jsx @@ -0,0 +1,49 @@ +// Roles.jsx — recent leadership timeline. Pulled from Peter's CV. + +const ROLES = [ + { when: "2026 → NU", title: "AI & Innovation Lead", org: "Frontwalker", tag: "Current" }, + { when: "2025 — 2026", title: "AI-konsult & medgrundare", org: "Frontwalker AIDA", tag: "Co-founded HunterMatcher" }, + { when: "2023 — 2024", title: "Grundare & konsult", org: "Sandstream AB", tag: "Interim · advisory" }, + { when: "2021 — 2023", title: "Managing Director", org: "Devoteam Creative Tech", tag: "Team 13 → 35" }, + { when: "2019 — 2021", title: "CTO Mobiento · Senior Manager", org: "Deloitte AB", tag: "M&A · Conversational AI" }, + { when: "2017 — 2018", title: "Nordic Engineering Manager", org: "SATS ELIXIA", tag: "Online ↑ 5% → 13%" }, + { when: "2012 — 2017", title: "Nordic IT Operations / Infra / Transformation", org: "SATS ELIXIA", tag: "Cloud-First · CRM" }, + { when: "2011 — 2012", title: "IT Manager", org: "Boots Apotek", tag: "0 → 9 apotek" }, + { when: "2005 — 2011", title: "IT-chef & Webbansvarig", org: "SwedSec Licensiering AB", tag: "MVC · REST · DevOps" }, +]; + +function Roles() { + return ( +
+
+ / 03   selected roles · 20 years + +
+ +
+
+

+ Tjugo år av uppdrag. +

+

+ En kortfattad timeline över formella roller. Full CV och case-beskrivningar på begäran. +

+
+ +
+ {ROLES.map((r, i) => ( +
+
{r.when}
+
+ {r.title}· {r.org} +
+
{r.tag}
+
+ ))} +
+
+
+ ); +} + +window.Roles = Roles; diff --git a/design-system/ui_kits/portfolio/index.html b/design-system/ui_kits/portfolio/index.html new file mode 100644 index 0000000..759d9ac --- /dev/null +++ b/design-system/ui_kits/portfolio/index.html @@ -0,0 +1,32 @@ + + + + + +Peter Sandström — Sandstream + + + + + + + + + + + + + +
+ + + + + + + + + + + + diff --git a/design-system/ui_kits/portfolio/portfolio.css b/design-system/ui_kits/portfolio/portfolio.css new file mode 100644 index 0000000..7e9a084 --- /dev/null +++ b/design-system/ui_kits/portfolio/portfolio.css @@ -0,0 +1,750 @@ +/* Portfolio-only styles — anything not in colors_and_type.css */ + +* { box-sizing: border-box; } +html, body { + margin: 0; padding: 0; + background: var(--bg); + color: var(--fg); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +::selection { background: var(--signal); color: var(--bone-50); } + +a { color: inherit; text-decoration: none; } + +/* Page container */ +.page { + min-height: 100vh; + position: relative; +} + +/* The left rule — a fixed vertical edge holding the wordmark + nav */ +.rail { + position: fixed; + left: 0; top: 0; + width: 220px; + height: 100vh; + padding: 32px 24px; + display: flex; + flex-direction: column; + justify-content: space-between; + border-right: 1px solid var(--border); + background: var(--bg); + z-index: 50; +} + +.rail-bottom { + display: flex; + flex-direction: column; + gap: 6px; + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--fg-2); +} + +.rail-bottom .row { display: flex; justify-content: space-between; gap: 8px; } +.rail-bottom b { color: var(--fg); font-weight: 500; } + +/* Theme toggle — compact pill + inline label */ +.theme-toggle { + display: inline-flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; + padding: 0; + background: none; + border: none; + cursor: pointer; + font: inherit; + color: inherit; + text-align: left; + align-self: flex-start; +} +.theme-toggle .track { + position: relative; + display: inline-block; + width: 36px; + height: 18px; + border: 1px solid var(--border); + background: var(--surface); + border-radius: 999px; + flex-shrink: 0; + transition: border-color 200ms var(--ease-out), background 200ms var(--ease-out); +} +.theme-toggle .knob { + position: absolute; + top: 1px; + left: 1px; + width: 14px; + height: 14px; + background: var(--accent); + border-radius: 50%; + transition: transform 240ms var(--ease-spring); + display: flex; + align-items: center; + justify-content: center; + color: var(--bone-50); + overflow: hidden; +} +.theme-toggle .track.dark .knob { + transform: translateX(18px); +} +.theme-toggle .glyph { + position: absolute; + font-family: var(--font-mono); + font-size: 8px; + line-height: 1; + transition: opacity 180ms var(--ease-out); +} +.theme-toggle .glyph.sun { opacity: 1; } +.theme-toggle .glyph.moon { opacity: 0; } +.theme-toggle .track.dark .glyph.sun { opacity: 0; } +.theme-toggle .track.dark .glyph.moon { opacity: 1; } +.theme-toggle:hover .track { border-color: var(--fg); } +.theme-toggle .theme-label { + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--fg-3); + white-space: nowrap; +} +.theme-toggle .theme-label .on { color: var(--fg); font-weight: 500; } +.theme-toggle .theme-label .sep { color: var(--fg-3); margin: 0 1px; } + +.wordmark { + font-family: var(--font-display); + font-size: 28px; + line-height: 1; + color: var(--fg); + letter-spacing: -0.02em; + display: inline-flex; + align-items: baseline; + cursor: pointer; + transition: + opacity 180ms var(--ease-out), + filter 180ms var(--ease-out), + letter-spacing 220ms var(--ease-out); + will-change: opacity, filter, letter-spacing; +} +.wordmark .tld { font-style: italic; color: var(--accent); } +.wordmark .tld-dot { color: var(--accent); } +.wordmark.morph-out { + opacity: 0; + filter: blur(4px); + letter-spacing: 0.12em; +} +.wordmark.morph-in { + opacity: 0; + filter: blur(4px); + letter-spacing: -0.04em; +} + +/* Full-screen overlay morph — triggered when scrolled down and the + wordmark is clicked. Plays at hero scale, then fades out. */ +.logo-overlay { + position: fixed; + inset: 0; + background: var(--bg); + color: var(--fg); + z-index: 200; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 420ms var(--ease-out); + pointer-events: none; +} +.logo-overlay.show { opacity: 1; pointer-events: auto; } +.logo-overlay.fade-out { opacity: 0; transition: opacity 600ms var(--ease-out); } + +.logo-overlay-grain { + position: absolute; + inset: 0; + pointer-events: none; + background-image: var(--grain); + opacity: 0.5; + z-index: 1; +} + +.logo-overlay-stage { + position: relative; + z-index: 2; + width: 100vw; + text-align: center; + padding: 0 4vw; +} + +.logo-overlay-word { + display: inline-block; + font-family: var(--font-display); + font-size: clamp(64px, 14vw, 180px); + line-height: 1; + letter-spacing: -0.025em; + color: var(--fg); + white-space: nowrap; + transition: + opacity 200ms var(--ease-out), + filter 200ms var(--ease-out), + letter-spacing 320ms var(--ease-out), + transform 320ms var(--ease-out); + will-change: opacity, filter, transform; + transform: scale(1); +} +.logo-overlay-word.morph-out { + opacity: 0; + filter: blur(14px); + letter-spacing: 0.22em; + transform: scale(1.04); +} +.logo-overlay-word.morph-in { + opacity: 0; + filter: blur(14px); + letter-spacing: -0.06em; + transform: scale(0.97); +} +.logo-overlay-word .tld { color: var(--accent); font-style: italic; } +.logo-overlay-word .tld-dot { color: var(--accent); } + +.nav { display: flex; flex-direction: column; gap: 14px; margin-top: 36px; } +.nav a { + font-family: var(--font-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--fg-2); + transition: color 140ms var(--ease-out); + display: inline-flex; + align-items: center; + gap: 10px; +} +.nav a::before { + content: attr(data-num); + color: var(--fg-3); + font-weight: 400; +} +.nav a:hover, .nav a.active { color: var(--fg); } +.nav a:hover::before, .nav a.active::before { color: var(--signal); } + +/* Main column */ +.main { + margin-left: 220px; + padding: 0; +} + +/* Each section */ +.section { + padding: 96px 64px; + border-bottom: 1px solid var(--border); + position: relative; +} +.section:last-child { border-bottom: none; } +.section.dark { + background: var(--ink-900); + color: var(--bone-50); + border-color: var(--ink-700); + position: relative; + overflow: hidden; +} +.section.dark::after { + content: ""; + position: absolute; inset: 0; + pointer-events: none; + background-image: url("data:image/svg+xml;utf8,"); + opacity: 0.12; + mix-blend-mode: overlay; + z-index: 0; +} +.section.dark > * { position: relative; z-index: 1; } + +.section-label { + font-family: var(--font-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--fg-2); + margin-bottom: 28px; + display: flex; + align-items: center; + gap: 8px; +} +.section.dark .section-label { color: var(--ink-300); } +.section-label .num { color: var(--signal); } +.section-label .bar { + flex: 1; + height: 1px; + background: var(--border); + max-width: 80px; +} +.section.dark .section-label .bar { background: var(--ink-600); } + +/* Hero */ +.hero { + padding: 88px 64px 96px; + background: var(--bg); + position: relative; + min-height: 100vh; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.hero-meta { + display: flex; + justify-content: space-between; + align-items: flex-start; + font-family: var(--font-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--fg-2); +} +.hero-meta b { color: var(--fg); font-weight: 500; } +.hero-meta .grp { display: flex; flex-direction: column; gap: 6px; } +.hero-meta .grp.right { align-items: flex-end; text-align: right; } +.hero-meta .grp span { white-space: nowrap; } + +.hero-headline { + font-family: var(--font-display); + font-size: clamp(72px, 11.5vw, 168px); + line-height: 0.96; + letter-spacing: -0.025em; + color: var(--fg); + text-wrap: balance; + margin: 64px 0 24px; + max-width: 13ch; +} +.hero-headline em { font-style: italic; } + +.hero-sub { + font-family: var(--font-sans); + font-size: clamp(17px, 1.4vw, 22px); + line-height: 1.45; + color: var(--fg-2); + max-width: 56ch; + margin-bottom: 40px; +} + +.hero-cta-row { + display: flex; + gap: 14px; + flex-wrap: wrap; + align-items: center; +} + +.btn { + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + line-height: 1; + padding: 14px 22px; + border: 1px solid transparent; + cursor: pointer; + transition: background 140ms var(--ease-out), color 140ms var(--ease-out), border-color 140ms var(--ease-out); + display: inline-flex; align-items: center; gap: 10px; + border-radius: 8px; +} +.btn:active { transform: scale(0.98); transition: transform 90ms var(--ease-spring); } +.btn-primary { background: var(--signal); color: var(--bone-50); } +.btn-primary:hover { background: var(--signal-hover); } +.btn-ghost { background: transparent; color: var(--fg); border-color: var(--border); } +.btn-ghost:hover { border-color: var(--fg); } +.btn-arrow { font-family: var(--font-mono); } + +/* About */ +.about-grid { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 64px; + align-items: start; +} + +.about-portrait { + width: 100%; + background: #000; + position: relative; + overflow: hidden; + aspect-ratio: 4 / 5; + border: 1px solid var(--ink-700); +} +.about-portrait img { + width: 100%; height: 100%; + object-fit: cover; + object-position: center top; + filter: contrast(1.02); +} +.about-portrait .corner { + position: absolute; + top: 12px; left: 12px; + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--bone-200); + padding: 4px 8px; + border: 1px solid rgba(255,255,255,0.2); + background: rgba(0,0,0,0.4); +} + +.about-body { display: flex; flex-direction: column; gap: 24px; max-width: 56ch; } +.about-h { + font-family: var(--font-display); + font-size: clamp(40px, 5vw, 64px); + line-height: 1.02; + letter-spacing: -0.02em; + color: var(--fg); + text-wrap: balance; +} +.about-h em { font-style: italic; } +.about-p { + font-family: var(--font-sans); + font-size: 17px; + line-height: 1.6; + color: var(--fg); +} +.about-p.muted { color: var(--fg-2); } + +.about-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0; + margin-top: 24px; + border-top: 1px solid var(--border); +} +.about-stats .stat { + padding: 22px 0 0; + border-right: 1px solid var(--border); + padding-right: 16px; +} +.about-stats .stat:last-child { border-right: none; padding-right: 0; } +.about-stats .stat:not(:first-child) { padding-left: 16px; } +.about-stats .stat .n { + font-family: var(--font-display); + font-size: 56px; + line-height: 1; + color: var(--fg); + letter-spacing: -0.02em; +} +.about-stats .stat .l { + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--fg-2); + margin-top: 8px; +} + +/* Cases */ +.cases-head { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 64px; + align-items: end; + margin-bottom: 56px; +} +.cases-title { + font-family: var(--font-display); + font-size: clamp(56px, 7vw, 96px); + line-height: 0.98; + letter-spacing: -0.02em; + color: inherit; + text-wrap: balance; +} +.cases-title em { font-style: italic; } +.cases-intro { + font-family: var(--font-sans); + font-size: 17px; + line-height: 1.6; + color: var(--ink-300); + max-width: 48ch; +} + +.cases-grid { + display: flex; + flex-direction: column; + gap: 0; +} + +.case { + display: grid; + grid-template-columns: 88px 1fr 1fr; + gap: 32px; + padding: 36px 0; + border-top: 1px solid var(--ink-700); + align-items: start; + transition: padding 240ms var(--ease-out); + cursor: default; +} +.case:hover { padding-left: 12px; } +.case:last-child { border-bottom: 1px solid var(--ink-700); } + +.case-num { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.12em; + color: var(--ink-400); + text-transform: uppercase; + padding-top: 8px; +} + +.case-title-block { + display: flex; flex-direction: column; gap: 10px; +} +.case-name { + font-family: var(--font-display); + font-size: 56px; + line-height: 0.98; + letter-spacing: -0.02em; + color: var(--bone-50); +} +.case-name em { font-style: italic; } +.case-tagline { + font-family: var(--font-sans); + font-size: 14px; + color: var(--ink-300); +} + +.case-body { display: flex; flex-direction: column; gap: 14px; padding-top: 6px; } +.case-desc { + font-family: var(--font-sans); + font-size: 15px; + line-height: 1.55; + color: var(--bone-200); +} +.case-meta { + display: flex; gap: 16px; flex-wrap: wrap; + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--ink-400); +} +.case-meta b { color: var(--bone-50); font-weight: 500; } +.case-link { + margin-top: 6px; + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + color: var(--bone-50); + display: inline-flex; + align-items: center; + gap: 8px; + transition: color 140ms; + align-self: flex-start; + border-bottom: 1px solid currentColor; + padding-bottom: 2px; +} +.case-link:hover { color: var(--signal); } + +/* Roles timeline */ +.roles { + display: grid; + grid-template-columns: 220px 1fr; + gap: 64px; +} +.roles-intro h2 { + font-family: var(--font-display); + font-size: clamp(40px, 5vw, 64px); + line-height: 1; + letter-spacing: -0.02em; + margin: 0; + color: var(--fg); +} +.roles-intro h2 em { font-style: italic; } +.roles-intro p { + margin-top: 18px; + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.55; + color: var(--fg-2); +} + +.roles-list { display: flex; flex-direction: column; gap: 0; } +.role { + display: grid; + grid-template-columns: 120px 1fr auto; + gap: 24px; + padding: 20px 0; + border-top: 1px solid var(--border); + align-items: baseline; + transition: background 140ms; +} +.role:hover { background: var(--surface); padding-left: 12px; } +.role:last-child { border-bottom: 1px solid var(--border); } + +.role-when { + font-family: var(--font-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--fg-2); +} +.role-title { + font-family: var(--font-sans); + font-size: 18px; + font-weight: 500; + color: var(--fg); + letter-spacing: -0.01em; +} +.role-title .org { + font-family: var(--font-display); + font-style: italic; + font-weight: 400; + color: var(--fg-2); + margin-left: 6px; +} +.role-tag { + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--fg-3); + text-align: right; + white-space: nowrap; +} + +/* Contact */ +.contact { + background: var(--ink-900); + color: var(--bone-50); + padding: 96px 64px 64px; + position: relative; + overflow: hidden; +} +.contact::after { + content: ""; + position: absolute; inset: 0; + pointer-events: none; + background-image: url("data:image/svg+xml;utf8,"); + opacity: 0.1; + mix-blend-mode: overlay; +} +.contact > * { position: relative; z-index: 1; } + +.contact-title { + font-family: var(--font-display); + font-size: clamp(72px, 10vw, 144px); + line-height: 0.96; + letter-spacing: -0.025em; + color: var(--bone-50); + text-wrap: balance; + margin: 0 0 40px; + max-width: 14ch; +} +.contact-title em { font-style: italic; } + +.contact-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 40px; + margin-top: 48px; + padding-top: 32px; + border-top: 1px solid var(--ink-700); +} +.contact-col { + display: flex; flex-direction: column; gap: 10px; +} +.contact-col .lbl { + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--ink-300); +} +.contact-col a, .contact-col .v { + font-family: var(--font-display); + font-size: 28px; + line-height: 1.1; + color: var(--bone-50); + letter-spacing: -0.01em; + transition: color 140ms; +} +.contact-col a:hover { color: var(--signal); } +.contact-col .v.sm { + font-family: var(--font-sans); + font-size: 14px; + color: var(--ink-200); + line-height: 1.45; +} + +/* Footer */ +.footer { + padding: 32px 64px; + background: var(--ink-900); + color: var(--ink-300); + border-top: 1px solid var(--ink-700); + display: flex; + justify-content: space-between; + align-items: center; + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.1em; + position: relative; + z-index: 1; +} +.footer .wm-sm { font-family: var(--font-display); font-size: 14px; text-transform: none; letter-spacing: -0.01em; color: var(--bone-50); } + +/* Floating "Schedule" pill */ +.float-pill { + position: fixed; + bottom: 24px; + right: 24px; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 12px 18px 12px 14px; + border-radius: 999px; + background: rgba(10,10,12,0.86); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + color: var(--bone-50); + border: 1px solid rgba(255,255,255,0.08); + box-shadow: 0 2px 0 rgba(10,10,12,0.05), 0 24px 48px -24px rgba(10,10,12,0.5); + font-family: var(--font-sans); + font-size: 13px; + font-weight: 500; + cursor: pointer; + z-index: 100; + transition: transform 200ms var(--ease-out); +} +.float-pill:hover { transform: translateY(-2px); } +.float-pill .dot { + width: 8px; height: 8px; + border-radius: 50%; + background: var(--signal); + box-shadow: 0 0 0 0 rgba(30,138,138,0.5); + animation: pulseSignal 2.2s infinite; +} +@keyframes pulseSignal { + 0%, 100% { box-shadow: 0 0 0 0 rgba(30,138,138,0.4); } + 50% { box-shadow: 0 0 0 8px rgba(30,138,138,0); } +} + +@media (max-width: 1024px) { + .rail { width: 180px; padding: 24px 18px; } + .main { margin-left: 180px; } + .section, .hero, .contact { padding-left: 40px; padding-right: 40px; } + .about-grid { grid-template-columns: 1fr; gap: 40px; } + .roles { grid-template-columns: 1fr; gap: 32px; } + .cases-head { grid-template-columns: 1fr; gap: 24px; } + .case { grid-template-columns: 60px 1fr; } + .case-body { grid-column: 1 / -1; padding-left: 60px; padding-top: 0; } +} + +@media (max-width: 720px) { + .rail { display: none; } + .main { margin-left: 0; } + .section, .hero, .contact { padding-left: 24px; padding-right: 24px; } + .hero-headline { font-size: clamp(56px, 14vw, 100px); } + .contact-grid { grid-template-columns: 1fr; gap: 28px; } + .footer { flex-direction: column; gap: 12px; padding: 24px; } + .case-name { font-size: 40px; } + .about-stats { grid-template-columns: 1fr 1fr; } + .about-stats .stat:nth-child(3) { grid-column: 1 / -1; border-right: none; padding-left: 0; padding-top: 16px; border-top: 1px solid var(--border); margin-top: 0; } +}