What we do Our work Team Mission For clients Students

Concept variant · build notes

V15 — Vantage

A full-viewport photographic hero with a bottom-only blur, liquid-glass controls, and a browsable pool of Rotterdam and Delft backgrounds. Vanilla HTML, CSS, and JavaScript—no framework, no build step, no bundler. Every page opens correctly as a plain file from disk.

The one thing to read on this page: every background image in this variant is AI-generated. None of them is a photograph, and none depicts a real 180 Degrees Consulting project, client, team, or event. The image ledger below records exactly what was generated and how.

Specification

Pagesindex.html, for-clients.html, mission.html, for-students.html, guide.html, styles.css
StackVanilla HTML/CSS/JS. Zero dependencies at runtime, zero build step. Deploys as static files; opens over file://.
TypeInter 300–700, loaded from Google Fonts.
ScriptsTwo files, hero-controller.js and hero-depth.js (about 24 KB each, unminified), both loaded defer. No library, no bundler.
Per-page weightOne hero photo plus CSS, JS, and two brand marks. A page fetches exactly one tier from the srcset: 7–49 KB on a phone, 26–151 KB at 1600, 48–283 KB on a 2× display. The spread is the subject, not the encoder — the Markthal facade is thousands of small windows and costs roughly five times the fog frame. Every page lands inside the 1 MB budget.
Nav differentiatorScroll-anchor links are plain text; own-page links are glass pills carrying a green status dot, which fills and gains a halo on the current page.

Image ledger — every background is AI-generated

V14 Confluence shipped real documentary footage and credited each filmmaker. This variant has no photographer to credit, because there is no photograph. All ten backgrounds were produced with a generative image tool, and every one of them has since been re-rendered a second time by a different generative model to raise its resolution. They are rendered scenes of real Rotterdam and Delft landmarks, not captures of them.

That distinction matters in three specific ways, and the site is built so a visitor can find all three:

  • They are not evidence. No image shows an 180DC project, client, engagement, team, or event. They are atmosphere for a concept layout.
  • They are not accurate records. The landmark geometry is close but generated. Details of the Erasmusbrug's cabling, the Oostpoort's brickwork, and the Delft canal facades will not survive comparison with the real structures.
  • They are labelled in public. Every page footer carries “backgrounds are AI-generated” linking here. This page is not the only place the disclosure lives.

Generation record

Shipped fileSource renderSubject as promptedUsed as
erasmusbrug-blue-hour.webperasmusbrug-hero-set/01-blue-hour.pngErasmusbrug at blue-hour dusk, river mist, long-exposure traffic trailsHome hero · pool 1/10
erasmusbrug-golden-hour.webperasmusbrug-hero-set/02-golden-hour-drama.pngBridge backlit by a low sun, pylon in silhouette against amberpool 2/10
erasmusbrug-storm-light.webperasmusbrug-hero-set/03-storm-light.pngBreaking-storm sky, one shaft of light on bridge and riverpool 3/10
erasmusbrug-fog.webperasmusbrug-hero-set/04-minimalist-fog.pngDense fog, only the upper pylon and a few cables emergingpool 4/10
erasmusbrug-night.webperasmusbrug-hero-set/05-night-energy.pngNight, architectural lighting, traffic trails across the deckpool 5/10
erasmusbrug-pylon-night.webprotterdam-delft-night-set/rotterdam-02-erasmusbrug-pylon-night.pngLow oblique view up the pylon, cable fan against near-black skyFor clients hero · pool 6/10
delft-canal-night.webprotterdam-delft-night-set/delft-02-canal-night.pngNarrow Delft canal at night, brick facades, one warm streetlampMission hero · pool 7/10
delft-oostpoort-night.webprotterdam-delft-night-set/delft-01-oostpoort-night.pngOostpoort gate and Oostpoortbrug at night, warm window lightStudents hero · pool 8/10
markthal-evening.webprotterdam-delft-landmark-set/03-rotterdam-markthal-evening.pngMarkthal at dusk, arched grey shell over an illuminated glass frontpool 9/10
delft-nieuwe-kerk.webprotterdam-delft-landmark-set/06-delft-markt-nieuwe-kerk-golden-hour.pngDelft Markt at golden hour, Nieuwe Kerk tower over gabled housespool 10/10

