diff --git a/_config.yml b/_config.yml index d79a867..e87b679 100644 --- a/_config.yml +++ b/_config.yml @@ -1,5 +1,8 @@ name: Constructive Programming -description: Type-driven and LLM-augmented engineering consultancy +description: >- + Software consultancy for platform teams: AI-accelerated delivery inside + type-driven guardrails. Architecture reviews, embedded senior engineers + (Scala, Rust, TypeScript), and agent-native development workflows. url: https://www.constructive.dev title: Constructive Programming @@ -8,7 +11,7 @@ favicon: assets/img/favicon.svg # Hero header_feature_image: "" header_text: Constructive Programming -header_subtitle: Types as guardrails for AI agents. We ship production code paired with LLMs, narrowed by construction. +header_subtitle: Ship at AI speed. Skip the gamble. Production code with AI, inside engineering guardrails that make most wrong versions impossible to build. # Footer footer_text: "Copyright Constructive Programming," diff --git a/_layouts/home.html b/_layouts/home.html index 6fa6299..e8dbb8b 100644 --- a/_layouts/home.html +++ b/_layouts/home.html @@ -1,199 +1,418 @@ --- layout: default -bootstrap: true --- -
- - {% comment %} - Plain hero — no theme call-out, no background-image trick. - {% endcomment %} -
-

{{ site.header_text | default: site.title }}

- {% if site.header_subtitle %} -

{{ site.header_subtitle }}

- {% endif %} -

- What we do - - Book a scoping call - -

-
- A type signature being narrowed three times: String (space for bugs: ∞), Int (bugs can still hide in an Int), and a domain-specific type alias (not so much when you clearly delimit your domain values). -
- Narrowing: the same shape that runs through every engagement. -
-
-
- -
-
-
-

+{% comment %} + Landing page — "typed-systems editorial" redesign. + + Structure: hero → positioning → services → process → case studies → + technical excellence (dark) → research/open source → writing → CTA. + + Anchors #main, #consulting, #libraries, #fixed and #writing are kept + from the previous layout so the navbar "What we do" link and any + external deep links keep resolving. + + Motion lives in assets/js/home.js (GSAP + ScrollTrigger, CDN, defer). + Every element is visible by default — JS only *adds* motion, so the + page reads fine with JS disabled or prefers-reduced-motion set. +{% endcomment %} + + + + + +

+ + +
+ + +
+

+ Consulting · Open source · Research +

+ +

+ Ship at AI speed. + Skip the gamble. +

+ +

+ We ship production code with AI, inside engineering guardrails + that make most wrong versions impossible to build — so the + speed compounds, while the breakage doesn't. +

+ + + +
+
+
move fast and break things
+

ten thousand ways to fail, each one somebody's 2 a.m.

+
+
+
move fast and fix things
+

a hundred ways left — you're still gambling

+
+
+
move fast
+

three ways, one chosen — the rest won't even compile

+
+
+ Narrowing: the same shape that runs through every engagement. +
+
+
+
+ + +
+
+
+

01 — Positioning

+

A consultancy for serious codebases.

+
+ +
+

You're a fit if you run a platform team using a typed language (C++, Rust, Scala, TypeScript) — typically Series B–D, - 20–80 engineers — with a regression you can name, an improvement - plan you've already postponed twice, and a real interest in - letting LLMs accelerate the work without quietly eroding what - types guarantee. + 20–80 engineers — with a regression you can name, an + improvement plan you've already postponed twice, and a real + interest in letting LLMs accelerate the work without quietly + eroding what types guarantee.

+ +
    +
  • aTechnical strategy A defensible second opinion before you commit a quarter to the wrong plan.
  • +
  • bType-driven architecture Design review through a cardinality lens — fewer representable states, fewer bugs.
  • +
  • cFull-stack delivery Typed-FP features shipped in Scala, Rust, and TypeScript, inside your repo.
  • +
  • dAgent-native AI workflows LLMs constrained by types, properties, and evals — output you can trust.
  • +
  • eCloud-native systems P95s, uptime, and the infra bill treated as design inputs, not afterthoughts.
  • +
  • fTeam enablement Workshops distilled from a decade of constructive-programming practice.
  • +
-
+ -
- -

Consulting

-

- For teams that can't afford another regressive improvement plan. -

-
-
+ +
+
+
+

02 — Services

+

For teams that can't afford another regressive improvement plan.

+
+

- Constructive Programming partners with teams on their codebases, - which have accumulated the usual debts of growth — typed - boundaries treated as untyped, required fields treated as - optional, 800-line objects nobody dares touch. For us, often, - a single thread runs through every engagement: narrowing. - Shrinking the set of values a type can hold so that fewer wrong - programs compile, so thousands of lines of code go red and can be - safely deleted. So the lines that do stay are - that much easier for both humans and machines to reason about. + Constructive Programming partners with teams on codebases that + have accumulated the usual debts of growth — typed boundaries + treated as untyped, required fields treated as optional, + 800-line objects nobody dares touch. A single thread runs + through every engagement: narrowing. Shrinking the set + of values a type can hold so that fewer wrong programs compile, + so thousands of lines go red and can be safely + deleted.

-

- What is different about our practice: we ship production - code paired with LLMs. Constructive programming - principles are how we keep generative models useful — narrow - types and machine-checked properties are the strictest - guardrails an agent can be made to operate against, and they - turn non-deterministic output into a verifiable artefact. We - help teams adopt agent-native development strategies: pairing - the non-deterministic behaviour of agents with the deterministic - harnesses — static analysis, refinement types, property tests, - goal-enriched context — that grant the ability to trust the - output of a self-correcting development session. + What is different about our practice: we ship + production code paired with LLMs. Narrow types and + machine-checked properties are the strictest guardrails an + agent can operate against — they turn non-deterministic output + into a verifiable artefact.

+
-

The kinds of work we take on:

