Burn Studio teardown

Read 2026-10-21 (2026-10-22 UTC) from https://burnstudio.co/, https://burnstudio.co/work and https://burnstudio.co/work/antropico, plus the site's own CSS and JS. Serves inspiration rows 13 (the landing) and 14 (the work grid, the hover pill, the draggable menu, the phone bar) in ../../INSPIRATION.md; deepens "The name as the grid" and "The grid, and a bar that speaks for the canvas" in ../../bar-stage-drawers.md.

Checked by a verifier on 2026-10-22: VERIFY.md lists what was checked, what was corrected in place below and what stays unconfirmed.

Short version: a hand-written jQuery site on Kirby. One number, the pitch of a cross-marked grid, is computed from the viewport width; the logo, every thumbnail and every gap are whole multiples of it, so things that look scattered are on the grid to the pixel. The menu is a row of grey pills that grows a title pill on hover, can be dragged anywhere, and becomes the video player's bar on a project page. Everything on the landing, /work and the bar moves with plain CSS transitions of 300 ms ease, except the menu's 1 s glide; the only scripted motion is the homepage slideshows (Swiper, 800 ms slides on a desktop) and the project page's credit ticker (Marquee3000).

1. What the owner liked

Quoted from INSPIRATION.md (fuller versions of the row 14 quotes are in bar-stage-drawers.md).

2. The stack