Provenance and processing

  • Origin. Three coordinated sets generated at 1536 × 1024 PNG. The full prompt text for every image is version-controlled at _brand/erasmusbrug-hero-set/PROMPTS.md, _brand/rotterdam-delft-night-set/PROMPTS.md, and _brand/rotterdam-delft-landmark-set/PROMPTS.md. The renders themselves are kept out of version control—they run to roughly 105 MB against 3.5 MB of shipped WebP—so the prompts, not the PNGs, are the reproducible record.
  • Second pass: resolution. 1536 × 1024 was the ceiling of the original renders, and the 2400 px tier below was a lanczos upscale of it—no detail the source did not already have. On a 2× display that meant roughly 1536 px of real detail stretched to 2880. Every frame was therefore re-rendered image-to-image at 2528 × 1696 with Nano Banana Pro (tools/regen-v15.mjs), instructed to hold composition, framing, colour grade, and the calm lower third while adding only genuine detail. The originals remain in _brand/ as the provenance record. These are re-renders, not upscales: fine geometry such as cable spacing shifts slightly between the two versions, which is why depth maps were regenerated and hero contrast re-measured on every frame afterwards.
  • Constraints applied at generation. Every prompt excluded people, readable text, typography, logos, watermarks, and brand marks—so no invented signage or fake company presence reaches the page.
  • Processing. Resized to 2400 px, 1600 px, and 900 px wide and converted to WebP with tools/prep-v15.mjs. No cropping, retouching, or colour grading was applied. The fog frame carries extra quality headroom because near-white gradients band under aggressive compression.
  • Not shipped. The source PNGs stay outside the variant directory, exactly as V13 and V14 keep their raw sources out of the deployable folder.

What this variant does not claim

No case study, client name, outcome figure, testimonial, team portrait, or partnership appears anywhere in this variant. Those spaces are visibly reserved. An AI-generated skyline is decoration and is labelled as such; an AI-generated client result would be a fabrication, and none exists here.

Motion is an upgrade, never the foundation

The hero is a real <img> in the markup with src, srcset, and a written alt. Turn JavaScript off and the page is complete: the photo renders correctly cropped, the copy is legible, and every link works. The controller only adds behaviour on top.

A

Two transforms, two elements

The Ken Burns pan runs as a CSS animation on the <img>. The scroll parallax is a JavaScript-written transform on its wrapper. Separating them means the two never fight over one transform property, and neither ever animates a property that triggers layout.

B

Nothing runs unseen

An IntersectionObserver pauses the pan and stops the parallax loop the moment the hero leaves the viewport; visibilitychange does the same for a hidden tab. The parallax is rAF-coalesced, reads a cached hero height rather than measuring per frame, and writes exactly one transform.

C

Preferences win

prefers-reduced-motion: reduce removes the pan, the parallax, the entrance stagger, and the crossfade, leaving a static correctly-cropped frame. It is re-evaluated when the preference changes at runtime, not only at load.

The bottom-only blur

A single element over the photo applies backdrop-filter: blur(26px) and masks itself with linear-gradient(to top, black 0%, transparent 45%), so the blur is strong along the bottom edge and gone by mid-frame. There is no dark gradient over the photo—the effect is blur alone, which keeps the sky and skyline at full brightness while the text below sits on softened detail.

The blur element is scoped inside the hero rather than fixed to the viewport. That is a deliberate change from the original specification: a viewport-fixed blur keeps compositing the whole page as you scroll and, with JavaScript disabled, would smear the light content sections underneath it. Scoped to a hero with overflow: hidden, it cannot leak past the hero in any code path.

The depth hero — how a still photograph moves

A pan and zoom moves the frame; it does not move the scene. To make the photograph itself feel alive, each image ships with a depth map — a small greyscale image where white is near and black is far — generated offline by Depth Anything V2 (tools/depth-v15.mjs, running in Node through ONNX; no PyTorch, no GPU, and nothing added to the shipped page). hero-depth.js then renders the photo through that depth map in plain WebGL. No Three.js, no framework, no build step.

Four things move, and only the first is a camera move:

  • Parallax. Each pixel is displaced by its depth, so the pylon, the bridge deck, and the far skyline slide against each other instead of moving as one flat plane. This is what reads as three-dimensional.
  • Drift. A slow autonomous orbit, so the hero is alive even when the visitor does nothing at all — the difference between a photograph and a held shot.
  • Water. A ripple masked by the depth map to the near, low part of the frame, so only the river or canal surface shimmers and the architecture stays rigid.
  • Breathing. A very gentle zoom that replaces the CSS Ken Burns.

Measured: 85.9% of hero pixels change over a 3.5-second window with no user input, against 0.0% under prefers-reduced-motion.

What the depth hero costs, and what it gives back

A depth map is about 2 KB — the ten together come to about 30 KB, and a page loads one. There is no video file, no decode, and no extra bandwidth worth naming.

It also makes the photograph sharper, which is the opposite of what adding an effect usually does. The CSS version had to give the parallax somewhere to travel, so .hero__media was 15% oversized and the pan zoomed to 1.13 — together magnifying the image about 1.3× before the display scaled it at all. Measured against the 1600px asset, that meant a 1440×900 screen was already showing a 1.24× upscale, a 1920×1080 screen 1.49×, and a 2× display 2.48×. The shader displaces coordinates inside the texture, so it needs neither the overscan nor the zoom, and the photo is sampled close to 1:1. A 2400px tier was added to srcset for high-DPI screens on top of that.