-
    -
  • Architecture and advisory — design review - through a cardinality lens; narrowing type signatures; - identifying where capability-based abstractions, refinement - types, or algebraic effects unlock simpler code.
  • -
  • IC contributions — embedded in your team - to ship typed-FP features in Scala or Rust, with the same - constructive discipline applied to PRs as to architecture.
  • -
  • Agent-native development — building the - deterministic harness around your codebase (linters, types, - properties, eval suites) so your team can move faster with - LLMs and trust the output.
  • -
  • Workshops and training — distilled from a - decade of constructive-programming workshops, tailored to - your stack and team seniority.
  • -
+
+
+

Advisory

+

Architecture & advisory

+

+ Design review through a cardinality lens; narrowing type + signatures; identifying where capability-based abstractions, + refinement types, or algebraic effects unlock simpler code. +

+

→ A written, defensible improvement plan — with effort estimates — before you spend two quarters on the wrong one.

+ Architecture review +
-

- - Our three packaged offerings — - Architecture review, Fractional advisory, Embedded IC - — with cadence, deliverables, and how a scoping call goes. - -

+
+

Delivery

+

Embedded IC contributions

+

+ Embedded in your team to ship typed-FP features in Scala or + Rust, with the same constructive discipline applied to PRs + as to architecture. +

+

→ Senior delivery hands in your repo within weeks — and a team the discipline rubs off on.

+ Embedded IC +
-

- - Recent outcomes · - 11h → 15min data pipeline at Otravo · - P95 <20ms, 100% uptime, −30% infra at Bodireel · - Total Program Cardinality static analysis at Royal Philips - -

+
+

AI

+

Agent-native development

+

+ Building the deterministic harness around your codebase — + linters, types, properties, eval suites — so the + non-deterministic behaviour of agents becomes a + self-correcting development session. +

+

→ Your team moves faster with LLMs, and can trust the output.

+ Fractional advisory +
-

- Engagements range from a focused two-week review to a multi-month - embedded role. Available for select engagements; based in the - Netherlands; comfortable with EU MSAs and standard NDAs. -

+
+

Enablement

+

Workshops & training

+

+ Distilled from a decade of constructive-programming + workshops, tailored to your stack and your team's + seniority mix. +

+

→ The practice stays after we leave.

+ All engagements +
+
-

- - Book a scoping call - -

-

- - or email consulting@constructive.dev - -

+ +
+
+ +
+
+
+

03 — Process

+

Systematic, collaborative, low-risk.

+
+ +
+ +
    +
  1. +

    01

    +

    Discover

    +

    A 30-minute scoping call — free, and honest about fit and + start dates. You describe the problem; we work out together + which offering fits. A written one-page scope follows within + 48 hours.

    +
  2. +
  3. +

    02

    +

    Design

    +

    Architecture review through a cardinality lens: observed + structure, the strongest risks, and three to five concrete + narrowing moves with effort estimates.

    +
  4. +
  5. +

    03

    +

    Build

    +

    Pull requests, design docs, and eval suites — shipped + under your repo and your release cadence, paired with LLMs + against deterministic harnesses.

    +
  6. +
  7. +

    04

    +

    Launch

    +

    Machine-checked before it merges: types, properties, and + evals green. Pair sessions on demand so your team owns what + ships.

    +
  8. +
  9. +

    05

    +

    Improve

    +

    Monthly written summaries for stakeholders, a hiring + rubric, team rituals — the constructive discipline stays + after we leave.

    +
  10. +
+ +
+

If the readout doesn't surface at least one narrowing move + worth more than the fee, we refund half. The report is yours to + keep regardless.

+ +
-
- -
- -

Open source contributions

- {% include libraries.html featured=true %} -

- All libraries → -

-
- -
- -

A new functional language

-

- Program against capabilities, not data structures. -

-
-
-

- Fixed takes recent advances in programming language design — - algebraic effects with evidence passing, Perceus reference counting, - functional-but-in-place updates — and pushes the whole language up to - a capabilities level. You describe what your methods need in order to function (cap); - the compiler picks the datastructures and machine layout. Properties (prop) - live alongside the capabilities they govern; refinement types are - first-class. -

-

- The design draws on - Quine's predicate functor logic - and Quantitative Type Theory. The language is still at Phase 0 — design — it is in flight, - with eleven example programs exercising the language. The compiler - (planned in Scala, targeting C) and the formal spec on their way. + + + +

+
+
+

04 — Recent outcomes

+

Results you can put in a board deck.

+
+ +
+
+ +
+

Otravo · Travel e-commerce

+

A data pipeline measured in hours

+

A core pipeline ran for eleven hours — too slow to rerun, + too central to ignore. Rebuilt with optics and a narrowed + data model.

+

Eleven hours to fifteen minutes.

+
+
+ +
+ +
+

Bodireel · Consumer product

+

Fast, up, and cheaper to run

+

A consumer-facing system that had to stay fast and stay up + while the infrastructure bill came down — performance and + cost treated as design inputs.

+

P95 under 20ms, 100% uptime, 30% less infra.

+
+
+ +
+ +
+

Royal Philips · Health technology

+

Correctness at programme scale

+

Static analysis grounded in Total Program Cardinality — + counting the inhabitants of a program's types to keep + quality measurable as the codebase grows.

+

A code-quality metric the compiler can enforce.

+
+
+
+
+
+ + +
+
+
+

05 — Engineering depth

+

Machine-checked, by construction.

+

The harness we build around every codebase — each layer + narrows what the one above it can get wrong.

+
+ +
+ + +
    +
  • Scalable architecture Capability-based abstractions and algebraic effects that stay simple as the system grows.
  • +
  • APIs & contracts Narrowed type signatures at every boundary — required means required.
  • +
  • Security Illegal states unrepresentable beats illegal states validated.
  • +
  • Performance P95s and pipeline runtimes treated as deliverables, not side effects.
  • +
  • Clean code Fewer representable states means thousands of lines can go red — and be safely deleted.
  • +
  • Maintainable systems Deterministic harnesses your team — and your agents — keep building against.
  • +