Layer Finding Evidence
CMS Kirby (PHP flat-file CMS) data-kirby-url="https://burnstudio.co" on menu items; media under /media/pages/<page>/<hash>-<timestamp>/ (Kirby's media folder pattern: landing stills under /media/pages/home/…, thumbnails under /media/pages/work/<slug>/…) with Kirby thumb names (-300x.jpg, -640x.webp to -1920x.webp); x-powered-by: PHP/8.1.34
Server, hosting Apache, PHP 8.1.34, hosted by GoDaddy (its Host Europe network, Germany); no CDN headers on the HTML; DNS on GoDaddy response headers (server: Apache, x-powered-by: PHP/8.1.34, no cf-ray, x-cache or age); NS ns17/ns18.domaincontrol.com; A 92.205.148.223 with no reverse name; RIPE: netname DE-GODADDY-20071017, org Host Europe GmbH, route 92.205.144.0/21, origin AS21499
Page routing Custom AJAX page swap, no router library assets/js/ajax.js: a[data-ajax] links fetch /ajax/null/main/<slug> (JSON {status, content, dataPage}), fade <main>, replace its HTML, history.pushState with the whole main HTML in the state
Framework None; no bundler; six plain scripts assets/js/helpers.js, observerprojects.js, clock.js, main.js, ajax.js, player.js (globals, no modules); probe globals: jQuery, $ only; no generator meta
DOM and drag jQuery 3.6.0, jQuery UI 1.14.1 (used for draggable only) https://code.jquery.com/jquery-3.6.0.min.js, https://code.jquery.com/ui/1.14.1/jquery-ui.min.js (SRI on the jQuery UI tag only); runtime $.fn.jquery 3.6.0, $.ui.version 1.14.1
Sliders Swiper 11 (floating major; jsDelivr served 11.2.10 on 2026-10-22), loaded on every page, used on the homepage only: two vertical slideshows set to loop: true, though Swiper warns that there are too few slides for loop mode https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js (banner "Swiper 11.2.10"); console "Swiper Loop Warning"; initHomepageSwipers() in helpers.js: #landscape-swiper 14 slides, #portrait-swiper 9
Ticker Marquee3000 1.1.1 for the project page's credit line https://cdn.jsdelivr.net/npm/marquee3000@1.1.1/marquee3k.min.js; Marquee3k.init({selector: 'marquee-project'}); data-speed=".25"
Animation CSS transitions only: 23 style rules declare a transition (a grep of the source counted 28 lines), no @keyframes, no prefers-reduced-motion rule, no animation library CSSOM walk through the 14 imported stylesheets (verifier) and a grep of them; tokens --transition-time: .3s, --transition-effect: ease; probe found no GSAP, Lenis, Motion, Lottie or canvas
Scroll Native scrolling; scroll bars hidden (Blink and WebKit only) ::-webkit-scrollbar { display: none; width: 0 } in commons/globals.css; probe scrollBehavior: auto, no smooth-scroll global
3D None probe: no canvas, no WebGL
Fonts "Camera Plain" regular and medium (ABC Camera Plain, served from the foundry's trial files; the Medium file is declared as font-weight: bold) for text; "Politics" book for the small caps-style labels commons/fonts.css: ABCCameraPlain-Regular-Trial.woff2, ABCCameraPlain-Medium-Trial.woff2, Politics-Book.woff2 under /assets/fonts/; probe fonts list
Video Native <video>: thumbnails lazy-played by an IntersectionObserver, hover previews preload="none", project films with a custom player bar lazyVideos() in helpers.js; player.js; 15 videos on home, 30 on /work, 1 on a project
Analytics Google Analytics 4 through gtag https://www.googletagmanager.com/gtag/js?id=G-QE723JP8VM; cookies _ga, _ga_QE723JP8VM
Stored state One cookie, color-mode, 30 days; nothing in localStorage or sessionStorage setCookie() in helpers.js; eval after drag and mode switch

Note on the probe: its counts of keyframes, transition rules and scrollbar rules read 0 because every rule arrives through @import from styles.css, which the probe does not follow. The source greps above are the evidence for those.

3. Component playbooks

Measurements are computed values at 1440 × 900 unless a phone (390 × 844) is named. Source excerpts are in sources/; per-run outcomes are in steps/*.meta.json.

3.1 The landing: BURN spelled in eleven blocks

What it is. A white layer over the whole viewport (#landing-container, #fff, fixed, z-index 110) above the site's #F8F8F8. A field of small grey crosses on a 59.25 px pitch. In the centre, a 473 × 176.75 px logo made of eleven grey rounded blocks (#afafaf80, which reads #d7d7d7 on white; radius 5 px) on an 8 × 3 grid of 58.25 px cells with 1 px gaps, so the logo's pitch equals the crosses' pitch. A small bold "TM" sits at the right end of row 1, just above the n; "A Multi-Platform Creative Company" (bold, 16 px, #AFAFAF80) sits one cell below. Nothing else on the page.

The letter map (column, row of the 8 × 3 grid; the counters of the letters are the 1 px gaps and one empty cell):

Block Cells Letter part
1 col 1, rows 1 to 2 b, ascender
2 col 1, row 3 b, foot
3 col 2, rows 2 to 3 b, bowl
4, 5 cols 3 and 4, row 2 u, two stems
6 cols 3 to 4, row 3 u, base
7 col 5, rows 2 to 3 r, stem
8 col 6, row 2 r, arm (col 6 row 3 stays empty)
9 cols 7 to 8, row 2 n, shoulder
10, 11 cols 7 and 8, row 3 n, two legs
text cols 2 to 8, row 1 holds the TM

Measured rects: block 1 at (483.5, 361.6) 58.25 × 117.5; block 6 at (602, 480.1) 117.5 × 58.25; block 9 at (839, 420.9) 117.5 × 58.25 (all twelve in steps/01-landing-rest-hover-click.meta.json, block-rects).

DOM.

<div id="landing-container">
  <div id="background-container"><svg width="100%" height="100%"><defs>
    <pattern id="cross-pattern" class="pattern-script" width="50" height="50" patternUnits="userSpaceOnUse">
      <polygon class="crosses-svg" points="4,1.8 2.2,1.8 2.2,0 1.8,0 1.8,1.8 0,1.8 0,2.2 1.8,2.2 1.8,4 2.2,4 2.2,2.2 4,2.2"/>
    </pattern></defs><rect width="100%" height="100%" fill="url(#cross-pattern)"/></svg></div>
  <div id="logo-landing">
    <div class="logo">
      <div class="logo-block logo-text"><p class="logo-tm bold">TM</p></div>
      <div class="logo-block" data-index="1"><figure class="figure-media"><picture class="media-container">
        <img src="…/burn_hoodie_2-300x.jpg" alt="" class="media portrait"></picture></figure></div>
      <!-- data-index 2 to 11, same markup, each with its own 300 px still -->
    </div>
    <p class="bold small">A Multi-Platform Creative Company</p>
  </div>
</div>

Styles that matter (commons/logo.css, pages/home.css): .logo { display: grid; grid-template-columns: repeat(8, 1fr); gap: 1px; width: calc(var(--gap) * 8 + 7px); height: calc(var(--gap) * 3 + 2px) }; each block places itself with grid-row and grid-column by data-index; .logo-block figure { opacity: 0; transition: opacity .3s ease }, shown by :hover or .active; #logo-landing { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) }; html.hide-landing #landing-container { opacity: 0; transform: translateY(5px); pointer-events: none } with transition: opacity .3s ease, transform .3s ease.

How the crosses meet the logo. The cross pattern's tile is set to the pitch by grid(); its container has 7 px padding, so cross centres fall at 9 px + k × pitch from the left. On the landing the SVG is made pitch × 901 + 4 px tall (--landing-grid-height, 53,388.25 px) and flex-centred, so an odd number of tiles straddles the viewport's middle and a row of crosses falls half a pitch above and below centre. Measured: every block edge sits 0.5 px from a cross centre on both axes.

Behaviour (main.js, inside $(window).on('load')): - Idle: startRandomHighlight() runs setInterval(…, 2000): pick a random block other than the current one, remove .active from all, add it to the new one. Both opacity transitions run at once, so it is a 300 ms cross-fade. Observed in the rest burst (no input): nothing lit for the first frame, then block 6, then 8 (block 6 still fading in the same frame), then 3 (landing-rest-logo-strip.png); block 7 was lit by the time of the eval right after the burst (active-after-burst). - First hover on any block: clears the interval for good and removes .active; from then on only :hover lights a block (landing-hover-block6.png, landing-hover-block1.png; active-after-hover-2500ms stayed empty). - Click on any block: html.hide-landing, the layer fades out and drops 5 px over 300 ms, and the homepage that was already rendered behind it is revealed (landing-after-click.png). The blocks hold no links: the click does not open the block's project (a correction to row 13's "opens on a click"; bar-stage-drawers.md has it right). - Behind the landing: the black BURN logo top left, the two-line claim, and two vertical Swiper slideshows (landscape stills on the left two thirds, portrait stills on the right) whose slides are .project-hover links, so the menu's title pill reacts to them too (landing-after-click.png shows "55. C Bluff Bounce | Bluff Bounce | Matheo Fernandes" because the pointer landed on a slide).

Assets. Eleven 300 px JPEG stills named after talent and campaigns (burn_kevin-durant_kd-300x.jpg, burn_coi-leray-300x.jpg, burn_brady_gun-control-300x.jpg and so on); the cross is a 4 × 4 unit plus drawn as one polygon with 0.4-unit arms, fill: currentColor (#AFAFAF, dark #505050).

Phone. The landing is display: none at 820 px and narrower (@media only screen and (max-width: 820px)). The phone home is one full-bleed vertical Swiper (below 821 px initHomepageSwipers() moves the landscape slides into the portrait slider, so it holds both sets), with the headline in large type and the logo across the full width at the foot, both white under mix-blend-mode: difference (the "translucent blocks" in ../../burnstudio/mobile-top.png are this blend), and the foot bar naming the active slide ("64. C.S Bored Room Copier | Circle | Giraffe Sisters", m-home.png).

Accessibility and motion. Blocks are divs with click handlers: not focusable, no role, no keyboard path past the landing; all stills alt="". The idle cycle never checks prefers-reduced-motion (there is no such query anywhere in the CSS); it is only an opacity cross-fade every 2 s, but it runs until the first hover, including in a background tab.

Rebuild for ETS27 (library-agnostic): 1. Compute the stage pitch once (see 3.2) and draw the cells as a CSS grid whose track size is the pitch minus 1 px with a 1 px gap, so cells sit on the background marks. 2. Describe the word as data: each cell gets {row, col, rowSpan, colSpan, item} where item is a speaker, session or theme. "27", "ETS27" or "free your mind" are just different maps. A small script can turn a pixel font into the map. 3. Each cell is a <button> with an accessible name (the speaker's name) holding a hidden layer (the marker sketch or photo) at opacity 0. 4. Idle: a 2 s timer that moves an is-lit class to a random other cell; stop it on the first pointerenter, on focus, on visibilitychange to hidden, and never start it under prefers-reduced-motion: reduce. 5. Reveal: opacity 0 to 1 over 300 ms ease on hover, focus and is-lit. 6. Click: unlike Burn, open the cell's drawer (the ETS27 drawer model), or hand the stage to the bar. - Libraries that make it easy: none needed (CSS grid plus a timer). GSAP or Motion only if the reveal becomes a drawn stroke (marker look: SVG stroke-dashoffset).

Gotchas. - The landing ignores dark mode for its own background: #landing-container is hard-coded #fff, so a dark-mode visitor gets a white page with darker blocks (landing-dark.png), then a black site behind it (home-behind-landing-dark.png). - The mode cookie is applied in the load handler, after every image has loaded, so a dark-mode visitor sees the light theme first. - grid() assigns an undeclared gap, which leaks as window.gap.

3.2 The grid system on /work

The math (grid() in helpers.js, run on load and on every resize):

var divisor = windowWidth < 820 ? 8 : 24;
var cellSize = divisor + 1;
gap = ((windowWidth - (4 * cellSize) - 14) / divisor) + 4;      // the pitch
$(".pattern-script").attr("width", gap).attr("height", gap);   // cross tile = pitch
body.style.setProperty("--gap", gap - 1 + "px");                // cell = pitch - 1

Answer: exact grid math. The expression simplifies to pitch = (W − 18) / d, with d = 24 cells at 820 px and wider and d = 8 below. --gap = pitch − 1. At 1440: pitch 59.25, --gap 58.25 px (measured; grid-vars). At 390: pitch 46.5, --gap 45.5 px (measured). Other widths: 1280 gives 52.58; 1920 gives 79.25; 820 gives 33.42; 819 jumps to 100.13 (the 8-cell phone grid, single column).

The projects grid (pages/work.css): display: grid; grid-template-columns: repeat(8, 1fr); column-gap: 2px; row-gap: calc(var(--gap) + 3px); align-items: center, inside main { padding: 1em 1em var(--gap) } (1em = 10 px, root font 62.5 %). Measured: eight 175.75 px columns, 2 px gutters, 61.25 px row gaps. The column pitch, 175.75 + 2 = 177.75, is exactly three cross pitches at any width: (W − 34)/8 + 2 = (W − 18)/8 = 3 × (W − 18)/24. With the 10 px inset and the crosses centred at 9 px, every 2 px gutter has a cross in its middle.

Answer: thumbnail size classes. Every thumbnail is one project column wide (three cells, 175.75 px at 1440). Only the height varies, by the image's class:

Class (on .media) Height rule At 1440 Cells tall Count on /work
landscape (still) calc(var(--gap) * 2) 116.5 px 2 (minus 2 px) 59
none (video, the "C.S" social cuts) calc(var(--gap) * 4 + 2px) 235 px 4 (minus 2 px) 5
portrait (still) calc(var(--gap) * 6 + 4px) 353.5 px 6 (minus 2 px) 4

(This corrects "one, two or three cells wide, sometimes tall" in bar-stage-drawers.md.) Every height is an even number of pitches minus 2 px, so when a row holds a taller item, align-items: center offsets the shorter ones by whole pitches and they stay on the grid. Row pitches come out as 3, 5 or 7 pitches; the measured row tracks were 116.5, 353.5, 235, 116.5, 353.5, … px.

Captions. .project-code-container hangs under the image (position: absolute; bottom: -1px; transform: translate(-50%, 100%); height: calc(var(--gap) - 1px)), so the caption is one cell tall (--gap − 1, 57.25 px at 1440) and starts 1 px under the image, inside the 61.25 px row gap: "N 58 TYPE C" with the labels in Politics 9 px uppercase and the values in Camera Plain 14 px, colour #00000080. On the phone the caption adds "DIRECTOR …".

The "random spots". Not random: a fixed 23-item cycle of seven column pins, on top of CSS grid's default sparse auto-placement:

.project.work-project:nth-child(23n + 1), .project.work-project:nth-child(23n + 13) { grid-column: 2; }
.project.work-project:nth-child(23n + 5)  { grid-column: 7; }
.project.work-project:nth-child(23n + 8)  { grid-column: 3; }
.project.work-project:nth-child(23n + 12) { grid-column: 8; }
.project.work-project:nth-child(23n + 15), .project.work-project:nth-child(23n + 22) { grid-column: 6; }

When a pinned column lies to the right of the placement cursor, the cells in between stay empty; when it lies to the left, the item wraps to the next row and leaves the rest of the row empty. Measured rows (item-rects): row 1 holds items 1 to 6 in columns 2, 3, 4, 5, 7, 8; row 2 items 7 to 12 in 1, 3, 4, 5, 6, 8; row 3 items 13 to 17 in 2, 3, 6, 7, 8; row 4 items 18 to 23 in 1, 2, 3, 4, 6, 7. The whole page is twelve rows, 3,088 px tall (work-full.png). Filtering (All, Commercial, Cinematic Social) hides items with display: none and sorts hidden ones to the end, so the pins apply to the visible set and the scatter re-forms.

Phone. One column (grid-template-columns: 1fr, every item grid-column: 1 !important): landscape stills 370 × 230.5 (5 pitches minus 2 px), portraits 276 × 414.5 centred, videos 370 × 184 (the desktop :not(.landscape):not(.portrait) rule outranks the phone rule). The cross pitch is 46.5 px and the crosses read as small "+" marks (m-work-00-top.png).

Rebuild for ETS27. 1. One stage variable: --pitch: calc((100vw - 18px) / var(--cells)) with --cells: 24 and 8 under 820 px. This is pure CSS once scroll bars are hidden, which ETS27 wants anyway; otherwise compute it from document.documentElement.clientWidth. Recompute on resize. 2. Background marks: an SVG pattern (Burn's polygon), or background-image of one inline-SVG cross with background-size: var(--pitch) var(--pitch) and background-position: 7px 7px. With the marker look, draw the cross as a tiny marker tick. 3. Blocks the mind places declare a size from a short list in pitches: for example 3 × 2 (a speaker card), 3 × 4 (a session), 3 × 6 (a portrait), 6 × 4 (a map), so heights are calc(var(--pitch) * k - 2px) with k even, inside a CSS grid of 8 tracks with 2 px gaps and 10 px insets. 4. Placement: let the model choose order and size; let the layout choose the column from a pin cycle like Burn's or a seeded random, never a free x/y. That keeps "scattered but never off-grid" a property of the layout, not something the model has to get right. 5. Libraries: none. CSS grid does the packing; grid-auto-flow: row (sparse) is what makes the holes.

Gotchas. The pitch depends on viewport width only, so a tall phone in landscape gets the 24-cell desktop grid at 820 px and wider. The CSS breakpoints use max-width: 820px while grid() switches at < 820, so at exactly 820 px the one-column phone layout runs on the 24-cell pitch (33.42 px). Thumbnails sit 1 px off the crosses by design; anything that adds a border or outline breaks the alignment. Full-page screenshots here show no video thumbnails (see "Not confirmed").

3.3 Hover extends the menu: the title pill

What it is. The menu is a row of pills 31.4 px tall, radius 3 px, fill #7c7c7c (dark mode #5b5b5b), text #afafaf, 1 px apart: BURN 76.0 px wide at x 35, "∷ Work" 71.5, "∷ All" 55.5, "List" 61.9 (y 156.4). The fifth pill, #nav-title-container, rests at width 0 just right of "List" (x 303.9). On hover it grows to the width of its text, 441.7 × 31.4 px for "06. C Ban Assault Weapons | Brady | Greg Hardes & Aaron Chapman" (work-hover.png), with the code in Politics 9 px uppercase and the title in Camera Plain 14 px.

DOM.

<div id="nav-title-container" class="nav-container-animate">
  <div class="nav-element" id="project-name-container">
    <span class="mono uppercase" id="nav-project-code"></span>
    <p class="small" id="nav-project-name"></p>
  </div>
</div>
<!-- each thumbnail: -->
<a href="…/work/ban-assault-weapons" data-code="06. C" data-title="Ban Assault Weapons | Brady | Greg Hardes & Aaron Chapman"
   data-ajax="work/ban-assault-weapons" class="project work-project project-hover">…
  <div class="hover-preview-container hide-on-mobile"><video playsinline muted loop preload="none" class="media">…</video></div>
</a>

Mechanism (main.js, "HOVER PROJECT"): a delegated mouseenter on .project-hover copies data-code and data-title into the pill (and into the player bar's title, used later on the project page), measures the inner #project-name-container, which is position: absolute and so keeps its natural width while the container is 0 wide, writes that width (and the height as max-height) inline on #nav-title-container, adds header.show-title (text opacity 0 to 1) and calls play() on the card's preview video. mouseleave sets the width back to 0, removes show-title and pauses the video. The CSS: .nav-container-animate { width: 0; max-height: 31.4px; overflow: hidden; transition: background-color .3s ease, width .3s ease, max-height .3s ease, margin-right .3s ease, backdrop-filter 1s ease .3s } and .nav-container-animate .nav-element { opacity: 0; transition: opacity .3s ease }. Thumbnails only accept the pointer once the header has shown (html:has(header.show) .project-hover { pointer-events: auto }).

Answer: pill timing and easing. Width and text opacity move together over 300 ms with CSS ease (cubic-bezier(.25, .1, .25, 1)), starting in the frame the pointer enters. Measured with an in-page requestAnimationFrame sampler (pill-width-samples-enter): 0 px at 69 ms, 11.4 at 78, 101.9 at 139, 218.3 at 203 (74 %), 262.9 at 261, 292.5 at 349, final 294.5 px at 394 ms, the text opacity tracking the same fraction (0.04, 0.35, 0.74, 0.89, 0.99, 1). Leaving runs the same curve backwards (241 px at 168 ms to 0 at 460 ms). There is no delay and no debounce. The preview video's container switches to opacity 1 instantly under :hover (no transition) inside @media (hover: hover); 25 of the 68 projects have a preview.

The pill re-targets as the pointer travels. Crossing other cards on the way out re-extends the pill with their titles before it retracts: the unhover burst catches it retracting with "Bored Room C…", the portrait the pointer crossed (work-unhover-pill-header-strip.png, frame 00), and the leave samples show the pill start to close (283.3 px, text 0.96), re-open to the crossed card's narrower title (271.6 px, text back to 1), then close.

Bursts. work-hover-pill-header-strip.png: frame 00 (+370 ms) catches the pill as an empty grey stub, frame 01 (+620 ms) fully out with text. work-unhover-pill-header-strip.png: frame 00 mid-retraction, frame 01 gone.

Phone. No hover and no desktop menu (#header is opacity 0 on non-project pages at 820 px and narrower); the foot bar (3.5) does the job.

Accessibility. Pointer only: no focus handler, no aria-live, so keyboard and screen-reader users never get the title. The pill text is #afafaf on #7c7c7c, about 1.9:1 contrast. Clicked buttons do show Chromium's focus ring (list-open-light.png, work-dark.png) despite outline: … auto 0px.

Rebuild for ETS27 ("the bar speaks for the canvas"): 1. Give the bar a detail slot after its fixed pills. 2. On pointerenter and on focus of any stage block, write the block's label into the slot and open it; on pointerleave and blur, close it after a short grace period (about 80 ms) so crossing the 2 px gutters between blocks does not make it flicker, which Burn's does. 3. Animate the width and opacity together over 300 ms ease. Modern CSS can do it without measuring: interpolate-size: allow-keywords with a width: 0 to width: auto transition (Chromium), or a grid track transition from 0fr to 1fr (all evergreen browsers). Burn's measuring approach works everywhere. 4. Put aria-live="polite" on the slot, and keep the text out of the accessibility tree while closed. 5. Libraries: none needed. Motion's layout animations or GSAP Flip make the width change trivial if the bar is a React or vanilla component.

Gotchas. - The pill state leaks across page swaps: the AJAX swap removes the hovered card without a mouseleave, so after a project and Back the pill is still out with the old title (pt-02-back-settled.png). Clear the slot on every route change. - The pill always grows to the right; with the menu dragged near the right edge it runs off-screen (drag-05-moved-menu-hover-pill.png, "…Jacob Pro"). - The open pill covers the captions under it (work-hover.png).

3.4 The draggable menu

What it is. A 12.5 × 12.5 px four-arrow glyph (an inline SVG with fill: currentColor), 6 px left of the first pill and vertically centred on it (#menu-drag, position: absolute; top: 50%; left: -6px; transform: translate(-100%, -50%); cursor: grab). It is always visible; the whole header appears 600 ms after the load event (a 300 ms fade), which is the "appears" the owner saw. Dragging it moves the entire header (clock line, pills, copyright lines), 303.9 × 130.7 px on /work.

Mechanism. jQuery UI: $("#header").draggable({ handle: "#menu-drag", containment: "window" }), so jQuery UI's defaults apply too (scroll: true, scrollSensitivity: 20, scrollSpeed: 20). The header is position: fixed and jQuery UI writes top and left inline on every move. CSS: #header:not(.ui-draggable-dragging) { transition: top 1s ease, left 1s ease, opacity .3s ease, color .3s ease }, so while dragging there is no transition (1:1 follow, cursor grabbing); after the drop nothing animates until a script sets top/left again.

Measured (steps/03-work-drag-menu.meta.json): start (10, 126.5); pointer held at (640, 520) puts the header at (628, 474.5); released at (900, 700): (888, 654.5), the exact pointer delta. Shots: drag-01 to drag-07, drag-sheet.png.

What persists. The inline position lasts as long as the document. AJAX navigation keeps it, because the header lives outside <main>. Only the project route moves it (to the bottom centre, gliding over 1 s), and returning to /work keeps that bottom position. Nothing is stored: cookies hold only GA and color-mode, local and session storage are empty, and a reload puts the menu back at (10, 126.5) (drag-07-after-reload.png). The owner's "the menu appears on the bottom" probably comes from this: on /work the menu starts top left, and it sits at the bottom after a visit to a project.

Phone. #menu-drag is display: none and the header is pinned to the bottom (top: auto !important; bottom: 0 !important), so there is no dragging on the phone.

Accessibility. The handle is a div: no keyboard move, no label, no snap back.

Rebuild for ETS27 ("a bar you can move"): 1. Use pointer events on a real handle (<button aria-label="Move the bar">) with setPointerCapture. 2. Move the bar with transform: translate3d() rather than top and left. 3. Clamp to the viewport on every move using the bar's current size, and again whenever the bar changes width (the detail slot) or the window resizes. 4. Remember the position in sessionStorage for the visit. 5. Snap back to the dock: double-click the handle, or drop near the bottom centre, with a 300 ms ease glide (Burn's 1 s glide feels slow). 6. Keyboard: arrows move the bar by one grid pitch while the handle has focus; Escape docks it. 7. Libraries: Motion (drag, dragConstraints, dragMomentum={false}, dragSnapToOrigin), GSAP Draggable (bounds: window), interact.js or @use-gesture; any of them makes the clamp and the snap a few lines.

Gotchas. - Containment is measured once, at drag start, from the header's size at that moment. With the title pill still open the header was wider, so a drag to the bottom-right corner stopped at x 852.9 instead of 1136 (header-corner, drag-06-contained-corner.png). - Dragging near an edge scrolls the page (jQuery UI's scroll: true): drag-06 shows the grid scrolled 60 px. - The header blocks clicks only on its pills and handle (pointer-events: none on the rest), but its text still overlays captions wherever it is dropped.

3.5 The phone foot bar

What it is. A fixed bar at the bottom of the phone viewport: container 390 × 51.4 px with 10 px padding, bar 370 × 31.4 px, #7c7c7c, radius 3 px, text #afafaf: the code in Politics ("45. C"), the title in Camera Plain 14 px ("Ciel Azur | Jaymie | Caraz"). It carries a backdrop-filter: blur(45px) that the opaque fill hides.

DOM.

<div id="mobile-title-container" class="hide-on-desktop">
  <div id="mobile-project-title-home" class="nav-element">
    <span class="mono uppercase" id="mobile-project-code-home">00.XX</span>
    <p class="small" id="mobile-project-name-home">Project title goes here | Client Name</p>
  </div>
</div>

Mechanism (observerprojects.js, started by an inline script on /work and by ajax.js after a swap): activateCenteredProject('.projects-grid .project', 'active', changeMobileProject) binds scroll and resize on window; every event measures every project's getBoundingClientRect(), keeps those on screen, picks the one whose centre is nearest innerHeight / 2, moves an active class to it and writes its data-code and data-title into the bar. The text swaps instantly, with no transition and no throttle. On the phone home the same bar is driven by Swiper's slideChangeTransitionEnd.

Measured (steps/07-phone-work-bar.meta.json): - At the top: "24. C Ball Was Out | Yony | Coming of Age". This is the second project; the first sits too high to be nearest the middle. - At 1500 px: "55. C Bluff Bounce | Bluff Bounce | Matheo Fernandes"; the active card's centre is at 392 against a middle of 422. - At 4200 px: "45. C Ciel Azur | Jaymie | Caraz". - Shots: m-work-00-top.png, m-work-01-scroll-1500.png, m-work-02-scroll-4200.png.

The phone menu. A full-width "∷ Menu" pill at the top under a one-line header ("BURN: A MULTI-PLATFORM CREATIVE COMPANY", "MODE LIGHT DARK"). Tapping it shows "∷ Close", two grey panels (Work, Talent, About, Contact, Shop | All, Commercial, Cinematic Social) and the BURN logo as eleven grey blocks across the width (m-work-03-menu-open.png). The panels rise 5 px and fade over 300 ms.

Accessibility. The bar is not a live region and cannot be tapped (it is information only).

Rebuild for ETS27: 1. Use an IntersectionObserver with rootMargin: '-50% 0px -50% 0px': a zero-height line across the middle of the viewport, so whichever block crosses it is "current", with no scroll handler and no layout reads. 2. Write the block's label into the bar's detail slot, the same slot as the desktop hover (3.3), so one bar speaks for the canvas on both. 3. Cross-fade the text over 150 to 200 ms. 4. Make the bar tappable to open the current block's drawer. 5. aria-live="polite", rate-limited. 6. Libraries: none; GSAP ScrollTrigger (start: 'center center', onToggle) if scroll effects are already using it.

Gotchas. Burn's version reads layout for all 68 cards on every scroll event. The top of the page names the second item, not the first.

3.6 Furniture: mode switch, live clock, list view

Mode switch. Two text buttons, "Light Dark", after "MODE:" in Politics 9 px uppercase above the pills (or in the phone header); the active one is underlined (offset 2 px). applyMode() swaps html.light for html.dark, marks the button and writes the cookie color-mode for 30 days. Dark: page #070707, text #FFFFFF80, pills #5b5b5b, crosses #505050; the page colours transition over 300 ms ease (work-dark.png, mode-dark-sheet.png; the burst's first frame landed after the transition had finished).

Live clock. clock.js runs every second: new Intl.DateTimeFormat('en-GB', {timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false}), printed as 'GMT+1 ' + time into #time-nav (50 px wide). Measured "GMT+1 05:07:47", then "05:07:49" two seconds later, at 03:07:48 UTC, which is Berlin summer time (CEST, UTC+2): the label is static and is wrong during summer time (late March to late October, about seven months a year).

List view. #list-trigger toggles html.show-list. .projects-list is a fixed, full-viewport sheet, rgba(255, 255, 255, .95) (dark rgba(0, 0, 0, .95)), z-index 10 under the menu, fading in over 300 ms ease while rising 5 px. It is a four-column grid (1fr 1fr 2fr 6fr: number, type, director, name) of tiny Politics uppercase rows, 5 px apart, each row a data-ajax link to the project; the four-dot selector by "List" turns on (list-open-light.png, list-open-dark.png). The sheet holds 204 rows: the 68 projects listed three times over, numbered 01 to 204 (3,088 px of content), so it runs far past a 900 px viewport (row 57 is the last fully visible). Its overflow is visible, so it is not a scroll container, and html.show-list sets the root to overflow: hidden, so rows 58 to 204 cannot be scrolled into view (computed styles; a real wheel or touch scroll was not tried).

Rebuild for ETS27. The clock idea maps to "time to the summit" or Austin time in the bar's quiet line; label it from the zone (timeZoneName: 'short'), not by hand. Apply the mode before first paint (a class from the cookie in an inline script in <head>). If a list view exists (the agenda as a list), give it overflow: auto and keep the bar above it.

4. Flow and transitions

All durations are CSS values confirmed by the captures. fsit2 was heavily loaded during the runs (load average about 25), so burst frames landed 220 to 1,070 ms apart (single shots took 110 to 1,200 ms); the bursts resolve the 1 s glide but not the 300 ms fades. Burst timestamps are when each frame finished.

Landing to home (click a block). Only the landing moves: opacity 1 to 0 and translateY 0 to 5 px over 300 ms ease, pointer-events: none at once. The homepage was rendered behind it all along. landing-click-sheet.png: frame 00 (+224 ms) still shows the landing, frame 01 (+904 ms) the homepage.

Grid to project (click a thumbnail, AJAX). 1. Click: preventDefault, body.loading (the wait cursor everywhere), GET /ajax/null/main/work/<slug>. 2. Response: one transitionend listener on #main, then body.hide-content: <main> fades to opacity 0 over 300 ms. 3. On transitionend: <main>'s HTML replaced, scroll to 0, videos and links re-wired, hide-content removed (fade in over 300 ms), body[data-page="project"]; CSS collapses the menu pills (#nav-container to max-height: 0, width 100 px) and opens the player bar (Back, code and title, Play with a progress line and time, fullscreen, Mute); history.pushState({slug, elementHref, lang, mainContent, dataPage, previousState}). 4. +400 ms: the header's top/left are set to the bottom centre (its bottom edge 50 px above the viewport's; a direct visit uses 20 px), and it glides there over 1 s ease. 5. +2000 ms: the script clicks Play: the film plays, the credit ticker fades out over 300 ms and Marquee3k pauses.

Observed (pt-in-sheet.png): - 00 (+841 ms): still the grid, title pill out. - 01 (+1910): the poster in the 1420 × 880 video box (10 px inset, 8 px radius), drawn letterboxed at about 1376 × 880 with #F8F8F8 showing 22 px either side (pixel edges in pt-in-01.png), the credit ticker across the middle ("N 58 DIRECTOR Matheo Fernandes PRODUCED Burn …"), the player bar still at the menu's old top-left spot (pt-in-01.png). - 02 (+2648): the bar mid-glide (pt-in-02.png). - 03 (+3524): settled at the bottom centre (top 768.7, left 362.2, 714.9 × 81.3). - 05 (+4938): the film playing from its black opening, ticker gone; 06 to 15 show the film's opening card and first shot. - pt-01-project-settled.png: the film playing with the bar at the bottom.

Answer: what a project page looks like and how it returns. A project page is the film and nothing else: a video element fitted to the viewport with 10 px insets and 8 px corners (the poster draws letterboxed inside it, about 1376 × 880 at 1440 × 900; the playing film fills 1420 × 861), a one-line credit ticker (number, director, producer, running time) scrolling slowly across the middle until play, and the menu turned into the player bar at the bottom centre. It returns by the bar's "Back": when the history state has a previous entry it calls history.back(); popstate re-renders the stored main HTML from the history state (no network) with the same 300 ms fade out and in (pt-back-sheet.png: frame 00 at +849 ms shows the film washed out mid-fade, frame 01 the grid). The grid comes back at the top (scroll position is not restored), the menu keeps the player bar's top and left (768.7, 362.2), so the taller work header now ends at the viewport's bottom edge, about 48 px left of centre, and the last title pill is still open (pt-02-back-settled.png, verify/verify-back.png). Without history (a direct visit) Back loads /work by AJAX.

Direct visit to a project URL. Server-rendered with data-page="project": the bar placed bottom: 20px and centred, no autoplay, the ticker with the running time ("TIME 14:53", project-direct.png; project-direct-later.png 2.5 s on shows the ticker moved and the poster unchanged). The bar's code reads "23.CS" here, a placeholder hard-coded in the template on every page; reached from the grid, the bar shows the hovered card's "58. C" and full title instead.

On the phone the project bar is two rows at the foot (progress on top, then Back and title, 370 × 65 px; m-project.png).

Within a page. Hover pill: 300 ms ease, width and text together (3.3). Thumbnail preview: instant. Landing idle: 300 ms cross-fade every 2 s. List sheet and phone menu: 300 ms fade with a 5 px rise. Mode: 300 ms on the page colours. Menu glide: 1 s ease whenever a script moves it.

Rebuild note for ETS27. Burn's page transition is a fade of the content plus the persistent bar moving to its new job, which is exactly the bar-stage-drawers model: keep the bar outside the swapped region, let the route set the bar's mode and position, and fade or slide only the stage. The View Transitions API (document.startViewTransition, with the bar given its own view-transition-name) does this natively, and Back gets the same treatment when the popstate re-render runs inside it; Barba or Swup do it for multi-page sites.

Contact sheets: landing-rest-sheet.png and landing-rest-logo-strip.png, landing-click-sheet.png, work-hover-pill-sheet.png and work-hover-pill-header-strip.png, work-unhover-pill-sheet.png and work-unhover-pill-header-strip.png, drag-sheet.png, list-open-sheet.png, mode-dark-sheet.png, mode-list-states-sheet.png, pt-in-sheet.png, pt-back-sheet.png, phone-sheet.png.

5. Shots

All in shots/. Desktop is 1440 × 900 at 1x; phone is 390 × 844 at 2x (780 × 1688 files).

File What it shows Device
landing-rest-00.png to -09.png, landing-rest-sheet.png, landing-rest-logo-strip.png Landing with no input, a frame about every 700 ms: blocks 6, 8, 3 light in turn desktop
landing-hover-block6.png, landing-hover-block1.png Hover lights only the hovered block; the idle cycle has stopped desktop
landing-click-00.png to -07.png, landing-click-sheet.png Click on a block: landing gone by frame 01 desktop
landing-after-click.png Homepage behind the landing: black logo, claim, two vertical slideshows, title pill out for the slide under the pointer desktop
work-rest.png /work at rest: pills, drag handle, clock, scatter on the cross grid desktop
work-full.png The whole /work page (12 rows, 3,088 px); the five video cards show only captions desktop
work-hover.png Title pill out for "06. C Ban Assault Weapons …" desktop
work-hover-pill-00.png to -07.png, -sheet.png, -header-strip.png Pill growing (frame 00 an empty stub) then full desktop
work-unhover-pill-00.png to -05.png, -sheet.png, -header-strip.png Pill retracting with "Bored Room C…" picked up on the way out desktop
drag-01-handle-hover.png Pointer on the four-arrow handle desktop
drag-02-mid-held.png, drag-03-mid-held-2.png Menu following the held pointer desktop
drag-04-released.png Menu left where it was dropped desktop
drag-05-moved-menu-hover-pill.png Title pill growing from the moved menu, clipped at the right edge desktop
drag-06-contained-corner.png Corner drag: page scrolled 60 px, menu stopped short of the right edge desktop
drag-07-after-reload.png After reload the menu is back top left desktop
drag-sheet.png The seven drag states together desktop
list-open-00.png to -05.png, list-open-sheet.png, list-open-light.png List sheet open (light) desktop
mode-dark-00.png to -05.png, mode-dark-sheet.png, work-dark.png /work in dark mode desktop
list-open-dark.png List sheet in dark mode desktop
landing-dark.png Landing in dark mode: still white desktop
home-behind-landing-dark.png Homepage in dark mode: white logo on black desktop
mode-list-states-sheet.png The five mode and list states together desktop
pt-00-work.png /work before clicking a card desktop
pt-in-00.png to -15.png, pt-in-sheet.png Card to project: fade, poster with ticker, bar gliding down, film plays desktop
pt-01-project-settled.png Project page, film playing, bar at the bottom centre desktop
pt-back-00.png to -11.png, pt-back-sheet.png Back: film fading out, grid returning desktop
pt-02-back-settled.png Grid after Back: menu still at the bottom, stale title pill desktop
project-direct.png, project-direct-later.png Project URL loaded directly: poster, ticker with running time, bar 20 px off the bottom, "23.CS" placeholder desktop
project-back-hover.png Pointer on Back: no hover style desktop
m-work-00-top.png Phone /work top: Menu pill, stacked cards, bar "24. C Ball Was Out …" phone
m-work-01-scroll-1500.png Bar "55. C Bluff Bounce …" phone
m-work-02-scroll-4200.png Bar "45. C Ciel Azur …" phone
m-work-03-menu-open.png Phone menu open: Close, two panels, logo blocks phone
m-project.png Phone project page: two-row player bar at the foot phone
m-home.png Phone home: film still, difference-blend headline and logo, foot bar phone
phone-sheet.png The six phone states together phone
verify/verify-back.png Verifier: /work after a project and Back; the menu sits where the player bar was, with the stale title pill desktop
verify/verify-dark.png Verifier: the same state in dark mode (#070707 page, #505050 crosses) desktop

Earlier screenshots of the site, reused for reference and not duplicated here: ../../burnstudio/desktop-top.png, ../../burnstudio/mobile-top.png, ../../burnstudio/work-desktop-top.png, ../../burnstudio/work-desktop-s1.png, ../../burnstudio/work-mobile-top.png.

6. Sources

7. Not confirmed


Verification

Burn Studio teardown: verification

Verifier pass on 2026-10-22 UTC (the evening of 2026-10-21 local). The aim was to refute the teardown's claims, not to restate them. Result: 69 claims held, 17 statements were wrong or overstated and are corrected in place in README.md and component.json (with 6 additions from new evidence), and 10 points stay unconfirmed.

1. How it was checked

2. What held

Stack

  1. Kirby: data-kirby-url on the 5 nav items; every image URL uses Kirby's media folders and thumb names (the folder name is corrected in section 3).
  2. Apache and PHP 8.1.34 with no CDN headers: HEAD /work returned content-type, date, server: Apache, x-content-type-options: nosniff, x-powered-by: PHP/8.1.34 and nothing else (no cf-ray, x-cache, age or via).
  3. DNS: NS ns17 and ns18.domaincontrol.com, A 92.205.148.223, no PTR.
  4. AJAX routing: the a[data-ajax] handler, /ajax/null/main/<slug> (the run logged /ajax/null/main/work/antropico), JSON status, content, dataPage, the transitionend swap and pushState with slug, elementHref, lang, mainContent, dataPage, previousState.
  5. No framework or bundler: 28 named global functions from the six site scripts are readable as source in the page; probe globals only jQuery and $; no generator meta.
  6. jQuery 3.6.0 and jQuery UI 1.14.1 at runtime ($.fn.jquery, $.ui.version); draggable is used once, on #header with handle #menu-drag and containment window; defaults scroll: true, scrollSensitivity: 20, scrollSpeed: 20.
  7. Swiper on the homepage: #landscape-swiper (vertical, slidesPerView: 'auto', 14 slides, 945 px wide on the left) and #portrait-swiper (vertical, 9 slides, 473 px wide on the right), every slide a .project-hover link.
  8. Marquee3000 1.1.1: initProjectMarquee() is Marquee3k.init({selector: 'marquee-project'}) plus a show class; data-speed=".25" in the markup.
  9. CSS transitions only: 0 @keyframes and 0 prefers-reduced-motion rules through all 14 imports; tokens .3s and ease; the probes found no GSAP, Lenis, Motion, Lottie or canvas and 0 running animations.
  10. The 14 imports, by name and order, are exactly those listed in README section 6.
  11. Scroll bars: ::-webkit-scrollbar { display: none; appearance: none; width: 0 !important } in commons/globals.css, the only scrollbar rule; scroll-behavior: auto.
  12. Fonts: three @font-face rules, Camera Plain normal (ABCCameraPlain-Regular-Trial.woff2), Camera Plain bold (ABCCameraPlain-Medium-Trial.woff2), Politics normal (Politics-Book.woff2).
  13. Videos: 15 on /, 30 on /work (25 hover previews and 5 thumbnails), 1 on the project; lazyVideos() is an IntersectionObserver on .autoplay that calls play() and adds video-playing.
  14. GA4 through gtag G-QE723JP8VM (inline config); cookies _ga and _ga_QE723JP8VM.
  15. Stored state: setCookie writes color-mode for days * 864e5 ms with 30 days; local and session storage empty after a drag.
  16. The probe caveat: its 0 counts come from not following @import; the verifier's walk followed them (16 sheets, the one cross-origin Swiper sheet unreadable).

Landing

  1. The layer: fixed, #fff, z-index 110, 1440 × 900, opacity .3s ease, transform .3s ease; hide-landing gives opacity 0, translateY(5px), pointer-events: none.
  2. Geometry: pitch 59.25 (the pattern's width attribute), --gap 58.25, logo 473 × 176.75 (8 × 58.25 + 7 by 3 × 58.25 + 2); blocks rgba(175, 175, 175, .5), radius 5 px, which is #d7d7d7 on white.
  3. The letter map: all twelve rects in block-rects sit on the 8 × 3 cells exactly as the table says.
  4. Cross alignment: the SVG sits at x 7, is 53,388.25 px tall (59.25 × 901 + 4) with its top at −26,244.125, so cross centres fall at 9 + k × 59.25; block 1's left, right and top edges sit 0.5 px from the nearest centre, and the viewport's middle falls halfway between two cross rows.
  5. The idle cycle: setInterval(…, 2000) with a loop that picks a different random block; the strip shows nothing, then 6, then 8 with 6 still fading, then 3.
  6. The first hover stops the cycle for good (hasHovered flag; active-after-hover-2500ms empty).
  7. The click adds hide-landing for any .logo-block (the TM text block included); the URL stays /; the block markup holds no link.
  8. Behind the landing (landing-after-click.png): the black BURN logo, the two-line claim, the two slideshows and the pill "55. C Bluff Bounce | Bluff Bounce | Matheo Fernandes" for the slide under the pointer.
  9. Eleven 300 px JPEG stills: burn_hoodie_2, burn_kevin-durant_kd, burn_coi-leray, burn_house-of-leon, burn_storm-reid, burn_anthony-edwards, burn_lamelo-ball, burn_brady_gun-control, burn_deloitte_senegal, burn_mvmi, burn_hoodie_1.
  10. The cross: a 4 × 4 plus with 0.4-unit arms, fill: currentColor from #background-container { color: #AFAFAF }; dark mode rgb(80, 80, 80).
  11. The phone home: foot bar "64. C.S Bored Room Copier | Circle | Giraffe Sisters" (eval and m-home.png), #landscape-container with mix-blend-mode: difference, the logo 370 px wide at the foot.
  12. No reduced-motion check anywhere; the cycle runs until the first hover.
  13. Dark mode: the landing stays white and the homepage turns black with a white logo (mode-list-states-sheet.png).
  14. The mode is applied late: the load handler reads getCookie('color-mode') and calls applyMode; with the dark cookie set, <html> had no class at readyState "interactive", and the cached /work HTML starts <html lang="en">, so the first paint is light.
  15. window.gap leaks (59.25 on both pages).

Grid

  1. The pitch formula simplifies to (W − 18) / d; the values at 1440, 390, 1280, 1920, 820 and 819 check out; measured 59.25 and 58.25 at 1440, 46.5 and 45.5 at 390.
  2. Tracks: eight 175.75 px columns, 2 px column gap, 61.25 px row gap, align-items: center, flow row; main padding 10 10 58.25 px; root font 10 px.
  3. The column pitch 177.75 px is three cross pitches; the first gutter's centre, 186.75 px, is a cross centre; by the CSS (7 px padding) thumbnails sit 1 px off the cross rows.
  4. Heights 116.5, 235 and 353.5 px; 59 landscape stills, 5 videos and 4 portraits among 68 projects (all five videos are "C.S" items; 13 of the 68 are "C.S").
  5. Row tracks 116.5, 353.5, 235, 116.5, 353.5 px and so on; a landscape in a portrait row is offset by 118.5 px, two pitches.
  6. The pin cycle is as quoted, and rows 1 to 4 in item-rects match both the README and the sparse auto-placement worked by hand.
  7. Twelve rows, 3,088 px (work-full.png is 1440 × 3088; html computes 3,088 px).
  8. Filters: handleProjectsFilters toggles .hidden (display: none) and, for a category, sorts hidden items last and re-appends them; "all" sorts by data-order.
  9. The phone sizes: 370 × 230.5, 276 × 414.5 at x 57, and 370 × 184; the desktop video rule (0,4,0) outranks the phone rule (0,3,0).

Title pill

  1. Geometry: 31.4 px tall, radius 3 px, #7c7c7c (dark #5b5b5b), text #afafaf; BURN 75.97 px at x 35, Work 71.47, List 61.94, the title pill at x 303.875 with width 0 (All is 55.5 by subtraction); open 441.7 × 31.4 px.
  2. The mechanism: delegated mouseenter and mouseleave, show-title, play() and pause() on the preview, as quoted.
  3. Timing: the enter samples fit 300 ms ease (cubic-bezier(.25, .1, .25, 1) gives 74.4 % at 134 of 300 ms; measured 74.1 %) with the text opacity tracking the width; leaving runs 241 px to 0 between 168 and 460 ms; the re-target dips to 283.3 px and opens again to 271.6 px with the text back at 1.
  4. The bursts: the header strip shows a grey stub in frame 00 and the full title in 01; the unhover strip shows "Bored Room C…" retracting in frame 00 (card 9, "64. C.S", lies on the pointer's path) and nothing in 01.
  5. 25 of 68 projects have a hover preview; its container switches with no transition under (hover: hover).
  6. Contrast of #afafaf on #7c7c7c: 1.90:1.
  7. The stale pill after a swap: 316.7 px wide with "Antrópico | Antrópico | Matheo Fernandes" after Back in the fresh run (pt-02-back-settled.png, verify-back.png).
  8. The pill runs off the right edge when the menu is dragged right (drag-05: "…Jacob Pro").
  9. Focus rings on clicked buttons (List in list-open-light.png, Dark in verify-dark.png).

Draggable menu

  1. The handle: 12.5 × 12.5 px at x 16.5, its right edge 6 px left of BURN, centred on the pills (y 172.1); cursor grab.
  2. The header: 303.875 × 130.734 px at (10, 126.5) from top: calc((var(--gap) * 2) + 10px), set 600 ms after load, faded in over 300 ms.
  3. While dragging: ui-draggable-dragging, a computed transition of only opacity and colour, cursor grabbing; inline top and left move by exactly the pointer deltas (618, 348) and (260, 180).
  4. Nothing stored; a reload puts it back at (10, 126.5).
  5. Containment from the drag start: the corner drag stopped at x 852.86; 1440 − 852.86 − 303.875 = 283.3 px, the closing pill's width 26 ms into its close in the leave samples, so the header was still that much wider when the drag began.
  6. Edge scrolling: the row 1 captions sit 60 px higher in drag-06 than in drag-05 (y 155 to 95), which is three move events inside the 20 px band times scrollSpeed 20.
  7. Phone: #controls-nav, #menu-drag, #copyright-nav { display: none !important } and the header pinned to the bottom.

Phone bar

  1. Container 390 × 51.39 px with 10 px padding, bar 370 × 31.39 px, #7c7c7c, radius 3 px, backdrop-filter: blur(45px) opacity(1).
  2. The observer as quoted, started by the inline $(window).on('load', …) on /work and again by handleNavActiveLink after an AJAX swap to /work.
  3. The bar texts at 0, 1,500 and 4,200 px; the active card's centre at 392.25 against 422; at the top item 2 is nearest (item 1's centre is 203.75 px off, item 2's 75.25 px).
  4. The phone menu (m-work-03-menu-open.png): Close, the Work, Talent, About, Contact, Shop panel and the All, Commercial, Cinematic Social panel, eleven blocks; the foot bar stays.
  5. On the phone home slideChangeTransitionEnd on the portrait slider calls changeMobileProject.

Furniture

  1. Mode: applyMode swaps the html class and writes the cookie; "MODE: Light Dark" with the active one underlined, text-underline-offset: 2px; dark page rgb(7, 7, 7), text rgba(255, 255, 255, .5), pills rgb(91, 91, 91), crosses rgb(80, 80, 80); html transitions colour and background over .3 s.
  2. Clock: 'Europe/Berlin', the static 'GMT+1 ' prefix, 50 px wide; the agent's own eval read "05:07:48 CEST" at 03:07:48 UTC.
  3. List: toggles html.show-list; fixed 1440 × 900, rgba(255, 255, 255, .95) (dark .95 black), z-index 10, 300 ms opacity and transform; 1fr 1fr 2fr 6fr; 5 px gaps; rows are a.project.list-project with data-ajax; the selector dot at opacity 1; row 57 is the last fully visible ("57. Caraz The Life Cycle of a Grey Ant | Trailer").

Flow and shots

  1. The landing click (landing-click-sheet.png): frame 00 the landing with block 1 lit under the pointer, frame 01 the homepage.
  2. Into a project: one /ajax/null/main/work/antropico request; handleNavActiveLink runs inside the transitionend callback, sets the header to calc(100vh - <height>px - 50px) after 400 ms and clicks Play after 2000 ms; the bar settles at (362.2, 768.7), 714.9 × 81.3, centred, 50 px above the bottom; the pt-in-sheet.png frames are as described.
  3. Back: checkBackTrigger is link.classList.contains('back-trigger') && history.state?.previousState; getInitialState gives the first entry a full state on load, so Back after a card calls history.back(); no new /ajax/ request after Back; scroll 0; a direct visit's Back loads /work by AJAX.
  4. The direct visit: bottom: 20px, centred (423.8 + 592.4 / 2 = 720), paused, ticker is-init show, "TIME 14:53" and "23.CS" (project-direct.png); the phone bar 370 × 65 px in two rows (m-project.png).
  5. Shot sizes: desktop 1440 × 900 at 1x, phone 780 × 1688, work-full.png 1440 × 3088.

3. Corrections, verifier, 2026-10-22

Each item was wrong or overstated; the text was corrected in place where named.

  1. Kirby's media folder. Said: media under /media/site/<hash>-<timestamp>/. True: every image URL seen is under /media/pages/<page>/<hash>-<timestamp>/ (landing stills /media/pages/home/…, thumbnails /media/pages/work/<slug>/…) with Kirby thumb names (-300x.jpg, -640x.webp to -1920x.webp); nothing in the folder or the runs shows /media/site/. Corrected in README section 2 and stack.evidence.
  2. SRI. Said: the two jQuery URLs "(with SRI)". True: only the jQuery UI tag carries integrity; the jQuery 3.6.0 tag has none. Corrected in README section 2 and stack.evidence.
  3. The transition count. Said: 28 transition declarations. The live CSSOM, walked through all 14 imports, has 23 style rules with a transition; the 28 was a line count of the source that the verifier did not reproduce (it can include overridden duplicates or comments). Both figures are now given, in README section 2 and stack.evidence.
  4. "Looping" slideshows. Said: two vertical, looping slideshows, on the homepage only. True: both are created with loop: true, but every homepage load logs Swiper's warning that there are too few slides for loop mode ("it will be disabled or not function properly"); the script loads on every page and is used on the homepage only. Corrected in README section 2 and stack.animation.
  5. Everything is a 300 ms CSS transition. Said in the short version. True for the landing, /work and the bar; the homepage slideshows are Swiper-driven (800 ms slides at 820 px and wider) and the credit ticker is Marquee3000. Corrected in the README's short version.
  6. "The only other furniture". Said: rows 13 and 14 of INSPIRATION.md call the mode switch, clock, list toggle and project transition "the only other furniture". True: the phrase is in bar-stage-drawers.md and names only the mode switch and the live clock. Corrected in README section 1 and the owner_words of the mode switch and the clock.
  7. The rest burst. Said: the strip shows blocks 6, 8, 3, then 7. True: the strip shows 6, 8 and 3; block 7 was lit only by the time of the eval after the burst (active-after-burst). Corrected in README 3.1.
  8. The 820 px breakpoint. Said: the landing is hidden "below 820 px" and the header is opacity 0 "under 820 px". True: both rules sit in @media only screen and (max-width: 820px), so they apply at 820 px too, while grid() keeps the 24-cell pitch at 820 px; at exactly 820 px the one-column phone layout runs on a 33.42 px pitch (now a sentence in the 3.2 gotchas). Corrected in README 3.1, 3.2 and 3.3 and the landing's phone state.
  9. The phone home slider. Said: one full-bleed vertical Swiper of portrait stills. True: below 821 px initHomepageSwipers() moves the landscape slides into the portrait slider, so it holds both sets (the screenshot happens to show a portrait). Corrected in README 3.1.
  10. Captions. Said: the caption "fills exactly the one-cell row gap". True: it is 57.25 px tall (--gap − 1) and starts 1 px under the image, inside a 61.25 px gap (1 px above it, 3 px below). Corrected in README 3.2 and the grid's mechanism.
  11. What the pill script writes. Said: the measured width and height. True: width and max-height. Corrected in README 3.3 and the pill's mechanism.
  12. The list has 204 rows, not 68. The sheet's ul holds a header and 204 rows: the 68 projects listed three times over, numbered 01 to 204 (row 69 starts again at Antrópico), 3,088 px of content. Row 57 is the last fully visible, as said, and the overflow point now rests on the computed styles (overflow: visible on the sheet, overflow: hidden on the root while it is open). Corrected in README 3.6 and section 7, the list's mechanism and not_confirmed.
  13. The clock's error. Said: wrong "for half the year". True: wrong during summer time, late March to late October, about seven months. Corrected in README 3.6 and the clock's mechanism.
  14. Capture timing. Said: each screenshot took 250 to 1000 ms. True, from the metas: burst frames landed 220 to 1,070 ms apart, the first frame 220 to 1,090 ms after a burst began, and single viewport shots took 110 to 1,200 ms. Corrected in README sections 4 and 7 and not_confirmed.
  15. The poster. Said: frame 01 shows "the poster at full size (1420 px wide … 10 px inset)". True: the video box is 1420 × 880 with 10 px insets, but the poster draws letterboxed at about 1376 × 880 (non-background pixels from x 32 to 1407 and y 10 to 889 in pt-in-01.png and project-direct.png); the playing film fills 1420 × 861 (x 10 to 1429, y 20 to 879 in pt-01-project-settled.png; css 1420 × 860.875). Corrected in README section 4 and the project page's mechanism.
  16. The menu after Back. Said: it stays at the bottom centre. True: it keeps the player bar's top and left (768.7, 362.2), but the work header is 620.5 × 130.7 px, so its bottom edge meets the viewport's (0.5 px off) and its centre sits about 48 px left of the middle (header-after-back, after-back, verify-back.png). Corrected in README section 4 and the back transition's layers.
  17. The codec remark. Said: a headed run on fsit2 with proprietary codecs would show the previews. True: this Chromium already plays H.264 (canPlayType "probably") and reports no HEVC support (""), so a headed run of it would not help. Corrected in README section 7 and not_confirmed.

Additions from the verifier's evidence, also in place:

4. Not confirmed

  1. The Kirby version and the server half of /ajax/<lang>/<id>/<slug>.
  2. The hosting product (shared or dedicated) behind the Host Europe address.
  3. The thumbnails' codec: the video track is missing or undecodable in this Chromium; HEVC fits but is not proven without a media download. The agent's note that the hover previews also painted nothing was not re-checked (the probe covered the five thumbnails).
  4. The 300 ms fades frame by frame (no new bursts were run).
  5. A real wheel or touch scroll over the open list (the computed styles say rows 58 to 204 cannot be reached).
  6. The 28 transition lines: not reproduced; the CSSOM has 23 rules with a transition.
  7. Which slider triggers the loop warning, and whether loop mode works on the other.
  8. The Politics foundry and the licence of the Camera Plain trial files.
  9. Details taken from the agent's source reading or captures without a fresh look: #nav-container collapsing to max-height: 0 and 100 px in project mode, Marquee3k pausing on play, the outline: … auto 0px rule, no hover style on Back (project-back-hover.png not viewed), and "23.CS" being in the template on every page (seen on the direct project page, desktop and phone, only).
  10. fsit2's load during the agent's runs (load average about 25): not re-checkable afterwards.

5. Also noticed, not added to the playbook

6. Folder tidiness