It does not run from a bare file, by design of the browser. Opened over file://, Chromium treats every local image as cross-origin data and refuses to upload it as a WebGL texture. The layer detects this, never touches the DOM, and the page stays on the CSS pan — which is why the <img> is still the real content. Served over HTTP, as it deploys, the depth hero runs. Nothing is lost locally except the effect.

The layer removes itself, restoring the CSS pan, on any of: no WebGL, a failed shader compile or link, a lost context, a missing depth map, a texture that will not upload, or its own measured frame time — the loop times itself and stands down if it cannot hold a smooth rate, so a weak GPU gets the simple hero rather than a stuttering rich one. Crossfade frames are excluded from that judgement, since sampling two frames at once is the heaviest moment and lasts only 620 ms.

Browsing the image pool

The previous/next city buttons walk all ten backgrounds without a page reload. Each page starts on its own hero and shares one pool. The incoming layer's src is set, then img.decode() is awaited before the crossfade begins—so no visitor sees a half-painted photo—and the two neighbouring frames are prefetched during idle time so a second click is instant. Rapid clicks are handled with a token: a superseded swap abandons quietly instead of racing the newer one to the screen.

Same facts, no invented proof

Content is at parity with the other recent variants and carries the same verified facts: founded 2019 in Delft, the first multi-city branch of 180 Degrees Consulting, TU Delft × Erasmus University Rotterdam, six service areas, two roles (Consultant and Team Leader), and two consulting cycles a year running October–January and March–June.

Visible placeholder register

  • Case studies — reserved on Home and For clients, awaiting client consent.
  • Team portraits — reserved on Home; roster still pending confirmation against Monday.com.
  • Member testimonials — reserved on For students, to be sourced with consent.
  • Partner logos — reserved on Home, awaiting first partnerships.
  • Intake form — UI only, visibly marked as not wired to a backend.
  • Events calendar — reserved on For students.
  • Impact numbers — reserved on Mission, for the first Impact Report.
  • Member area — the profile control opens a panel that states plainly that no accounts exist.

The navbar's search control is the one right-hand affordance that is genuinely functional: it searches a real index of this site's pages and sections. It does not pretend to search content that has not been written.

Build record

Like every material build in this repository, V15 earned three rounds of self-critique before release. Findings and fixes are recorded below.

2026-07-27 — V15 Vantage, initial build

Ten AI-generated backgrounds compressed to WebP; six pages built; hero controller written against the mount/pause/reduced-motion discipline established in v14-confluence/media-controller.js.

Each round was run as a scripted browser audit rather than a read-through, so every claim below was measured. The scripts are checked in at tools/audit-v15.mjs, tools/audit-v15-r2.mjs, tools/audit-v15-r3.mjs, and tools/audit-v15-hero-contrast.mjs.

Critique round 1 — correctness of the effects

  • Fixed — horizontal overflow between 1024 px and 1180 px. The desktop navbar switched on at the lg breakpoint, but the brand lockup, six links, labelled search pill, and profile button measured 1039 px against a 1024 px viewport, so every page scrolled sideways in that band. The search pill now collapses to its icon there and the link row is allowed to shrink.
  • Verified: no console or page errors on any page; the bottom blur resolves both backdrop-filter and its mask; two hero layers exist; the Ken Burns keyframes are bound; the swap decodes before painting; the counter wraps in both directions; six rapid clicks land on the correct frame with the label matching the painted photo; reduced motion and JavaScript-disabled paths both render a static legible hero; no page exceeds 165 KB on initial load.

Critique round 2 — interaction polish and accessibility

  • Fixed — Search and Profile were missing from the mobile menu. The rule hiding them in the header below 1024 px also matched the copies repeated inside the menu, so the narrow-viewport layout lost both controls the specification asks for. The rule is now scoped to .site-header.
  • Fixed — closed panels kept compositing. The search and member panels declared backdrop-filter while hidden, which left a blurred rectangle being composited over the page at all times. The filter now attaches only in the open state.
  • Fixed — the placeholder chip inside the member panel measured 3.16:1. It inherited the light-theme placeholder palette while sitting on the panel's dark surface. It now uses the dark treatment, as the dark sections already did.
  • Fixed — touch targets were 38–40 px. Raised to 44 px under (pointer: coarse), matching the size standard the rest of the repository uses.
  • Fixed — the mobile menu's tools row appeared without an entrance while the links above it animated in. It now joins the stagger.
  • Verified by measurement: zero layout passes and 3.5 ms of style recalculation during a 1.6 second scroll with the pan and parallax both live — confirming the parallax is compositor-only. The hero pauses and releases will-change off-screen, opening the mobile menu produces a layout-shift score of 0, the scroll lock releases on close, focus returns to the toggle, every control shows a ring under real Tab traversal, and heading order is unbroken on all five pages.