+
+
+
+ + +
+
+
+

06 — Research & open source

+

The practice, in public.

+
+ +
+ {% include libraries.html featured=true %} +
+ +
+

Fixed — a new functional language

+

+ Program against capabilities, not data structures. Fixed + pushes algebraic effects, Perceus reference counting, and + functional-but-in-place updates up to a capabilities level: + you describe what your methods need (cap), the + compiler picks the data structures and machine layout. + Properties (prop) live alongside the capabilities + they govern; refinement types are first-class. Phase 0 — + design — with eleven example programs exercising the language.

-

+

-
- -
- -

Long-form on type systems, optics, cardinality, and LLM-augmented engineering.

- {% include medium-posts.html %} -

- - More on Medium → - -

-
+ + + +
+
+
+

07 — Writing

+

Long-form on type systems, optics, cardinality, and LLM-augmented engineering.

+
+ +
+ {% include medium-posts.html %} +
+ +

+ More on Medium → +

+
+
+ + +
+
+

+ Available for select engagements +

+

Build something useful, scalable, and beautifully crafted.

+

+ Engagements range from a focused two-week review to a multi-month + embedded role. Based in the Netherlands; comfortable with EU MSAs + and standard NDAs. The scoping call is free — and honest about fit. +

+ +
+
+ + + + diff --git a/_sass/home.scss b/_sass/home.scss new file mode 100644 index 0000000..d8bc07a --- /dev/null +++ b/_sass/home.scss @@ -0,0 +1,798 @@ +/* ============================================================ + Landing page — "typed-systems editorial" layer. + + Scoped to .cp-home (plus a body background hook). Tokens are CSS + custom properties so the existing light/dark theme toggle + (html[data-theme]) keeps working. Fraunces (variable serif) is the + display face for the landing page only; body copy stays on the + system sans stack defined in main.scss. + + Motion notes: everything here is visible by default. GSAP + (assets/js/home.js) only *adds* entrances — no content is hidden + in CSS, so no-JS and prefers-reduced-motion users get the full + page, static. + ============================================================ */ + +$hp-serif: "Fraunces", "Iowan Old Style", Georgia, "Times New Roman", serif; + +.cp-home { + /* --- Tokens: light (warm paper) ------------------------------- */ + --hp-bg: #faf8f3; + --hp-ink: #1b1b26; + --hp-muted: rgba(27, 27, 38, 0.62); + --hp-faint: rgba(27, 27, 38, 0.45); + --hp-line: rgba(27, 27, 38, 0.14); + --hp-line-soft: rgba(27, 27, 38, 0.07); + --hp-accent: #3b3b8c; + --hp-accent-ink: #32327a; + --hp-accent-soft: rgba(59, 59, 140, 0.08); + --hp-accent-mid: rgba(59, 59, 140, 0.18); + --hp-card-bg: #fffdf9; + --hp-stat-bg: linear-gradient(135deg, rgba(59, 59, 140, 0.10), rgba(59, 59, 140, 0.03)); + + background: var(--hp-bg); + color: var(--hp-ink); + overflow-x: clip; + /* .site-header is a full-width left float the theme never clears. + Block children overlap it harmlessly, but flex sections (BFCs) + would be squeezed into the zero remaining space beside it. */ + clear: both; +} + +html[data-theme="dark"] .cp-home { + --hp-bg: #1f242c; + --hp-ink: #d9e0ea; + --hp-muted: rgba(217, 224, 234, 0.66); + --hp-faint: rgba(217, 224, 234, 0.45); + --hp-line: rgba(217, 224, 234, 0.16); + --hp-line-soft: rgba(217, 224, 234, 0.08); + --hp-accent: #9b9bf2; + --hp-accent-ink: #b3b3f7; + --hp-accent-soft: rgba(138, 138, 240, 0.10); + --hp-accent-mid: rgba(138, 138, 240, 0.22); + --hp-card-bg: #262c35; + --hp-stat-bg: linear-gradient(135deg, rgba(138, 138, 240, 0.14), rgba(138, 138, 240, 0.04)); +} + +/* Page background must extend behind navbar + footer, not just the + .cp-home block, or the warm paper ends in a hard seam. */ +html body:has(.cp-home) { background: #faf8f3; } +html[data-theme="dark"] body:has(.cp-home) { background: #1f242c; } + +/* The theme paints header/footer with --background (pure white / + #22272e), which seams against the warm paper. Let them inherit. */ +body:has(.cp-home) .site-header, +body:has(.cp-home) .site-footer { background: transparent; } + +/* --- Shared primitives ------------------------------------------ */ + +.cp-home .hp-wrap { + max-width: 76rem; + margin-inline: auto; + padding-inline: clamp(1.25rem, 4vw, 3rem); +} + +.cp-home .hp-section { + padding-block: clamp(4.5rem, 9vw, 8rem); + border-top: 1px solid var(--hp-line-soft); +} + +.cp-home .hp-eyebrow { + font-family: $cp-mono; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--hp-accent); + margin: 0 0 1.25rem; +} + +.cp-home .hp-h2 { + font-family: $hp-serif; + font-weight: 560; + font-variation-settings: "opsz" 90; + font-size: clamp(2.1rem, 4.5vw, 3.4rem); + line-height: 1.06; + letter-spacing: -0.015em; + margin: 0; + max-width: 22ch; + text-wrap: balance; +} + +.cp-home .hp-h2 em { + font-style: italic; + color: var(--hp-accent-ink); +} + +.cp-home .hp-section-head { margin-bottom: clamp(2.5rem, 5vw, 4.5rem); } + +.cp-home .hp-section-lead { + font-size: 1.15rem; + color: var(--hp-muted); + max-width: 52ch; + margin: 1.25rem 0 0; + line-height: 1.6; +} + +.cp-home .hp-large { + font-size: clamp(1.2rem, 1.8vw, 1.45rem); + line-height: 1.55; + letter-spacing: -0.008em; + margin: 0; +} + +.cp-home .hp-aside { + font-size: 0.92rem; + color: var(--hp-muted); + margin: 0.85rem 0 0; +} + +.cp-home h3 { + font-family: $hp-serif; + font-weight: 600; + font-size: 1.45rem; + letter-spacing: -0.01em; + line-height: 1.15; + margin: 0 0 0.6rem; +} + +.cp-home a:focus-visible, +.cp-home .btn:focus-visible { + outline: 2px solid var(--hp-accent); + outline-offset: 3px; +} + +/* Buttons inherit main.scss; re-derive colours from tokens so the + dark hero/dark section stay legible. */ +.cp-home .btn-primary { + background-color: var(--hp-accent); + border-color: var(--hp-accent); + color: var(--hp-bg); +} +.cp-home .btn-primary:hover { background-color: var(--hp-accent-ink); border-color: var(--hp-accent-ink); color: var(--hp-bg); } +.cp-home .btn-outline-primary { color: var(--hp-accent); border-color: var(--hp-accent-mid); background: transparent; } +.cp-home .btn-outline-primary:hover { background: var(--hp-accent-soft); color: var(--hp-accent-ink); border-color: var(--hp-accent); } + +/* The theme's html[data-theme="dark"] button rules (teal --link) outrank + the .cp-home overrides above; restate them at dark specificity. */ +html[data-theme="dark"] .cp-home .btn-primary { + background-color: var(--hp-accent); + border-color: var(--hp-accent); + color: #14141d; +} +html[data-theme="dark"] .cp-home .btn-primary:hover, +html[data-theme="dark"] .cp-home .btn-primary:focus { + background-color: var(--hp-accent-ink); + border-color: var(--hp-accent-ink); + color: #14141d; +} +html[data-theme="dark"] .cp-home .btn-outline-primary { + color: var(--hp-accent); + border-color: var(--hp-accent-mid); +} +html[data-theme="dark"] .cp-home .btn-outline-primary:hover, +html[data-theme="dark"] .cp-home .btn-outline-primary:focus { + background: var(--hp-accent-soft); + color: var(--hp-accent-ink); + border-color: var(--hp-accent); +} + +/* ============================================================ Hero */ + +.cp-home .hp-hero { + position: relative; + padding-block: clamp(4rem, 9vh, 7rem) clamp(4rem, 8vw, 6.5rem); + min-height: min(92vh, 60rem); + display: flex; + align-items: center; +} + +.cp-home .hp-hero-bg { + position: absolute; + inset: 0; + pointer-events: none; + /* fine blueprint grid, faded at the edges */ + background-image: + linear-gradient(var(--hp-line-soft) 1px, transparent 1px), + linear-gradient(90deg, var(--hp-line-soft) 1px, transparent 1px); + background-size: 72px 72px; + -webkit-mask-image: radial-gradient(120% 90% at 50% 10%, black 30%, transparent 75%); + mask-image: radial-gradient(120% 90% at 50% 10%, black 30%, transparent 75%); +} + +.cp-home .hp-hero-bg canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + opacity: 0.5; +} + +.cp-home .hp-hero .hp-wrap { position: relative; width: 100%; } + +.cp-home .hp-hero-eyebrow { margin-bottom: 2rem; color: var(--hp-faint); } +.cp-home .hp-hero-eyebrow span { color: var(--hp-accent); } + +.cp-home .hp-hero-title { + font-family: $hp-serif; + font-weight: 540; + font-variation-settings: "opsz" 144; + font-size: clamp(2.9rem, 8.2vw, 6.4rem); + line-height: 1.0; + letter-spacing: -0.022em; + margin: 0 0 1.75rem; + max-width: 14ch; +} + +.cp-home .hp-hero-title em { + font-style: italic; + color: var(--hp-accent-ink); +} + +.cp-home .hp-line { display: block; overflow: hidden; padding-block: 0.06em; } +.cp-home .hp-line-inner { display: inline-block; will-change: transform; } + +.cp-home .hp-hero-lead { + font-size: clamp(1.1rem, 1.6vw, 1.3rem); + line-height: 1.6; + color: var(--hp-muted); + max-width: 46ch; + margin: 0 0 2.25rem; +} + +.cp-home .hp-hero-cta { + display: flex; + flex-wrap: wrap; + gap: 0.85rem; + align-items: center; +} + +.cp-home .hp-magnetic { will-change: transform; } + +/* --- Narrowing diagram (HTML rebuild of hero-narrowing.svg) ------ */ + +.cp-home .hp-narrowing { + margin: clamp(3rem, 6vw, 4.5rem) 0 0; + max-width: 64rem; +} + +.cp-home .hp-bar-row { margin-bottom: 1.1rem; } +.cp-home .hp-bar-row-2 { margin-left: 7%; } +.cp-home .hp-bar-row-3 { margin-left: 14%; } + +.cp-home .hp-bar { + box-sizing: border-box; + display: flex; + align-items: center; + height: clamp(2.6rem, 5vw, 3.3rem); + padding-inline: clamp(0.9rem, 2vw, 1.4rem); + border: 1px solid var(--hp-accent); + border-radius: 0.55rem; + background: var(--hp-accent-soft); + transform-origin: left center; + overflow: hidden; + white-space: nowrap; +} +.cp-home .hp-bar-1 { width: 100%; } +.cp-home .hp-bar-2 { width: 61.8%; background: var(--hp-accent-mid); } +.cp-home .hp-bar-3 { width: 38.2%; background: var(--hp-accent-mid); border-width: 1.4px; } + +.cp-home .hp-bar code { + font-family: $cp-mono; + font-size: clamp(0.82rem, 1.5vw, 1.05rem); + font-weight: 500; + background: none; + color: var(--hp-ink); +} +.cp-home .hp-key { color: var(--hp-accent-ink); font-weight: 600; } +.cp-home .hp-dim { color: var(--hp-faint); } + +.cp-home .hp-bar-sub { + font-size: 0.88rem; + color: var(--hp-muted); + margin: 0.45rem 0 0 0.15rem; +} + +.cp-home .hp-bar-caption { + font-family: $cp-mono; + font-size: 0.78rem; + letter-spacing: 0.06em; + color: var(--hp-faint); + margin-top: 1.75rem; +} + +@media (max-width: 640px) { + .cp-home .hp-bar-row-2 { margin-left: 3%; } + .cp-home .hp-bar-row-3 { margin-left: 6%; } + .cp-home .hp-bar-2 { width: 82%; } + .cp-home .hp-bar-3 { width: 70%; } + .cp-home .hp-bar-3 code { font-size: 0.7rem; } +} + +/* ==================================================== Positioning */ + +.cp-home .hp-positioning-grid { + display: grid; + gap: clamp(2.5rem, 5vw, 5rem); + align-items: start; +} +@media (min-width: 900px) { + .cp-home .hp-positioning-grid { grid-template-columns: 5fr 6fr; } +} + +.cp-home .hp-capabilities { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: 1fr; + gap: 0; +} +@media (min-width: 560px) { + .cp-home .hp-capabilities { grid-template-columns: 1fr 1fr; column-gap: 2.5rem; } +} + +.cp-home .hp-capabilities li { + border-top: 1px solid var(--hp-line); + padding: 1.1rem 0 1.3rem; + font-size: 0.97rem; + line-height: 1.5; + color: var(--hp-muted); +} + +.cp-home .hp-capabilities strong { + display: block; + color: var(--hp-ink); + font-weight: 650; + letter-spacing: -0.01em; + margin-bottom: 0.3rem; + font-size: 1.05rem; +} + +.cp-home .hp-cap-index { + float: right; + font-family: $cp-mono; + font-size: 0.72rem; + color: var(--hp-faint); + padding-top: 0.25rem; +} + +/* ======================================================= Services */ + +.cp-home .hp-services-intro { + display: grid; + gap: clamp(1.5rem, 3vw, 3rem); + margin-bottom: clamp(2.5rem, 5vw, 4rem); + max-width: 100%; +} +@media (min-width: 900px) { + .cp-home .hp-services-intro { grid-template-columns: 1fr 1fr; } +} +.cp-home .hp-services-intro p { + margin: 0; + font-size: 1.04rem; + line-height: 1.65; + color: var(--hp-muted); +} +.cp-home .hp-services-intro strong, +.cp-home .hp-services-intro em { color: var(--hp-ink); } + +.cp-home .hp-services-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.1rem; +} +@media (min-width: 760px) { + .cp-home .hp-services-grid { grid-template-columns: 1fr 1fr; } +} + +.cp-home .hp-card { + display: flex; + flex-direction: column; + border: 1px solid var(--hp-line); + border-radius: 0.9rem; + background: var(--hp-card-bg); + padding: clamp(1.4rem, 2.5vw, 2rem); + transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), + border-color 0.35s ease, + box-shadow 0.35s ease; +} + +.cp-home .hp-card:hover { + transform: translateY(-5px); + border-color: var(--hp-accent-mid); + box-shadow: 0 18px 40px -22px rgba(27, 27, 60, 0.35); +} + +.cp-home .hp-card-tag { + font-family: $cp-mono; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--hp-faint); + margin: 0 0 1.4rem; +} + +.cp-home .hp-card-body { + font-size: 0.99rem; + line-height: 1.6; + color: var(--hp-muted); + margin: 0 0 1.4rem; +} + +.cp-home .hp-card-outcome { + font-size: 1.02rem; + line-height: 1.5; + color: var(--hp-ink); + font-weight: 550; + letter-spacing: -0.006em; + border-top: 1px solid var(--hp-line-soft); + padding-top: 1.1rem; + margin: auto 0 1.4rem; +} + +.cp-home .hp-card-link { + font-family: $cp-mono; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--hp-accent); + text-decoration: none; + align-self: flex-start; + position: relative; +} +.cp-home .hp-card-link::after { + content: ""; + position: absolute; + left: 0; bottom: -3px; + width: 100%; + height: 1px; + background: var(--hp-accent); + transform: scaleX(0); + transform-origin: left center; + transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); +} +.cp-home .hp-card-link:hover::after { transform: scaleX(1); } + +.cp-home .hp-services-cta { + margin-top: clamp(2.5rem, 5vw, 4rem); + text-align: center; +} + +/* ======================================================== Process */ + +.cp-home .hp-process { + position: relative; + max-width: 46rem; +} + +.cp-home .hp-process-rail { + position: absolute; + top: 0.4rem; + bottom: 0.4rem; + left: 0.45rem; + width: 1px; + background: var(--hp-line); +} +.cp-home .hp-process-line { + position: absolute; + inset: 0; + background: var(--hp-accent); + transform-origin: top center; + /* JS scrubs scaleY 0→1; without JS the line is simply full. */ +} + +.cp-home .hp-steps { + list-style: none; + margin: 0; + padding: 0; +} + +.cp-home .hp-step { + position: relative; + padding: 0 0 clamp(2.2rem, 4vw, 3.2rem) clamp(2.4rem, 5vw, 3.5rem); +} +.cp-home .hp-step:last-child { padding-bottom: 0; } + +.cp-home .hp-step::before { + content: ""; + position: absolute; + left: 0.18rem; + top: 0.42rem; + width: 0.6rem; + height: 0.6rem; + border-radius: 50%; + background: var(--hp-bg); + border: 1.5px solid var(--hp-accent); +} + +.cp-home .hp-step-num { + font-family: $cp-mono; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.16em; + color: var(--hp-accent); + margin: 0 0 0.35rem; +} + +.cp-home .hp-step p:last-child { + margin: 0; + font-size: 0.99rem; + line-height: 1.6; + color: var(--hp-muted); + max-width: 52ch; +} + +.cp-home .hp-pullquote { + margin: clamp(3rem, 6vw, 5rem) 0 0 auto; + max-width: 38rem; + border: 0; + border-left: 2px solid var(--hp-accent); + padding: 0.4rem 0 0.4rem 1.75rem; +} +.cp-home .hp-pullquote p { + font-family: $hp-serif; + font-style: italic; + font-size: clamp(1.25rem, 2.2vw, 1.6rem); + line-height: 1.4; + margin: 0 0 0.85rem; +} +.cp-home .hp-pullquote footer { + font-family: $cp-mono; + font-size: 0.78rem; + letter-spacing: 0.08em; + color: var(--hp-faint); +} + +/* =================================================== Case studies */ + +.cp-home .hp-cases { + display: flex; + flex-direction: column; + gap: clamp(2.5rem, 5vw, 4.5rem); +} + +.cp-home .hp-case { + display: grid; + gap: clamp(1.5rem, 3vw, 3rem); + align-items: center; +} +@media (min-width: 860px) { + .cp-home .hp-case { grid-template-columns: 7fr 5fr; } + .cp-home .hp-case:nth-child(even) .hp-case-visual { order: 2; } +} + +.cp-home .hp-case-visual { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + aspect-ratio: 16 / 9; + border: 1px solid var(--hp-line); + border-radius: 1.1rem; + background: + linear-gradient(var(--hp-line-soft) 1px, transparent 1px) 0 0 / 48px 48px, + linear-gradient(90deg, var(--hp-line-soft) 1px, transparent 1px) 0 0 / 48px 48px, + var(--hp-stat-bg); + overflow: hidden; +} + +.cp-home .hp-case-stat { + font-family: $hp-serif; + font-weight: 540; + font-variation-settings: "opsz" 144; + font-size: clamp(3rem, 7vw, 5.5rem); + letter-spacing: -0.03em; + color: var(--hp-accent-ink); + line-height: 1; +} + +.cp-home .hp-case-statsub { + font-family: $cp-mono; + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--hp-muted); +} + +.cp-home .hp-case-copy .hp-eyebrow { margin-bottom: 0.85rem; } +.cp-home .hp-case-copy h3 { max-width: 18ch; } +.cp-home .hp-case-copy p { + font-size: 0.99rem; + line-height: 1.6; + color: var(--hp-muted); + max-width: 44ch; + margin: 0 0 1rem; +} + +.cp-home .hp-case-result { + font-weight: 650; + color: var(--hp-ink) !important; + letter-spacing: -0.008em; +} + +/* =========================================== Technical excellence */ + +.cp-home .hp-dark { + --hp-bg: #14141d; + --hp-ink: #e8e8f2; + --hp-muted: rgba(232, 232, 242, 0.64); + --hp-faint: rgba(232, 232, 242, 0.42); + --hp-line: rgba(232, 232, 242, 0.16); + --hp-line-soft: rgba(232, 232, 242, 0.08); + --hp-accent: #a3a3f5; + --hp-accent-ink: #bcbcf8; + --hp-accent-soft: rgba(163, 163, 245, 0.10); + --hp-accent-mid: rgba(163, 163, 245, 0.26); + + background: #14141d; + color: var(--hp-ink); + border-top: 0; +} + +.cp-home .hp-excellence { + display: grid; + gap: clamp(2.5rem, 5vw, 5rem); + align-items: center; +} +@media (min-width: 900px) { + .cp-home .hp-excellence { grid-template-columns: 6fr 5fr; } +} + +.cp-home .hp-layers { + display: flex; + flex-direction: column; + gap: 0.7rem; +} + +.cp-home .hp-layer { + display: flex; + align-items: baseline; + gap: 1rem; + border: 1px solid var(--hp-accent-mid); + border-radius: 0.6rem; + background: var(--hp-accent-soft); + padding: 0.85rem 1.25rem; + margin-inline: auto; + width: 100%; + white-space: nowrap; + overflow: hidden; +} +/* each layer narrows the one above it — the hero motif, restated */ +.cp-home .hp-layer:nth-child(2) { width: 86%; } +.cp-home .hp-layer:nth-child(3) { width: 72%; } +.cp-home .hp-layer:nth-child(4) { width: 58%; } +.cp-home .hp-layer:nth-child(5) { width: 44%; background: var(--hp-accent-mid); } + +.cp-home .hp-layer code { + font-family: $cp-mono; + font-size: 0.92rem; + font-weight: 600; + color: var(--hp-accent-ink); + background: none; +} +.cp-home .hp-layer span { + font-size: 0.85rem; + color: var(--hp-muted); + text-overflow: ellipsis; + overflow: hidden; +} + +@media (max-width: 560px) { + .cp-home .hp-layer span { display: none; } + .cp-home .hp-layer:nth-child(4) { width: 66%; } + .cp-home .hp-layer:nth-child(5) { width: 54%; } +} + +.cp-home .hp-depth { + list-style: none; + margin: 0; + padding: 0; +} +.cp-home .hp-depth li { + border-top: 1px solid var(--hp-line-soft); + padding: 0.95rem 0; + font-size: 0.95rem; + line-height: 1.55; + color: var(--hp-muted); +} +.cp-home .hp-depth li:last-child { border-bottom: 1px solid var(--hp-line-soft); } +.cp-home .hp-depth strong { + color: var(--hp-ink); + font-weight: 650; + margin-right: 0.5rem; +} + +/* ===================================== Research & open source */ + +.cp-home .hp-fixed { + margin-top: clamp(3rem, 6vw, 5rem); + border-top: 1px solid var(--hp-line); + padding-top: clamp(2rem, 4vw, 3rem); + max-width: 56rem; +} +.cp-home .hp-fixed .hp-large { color: var(--hp-muted); } +.cp-home .hp-fixed .hp-large strong, +.cp-home .hp-fixed .hp-large code { color: var(--hp-ink); } +.cp-home .hp-fixed-links { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin: 1.75rem 0 0; +} + +/* Cards from the shared includes, re-skinned to the landing tokens. */ +.cp-home .card { + background: var(--hp-card-bg); + border: 1px solid var(--hp-line); + border-radius: 0.9rem; + transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.35s ease; +} +.cp-home .card:hover { transform: translateY(-4px); border-color: var(--hp-accent-mid); } +html[data-theme="dark"] .cp-home .card { background: var(--hp-card-bg); border-color: var(--hp-line); } + +.cp-home .hp-writing-more { + margin: 2rem 0 0; + font-family: $cp-mono; + font-size: 0.85rem; + letter-spacing: 0.06em; +} + +/* ====================================================== Final CTA */ + +.cp-home .hp-final { + text-align: center; + padding-block: clamp(6rem, 12vw, 10rem); +} + +.cp-home .hp-final .hp-eyebrow { margin-bottom: 1.75rem; } + +.cp-home .hp-availability { + display: inline-flex; + align-items: center; + gap: 0.55rem; + border: 1px solid var(--hp-line); + border-radius: 99px; + padding: 0.45rem 1rem; + color: var(--hp-muted); +} +.cp-home .hp-availability-dot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background: #3e9c5f; + animation: hp-pulse 2.4s ease-in-out infinite; +} +@keyframes hp-pulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(62, 156, 95, 0.45); } + 50% { box-shadow: 0 0 0 6px rgba(62, 156, 95, 0); } +} + +.cp-home .hp-final-title { + margin-inline: auto; + max-width: 18ch; + font-size: clamp(2.4rem, 5.5vw, 4.2rem); +} + +.cp-home .hp-final-lead { + font-size: 1.08rem; + line-height: 1.65; + color: var(--hp-muted); + max-width: 52ch; + margin: 1.75rem auto 2.5rem; +} + +.cp-home .hp-final .hp-hero-cta { justify-content: center; } + +/* ====================================== Reduced motion / no-JS */ + +@media (prefers-reduced-motion: reduce) { + .cp-home .hp-availability-dot { animation: none; } + .cp-home .hp-card, + .cp-home .card, + .cp-home .hp-card-link::after { transition: none; } +} diff --git a/assets/css/main.scss b/assets/css/main.scss index f9e6903..e2a3c42 100644 --- a/assets/css/main.scss +++ b/assets/css/main.scss @@ -766,3 +766,10 @@ html[data-theme="dark"] .site-footer .footer-icons a:hover { color: $cp-accent-d back to invisible on stripped Linux font sets. */ .fa-adjust::before { content: "\25D0"; margin-right: .25em; } /* ◐ left half black */ .fa-adjust.fa-rotate-180::before { content: "\25D1"; margin-right: .25em; } /* ◑ right half black */ + +/* ============================================================ + Landing page redesign — "typed-systems editorial" layer. + Lives in _sass/home.scss; scoped to .cp-home. Imported last so + its tokens win over the generic card/button rules above. + ============================================================ */ +@import 'home'; diff --git a/assets/js/home.js b/assets/js/home.js new file mode 100644 index 0000000..1af06d2 --- /dev/null +++ b/assets/js/home.js @@ -0,0 +1,211 @@ +/* Landing page motion — GSAP + ScrollTrigger (loaded via CDN, defer). + * + * Design constraints: + * - Content is fully visible without JS; this file only ADDS motion. + * - prefers-reduced-motion: no entrances, no parallax, no canvas. + * - Canvas pauses when the hero is off-screen or the tab is hidden. + */ +(function () { + 'use strict'; + + var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + var home = document.querySelector('.cp-home'); + if (!home) return; + + /* ---------------------------------------------------- Canvas */ + // A sparse field of drifting nodes; near nodes get a connecting + // line. Reads as "flowing logic" without demanding attention. + function startFlowField() { + var canvas = document.getElementById('hp-flow'); + if (!canvas) return; + var ctx = canvas.getContext('2d'); + var dpr = Math.min(window.devicePixelRatio || 1, 2); + var w = 0, h = 0, nodes = [], running = false, raf = 0; + + function accent() { + return document.documentElement.getAttribute('data-theme') === 'dark' + ? { r: 155, g: 155, b: 242 } + : { r: 59, g: 59, b: 140 }; + } + + function resize() { + var rect = canvas.parentElement.getBoundingClientRect(); + w = rect.width; h = rect.height; + canvas.width = w * dpr; canvas.height = h * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + var count = Math.min(70, Math.floor(w * h / 26000)); + nodes = []; + for (var i = 0; i < count; i++) { + nodes.push({ + x: Math.random() * w, + y: Math.random() * h, + vx: (Math.random() - 0.5) * 0.22, + vy: (Math.random() - 0.5) * 0.22 + }); + } + } + + function tick() { + if (!running) return; + ctx.clearRect(0, 0, w, h); + var c = accent(); + var i, j, a, b, dx, dy, d2; + for (i = 0; i < nodes.length; i++) { + a = nodes[i]; + // gentle sine drift — "flow", not bounce + a.x += a.vx + Math.sin((a.y + performance.now() * 0.02) * 0.004) * 0.12; + a.y += a.vy; + if (a.x < -20) a.x = w + 20; else if (a.x > w + 20) a.x = -20; + if (a.y < -20) a.y = h + 20; else if (a.y > h + 20) a.y = -20; + ctx.beginPath(); + ctx.arc(a.x, a.y, 1.4, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',0.35)'; + ctx.fill(); + } + for (i = 0; i < nodes.length; i++) { + for (j = i + 1; j < nodes.length; j++) { + a = nodes[i]; b = nodes[j]; + dx = a.x - b.x; dy = a.y - b.y; d2 = dx * dx + dy * dy; + if (d2 < 110 * 110) { + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.strokeStyle = 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + + (0.12 * (1 - d2 / (110 * 110))).toFixed(3) + ')'; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + } + raf = requestAnimationFrame(tick); + } + + function setRunning(on) { + if (on === running) return; + running = on; + if (on) raf = requestAnimationFrame(tick); + else cancelAnimationFrame(raf); + } + + resize(); + window.addEventListener('resize', resize); + document.addEventListener('visibilitychange', function () { + setRunning(!document.hidden && visible); + }); + var visible = true; + new IntersectionObserver(function (entries) { + visible = entries[0].isIntersecting; + setRunning(visible && !document.hidden); + }).observe(canvas.parentElement); + setRunning(true); + } + + if (!reduced) startFlowField(); + + /* ------------------------------------------------ GSAP motion */ + if (reduced || typeof gsap === 'undefined') return; + if (typeof ScrollTrigger !== 'undefined') gsap.registerPlugin(ScrollTrigger); + + var EASE = 'power3.out'; + + // Hero entrance — one timeline, compiler-pass pacing. + var hero = gsap.timeline({ defaults: { ease: EASE } }); + hero + .from('[data-hero="eyebrow"]', { y: 14, autoAlpha: 0, duration: 0.7 }) + .from('[data-hero="line"]', { + yPercent: 115, duration: 1.0, stagger: 0.12 + }, '-=0.45') + .from('[data-hero="fade"]', { + y: 26, autoAlpha: 0, duration: 0.9, stagger: 0.14 + }, '-=0.55') + // the narrowing: each bar enters wide, then narrows to its type + .from('.hp-bar-row', { autoAlpha: 0, y: 14, duration: 0.5, stagger: 0.28 }, '-=0.6') + .from('.hp-bar-2', { width: '100%', duration: 0.9, ease: 'power4.inOut', clearProps: 'width' }, '-=0.7') + .from('.hp-bar-3', { width: '100%', duration: 0.9, ease: 'power4.inOut', clearProps: 'width' }, '-=0.55') + .from('.hp-bar-caption', { autoAlpha: 0, duration: 0.6 }, '-=0.3'); + + if (typeof ScrollTrigger === 'undefined') return; + + // Generic reveals. + gsap.utils.toArray('[data-reveal]').forEach(function (el) { + gsap.from(el, { + y: 32, autoAlpha: 0, duration: 0.95, ease: EASE, + scrollTrigger: { trigger: el, start: 'top 84%', once: true } + }); + }); + + // Grouped reveals — children stagger. + gsap.utils.toArray('[data-reveal-group]').forEach(function (group) { + gsap.from(group.children, { + y: 24, autoAlpha: 0, duration: 0.8, ease: EASE, stagger: 0.08, + scrollTrigger: { trigger: group, start: 'top 84%', once: true } + }); + }); + + // Service cards. + gsap.utils.toArray('[data-stagger]').forEach(function (grid) { + gsap.from(grid.children, { + y: 44, autoAlpha: 0, duration: 0.9, ease: EASE, stagger: 0.12, + scrollTrigger: { trigger: grid, start: 'top 82%', once: true } + }); + }); + + // Process — the rail draws as you scroll; steps pop in. + var rail = document.querySelector('.hp-process-line'); + if (rail) { + gsap.fromTo(rail, { scaleY: 0 }, { + scaleY: 1, ease: 'none', + scrollTrigger: { + trigger: '.hp-process', + start: 'top 75%', end: 'bottom 55%', scrub: 0.6 + } + }); + } + gsap.utils.toArray('[data-step]').forEach(function (step) { + gsap.from(step, { + x: -28, autoAlpha: 0, duration: 0.8, ease: EASE, + scrollTrigger: { trigger: step, start: 'top 82%', once: true } + }); + }); + + // Case studies — copy reveals; the stat block drifts (parallax). + gsap.utils.toArray('[data-case]').forEach(function (item) { + gsap.from(item.querySelector('.hp-case-copy'), { + y: 36, autoAlpha: 0, duration: 0.95, ease: EASE, + scrollTrigger: { trigger: item, start: 'top 80%', once: true } + }); + gsap.from(item.querySelector('.hp-case-visual'), { + autoAlpha: 0, scale: 0.96, duration: 1.0, ease: EASE, + scrollTrigger: { trigger: item, start: 'top 80%', once: true } + }); + gsap.fromTo(item.querySelector('.hp-case-stat'), { y: 18 }, { + y: -18, ease: 'none', + scrollTrigger: { trigger: item, start: 'top bottom', end: 'bottom top', scrub: 0.8 } + }); + }); + + // Excellence layers — each layer draws in, narrowing as it goes. + var layers = gsap.utils.toArray('[data-layer]'); + if (layers.length) { + gsap.from(layers, { + width: 0, autoAlpha: 0, duration: 0.85, ease: 'power4.inOut', stagger: 0.12, + scrollTrigger: { trigger: '.hp-layers', start: 'top 78%', once: true } + }); + } + + /* --------------------------------------------- Magnetic CTAs */ + if (window.matchMedia('(pointer: fine)').matches) { + document.querySelectorAll('.hp-magnetic').forEach(function (btn) { + var setX = gsap.quickTo(btn, 'x', { duration: 0.35, ease: 'power3.out' }); + var setY = gsap.quickTo(btn, 'y', { duration: 0.35, ease: 'power3.out' }); + btn.addEventListener('pointermove', function (e) { + var r = btn.getBoundingClientRect(); + setX((e.clientX - (r.left + r.width / 2)) * 0.22); + setY((e.clientY - (r.top + r.height / 2)) * 0.3); + }); + btn.addEventListener('pointerleave', function () { + gsap.to(btn, { x: 0, y: 0, duration: 0.6, ease: 'elastic.out(1, 0.45)' }); + }); + }); + } +})(); diff --git a/index.html b/index.html index 1349d33..e4d427d 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,3 @@ --- layout: home -bootstrap: true ---