Critique round 3 — honesty, parity, and release polish

  • Fixed — two facts were lost with V14's hero-facts block. This variant's bottom-aligned hero replaced that panel, and with it went “uniquely affordable” on Home and the October–January / March–June cycle dates on For students. Both are restored: the first into the Home metadata row, the second into the applications paragraph.
  • Fixed — the fog frame broke hero legibility. Sampling the real rendered pixels behind every hero element across all ten frames showed white copy collapsing on the one near-white render: the secondary CTA, the cycling buttons, and the city label were effectively invisible. Since darkening the photo is ruled out, the pool entry now carries a tone flag that flips the hero to dark type and deepens the control tint for that frame. The worst contrast anywhere in the pool is now 5.62:1, against 4.5:1 required.
  • Verified: nothing from the “don't use” column of the brand terminology table appears in any page's copy — the fee-related phrasings, the superseded project-leadership job title, the disallowed synonym for “branches”, the run-together spelling of the organisation name, and the incorrect email domain were each checked by pattern. (The audit script scans this page too, which is why the rules are described here rather than quoted.) Every required fact present per page; six service areas on both pages that list them; the AI-generation disclosure in all five footers, each linking to the ledger; every placeholder in place; no dead links, anchors, or images; the pool wired to all ten frames on every page with every width on disk; and no framework, bundler, or build step anywhere in the variant.

Release measurement — Lighthouse

Run locally against a static server, desktop preset, after the three critique rounds. Three further defects surfaced here and were fixed before these numbers were recorded:

  • Fixed — the footer's AI-disclosure link was unreadable. It inherited the body link green (#4A7322) against the near-black footer. The footer now uses a light green.
  • Fixed — the brand link's accessible name did not contain its visible text. The two spans rendered as “180 Degrees ConsultingDelft–Rotterdam” with no separator, so it failed label-content-name-mismatch against the aria-label. A space between the spans fixes the name without changing the flex layout.
  • Fixed — the navbar failed contrast on the guide. The header's translucent dark glass was tuned against the photo heroes; the guide has no hero, so white nav text sat on a near-white body at 55% opacity. Raised to 80%, which also covers the moment any page's header crosses into its light sections.
  • Fixed — the webfont was fetched by @import inside the stylesheet, which chains HTML → CSS → font CSS → font files. It is now a <link> with preconnect in each page head.
PagePerformanceAccessibilityBest practicesSEOTransfer
index.html91100100100186 KB
for-clients.html93100100100202 KB
mission.html98100100100192 KB
for-students.html99100100100175 KB
guide.html100100100100137 KB

Performance is the only figure that moves between runs; Accessibility, Best Practices, and SEO held at 100 on every page in every run. Home is the slowest and most variable — repeated warm runs settle at 91, while one cold browser launch produced a 48 — and the guide has been observed between 86 and 100. The table records one representative warm run rather than a best-of, and the observed spread is stated here instead of hidden, following the convention V14 set. Transfer figures are Lighthouse's total byte weight, which includes the webfont; the browser-measured initial page load is 125 KB for Home.

Two deliberate deviations from the written specification

Both are recorded here rather than left implicit:

  1. The bottom blur is scoped to the hero, not fixed to the viewport. A viewport-fixed blur composites the whole page on every scroll frame and, with JavaScript disabled, smears the light content sections below the hero. Scoped inside a hero with overflow: hidden it is visually identical where it matters and cannot leak in any code path.
  2. Hero-scoped controls carry a tint the reusable class does not. .liquid-glass keeps its specified rgba(255,255,255,0.01) fill and is used unmodified in the navbar and panels, where it sits on dark chrome. Controls placed directly on the photography would be invisible at that value on light frames, so they add a neutral tint on top. The photograph itself is never darkened.

Extending this variant

  • Replacing the backgrounds with real photography is the intended upgrade path. Swap the files in img/, update the POOL array at the top of hero-controller.js with new labels and alt text, then rewrite this ledger to credit the photographer and licence in the shape V14's MEDIA-CREDITS.md uses. Delete the AI disclosure only once no generated image remains.
  • Adding a page means copying any inner page, changing data-hero-start to the pool index you want it to open on, and adding an entry to the INDEX array so site search can find it.
  • The pool size is not fixed. POOL drives the counter, the arrows, and the prefetch; adding a ninth image requires no other change.
  • Do not reuse the generated images as evidence in a deck, report, or proposal. They are layout atmosphere with a disclosure attached, and the disclosure does not travel with the file.

← Back to the site