惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

IT之家
IT之家
C
CXSECURITY Database RSS Feed - CXSecurity.com
V
Visual Studio Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
小众软件
小众软件
L
LangChain Blog
Cyberwarzone
Cyberwarzone
美团技术团队
The Register - Security
The Register - Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
V
V2EX
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News: Ask HN
Hacker News: Ask HN
L
LINUX DO - 最新话题
Recent Announcements
Recent Announcements
H
Hackread – Cybersecurity News, Data Breaches, AI and More
酷 壳 – CoolShell
酷 壳 – CoolShell
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
aimingoo的专栏
aimingoo的专栏
人人都是产品经理
人人都是产品经理
F
Full Disclosure
V2EX - 技术
V2EX - 技术
The Cloudflare Blog
博客园 - 叶小钗
T
Threat Research - Cisco Blogs
阮一峰的网络日志
阮一峰的网络日志
G
GRAHAM CLULEY
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Latest news
Latest news
S
Security @ Cisco Blogs
Spread Privacy
Spread Privacy
Project Zero
Project Zero
K
Kaspersky official blog
MyScale Blog
MyScale Blog
Attack and Defense Labs
Attack and Defense Labs
云风的 BLOG
云风的 BLOG
博客园 - 【当耐特】
Hacker News - Newest:
Hacker News - Newest: "LLM"
大猫的无限游戏
大猫的无限游戏
P
Privacy International News Feed
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
C
Cybersecurity and Infrastructure Security Agency CISA
Webroot Blog
Webroot Blog
罗磊的独立博客
Vercel News
Vercel News
N
News and Events Feed by Topic
A
Arctic Wolf
C
CERT Recently Published Vulnerability Notes

Codrops

Meet the Speakers of the First Three.js Conference | Codrops The Architecture Behind Trionn: Coordinating GSAP, Three.js, Lenis, and Web Audio | Codrops Where Stories Become Interactive Worlds: How makemepulse Brings Ideas to Life Across Web, XR, and Beyond | Codrops The Sleepers: Creating an Atmospheric WebGL Experience with Lightweight Techniques Building an Interactive Wave Propagation Cube Grid with Three.js | Codrops Ten Years Away: Designing an Interactive Comic for Studio375’s Tenth Anniversary | Codrops Building a Scroll-Driven 3D Gallery Using a Blender Camera Path with Three.js and GSAP | Codrops From Years of Client Work to a Next.js + Sanity Starter Kit | Codrops Sculpting a Digital Athlete: Capturing Stefanos Tsitsipas Beyond the Court | Codrops Building Persistent Page Transitions with WebGPU and Vanilla JavaScript | Codrops Inside Bisous: Designing an Editorial Experience for Cinematic CGI | Codrops Engineering the Web Experience Behind Shopify’s Spring ’26 Edition: Everywhere | Codrops Shaping Stories into Experience: The Work of Kevin Lam | Codrops Testing What Users Actually See with Vitest and Chromatic | Codrops Podium: Building a Website Where Running Becomes Storytelling | Codrops Designing Beyond the Surface: How DashDigital Turns Complexity into Clarity | Codrops Creative Entrepreneurship: Designing the Machine | Codrops Exploring 3D Image Rotations on Scroll | Codrops Always Building, Always Learning: Cyd Stumpel’s Journey Through the Modern Web | Codrops Designing A Better Lou: Reducing Cognitive Load Through Design, Content, and Systems | Codrops Sketching the Impossible: A 3D Portfolio Built Without a Single 3D Model | Codrops Building Horeca: Advanced Motion Design in Webflow Without the Performance Trade-Offs | Codrops Building an Interactive Digital Stamp Collection with Shaders, Postcards, and Playful Inspection | Codrops No Two Paths Alike: Inside San Rita’s Approach to Digital Experiences | Codrops The Making of the New Lesse Studio Website: Clarity, Performance, and Intentionality | Codrops Creating a Thumbnail Flow Animation with GSAP MotionPath | Codrops Forging Her Own Path: Houmahani Kane’s Journey in Creative Development | Codrops Beaucoup and the Pursuit of Memorable Experiences | Codrops The Future of Emotional Technology: Comfort Websites for Growth and Self-Awareness | Codrops The Never Ending Story: Building a Seamless Infinite Scroll Experience with GSAP & Lenis | Codrops Whooshes, Snaps and Shaders: Adrien Vanderpotte and the Feeling of the Interface | Codrops Building a Scroll-Driven 3D Cube Gallery in Webflow with GSAP | Codrops Creating Scroll-Driven SVG Map Animations with GSAP | Codrops Made With Gsap: Building a Fun Gravity-Based Mouse Trail | Codrops 80s Business Tech and Seamless Scene Transitions: Inside Shader.se’s Scroll-Driven WebGPU Pipeline | Codrops Merlin: The Code Boutique Turning Motion Into Digital Magic | Codrops Designing Ourselves: The New Obys Identity and Website | Codrops Exploring the HTML-in-Canvas Proposal | Codrops Le:mma Studio: Building the Feeling Behind the Screen | Codrops Reverse-Engineering Claude AI’s Mascot Animations with SVG and GSAP | Codrops Akaru: Crafting Precision-Driven Digital Experiences | Codrops Designing Against the Gallery: A Two-Year Journey to a Layered Portfolio Experience | Codrops Between Human and Machine: Sofia Papadopoulou’s Approach to Augmented Creativity | Codrops More Than a Portfolio: Building a Scroll-Driven 3D World with Something to Say | Codrops Rhumb Studio: Small but Mighty, Shaped by Curiosity | Codrops Susurrus: Crafting a Cozy Watercolor World with Three.js and Shaders | Codrops Building 4WIDE: Turning Distortion, Blur, and Motion into a Coherent Experience | Codrops A Playful Clip Menu with GSAP’s easeReverse | Codrops False Earth: From WebGL Limits to a WebGPU-Driven World | Codrops Interactive Storytelling for the Web: Building Immersive Stories with Timelines, 3D, and Layered Scenes | Codrops The Art of Complex Motion: Corentin Bernadou’s World of Shaders and Experimentation | Codrops They Call Me Giulio: The Making of a Cinematic Cyberpunk Portfolio | Codrops Lusion: Where Digital Craft Meets Ambitious Experimentation | Codrops The Exat Microsite: Pushing a Typography Showcase to New Creative Extremes | Codrops A Single DM, a First SOTD, and the Start of Chanh M. Ho’s Creative Journey | Codrops Creating Custom Page Transitions in Astro with Barba.js and GSAP | Codrops R—K ’26: The Thinking and Code Behind a Portfolio Led by Presence | Codrops Building the Maxima Therapy Website: React, GSAP, and Dabbling with AI | Codrops Where Engineering Meets Craft: Edoardo Lunardi’s Obsession with the Details | Codrops Animating 160,000 Cubes in Three.js to Visualize Dithering | Codrops Arnaud Rocca’s Portfolio: From a GSAP-Powered Motion System to Fluid WebGL | Codrops Where Confidence Needs No Decoration: The Malvah.Studio Story | Codrops Motion Highlights #17 | Codrops
From Shader Uniforms to Clip-Path Wipes: How GSAP Drives My Portfolio | Codrops
By Thibault Guignand · 2026-05-06 · via Codrops

My name is Thibault Guignand. I work both as a freelancer and full-time employee (CDI). It’s a dual setup that exposes me to every kind of project, from corporate work to fully creative ones. My long-term goal is to shift 100% of my time toward creative work.

This redesign was, in a way, a lab. A way to measure where I stand in the creative web game today, and what I’m worth in it. My previous portfolio already carried the seed of this design direction; I wanted to pick it back up, push it further, and bring it in line with the years of experience accumulated since.

My daily intake comes from creatives I follow closely (Aristide Benoist, Cathy Dole, Corentin Bernabou, and others) and from specialized platforms like Awwwards. I don’t look at their work to copy; I look at it to set a bar for myself.

Several weeks in total. The core build came together fast; polishing it stretched the timeline. Making every format (desktop, tablet, mobile, reduced motion) behave exactly the way I wanted took most of it.

I started the WebGL layer with Three.js, the obvious choice. Halfway in, I rewrote everything in OGL. The tradeoff was worth it: lighter bundle, leaner API, and a codebase I felt I truly owned line by line.

Tech Stack & Tools

The stack is intentionally mainstream. Picking widely-deployed tools means I can demonstrate mastery of the same building blocks studios already use.

Vite + React 18 + TypeScript. Default answer for me now: fast dev loop, typed confidence, zero surprise for anyone reading the code.

GSAP. Fan since day one, and now that it’s fully free, it’s a no-brainer. Green-heart love. SplitText, ScrollTrigger, and the timeline API are unmatched for the kind of motion I want.

OGL. Covered in the backstory: lightness over three.js, and an excuse to dig deeper into low-level WebGL.

Lenis. Native scroll is too brittle for tightly-coupled ScrollTrigger animations. Lenis gives me smooth scrolling plus a single source of truth I can sync with GSAP’s ticker.

SCSS + BEM. Habit and personal preference. When I’m writing shaders and bespoke layouts, a predictable naming convention keeps my head clear.

i18n (FR/EN). Creative awards platforms have international juries; bilingual isn’t optional if I want the site judged by the widest possible audience.

Design tooling. I sketched the layout grid in Figma to lock the rhythm and proportions, then shipped it as part of the production CSS. Press Cmd/Ctrl + G anywhere on the site and the grid overlays in place: same gutters, same rows, same columns. The actual content sits on that grid, not approximating it. Everything else (typography, motion, transitions) I designed directly in code. Fewer handoffs, tighter feedback loop.

Feature Breakdowns

Video Carousel Transition

The homepage sits on top of a full-screen video carousel: hover any project rectangle and the current video melts into the next through a block-reveal pattern distorted by noise, with a chromatic aberration that peaks mid-transition.

How it works. A full-screen triangle in NDC space runs the fragment shader below. Three things happen in parallel:

  1. Block reveal mask: UVs are pixelated and sampled against a static noise texture. A step() against the progress uniform turns this into a binary mask that grows block by block. No linear wipe; every pixel switches abruptly but not simultaneously.
  2. Displacement: a second noise sample, scrolling in time, warps UVs along a 2D direction. Intensity follows parabola(progress, 2.) so the warp peaks at 50% progress and returns to zero.
  3. Chromatic aberration: red and blue channels of both textures are sampled with opposite offsets. Same parabola easing.
float dt = parabola(progress, 2.);

// Block reveal: static noise pixelated, compared to progress
vec2 blockUv = floor(vUv * uNoisePixelSize) / uNoisePixelSize;
float noiseVal = texture2D(displacement, blockUv).g;
float intpl = step(noiseVal, progress);

// Warp
vec2 displaceDir = (noise.rg - 0.5) * 2.0;
vec2 warpedUv = uv + displaceDir * dt * uDisplaceIntensity;

// Chromatic aberration
float shift = dt * uRGBShift;
t1.r = texture2D(texture1, warpedUv + vec2(shift, 0.0)).r;
t1.g = texture2D(texture1, warpedUv).g;
t1.b = texture2D(texture1, warpedUv - vec2(shift, 0.0)).b;

// same for t2…
gl_FragColor = mix(t1, t2, intpl);

GSAP × WebGL: one uniform as the bridge. The entire choreography (every block snap, every pixel of warp, every chromatic offset) is driven by a single progress number between 0 and 1. That number lives in JavaScript, tweened by a GSAP timeline with a custom ease; every animation frame I copy it into the progress uniform and OGL sends it to the GPU. The shader is stateless, GSAP owns the motion curve, and I can swap eases or chain timelines without touching a line of GLSL. It’s the single pattern I reuse across every effect in the project.

Per-frame upload, the minimum needed. Video textures must be re-uploaded each frame, since the browser has decoded new pixels. I flag texture.needsUpdate = true only on the two textures currently involved in a transition (source + destination), never on the whole pool. Outside of a transition, the carousel falls back to native <video> playback with zero GPU uploads.

Flowmap Text Distortion

Across the site, project titles and hero images react to the cursor with a fluid distortion and a velocity-driven chromatic rainbow. It’s the kind of effect that dies if you stutter the mouse over it, so every millisecond counts.

How it works. OGL’s Flowmap helper writes the cursor’s velocity into an off-screen RG texture each frame, accumulating a fading “brush stroke” of motion. The shader samples that flowmap to distort the text UVs, then does a second pass of directional chromatic aberration: instead of a symmetric RGB shift, each channel is offset along the vector from the mouse to the pixel, with different magnitudes per channel (R at 1.5×, G at 0.5×, B at 1.8×).

// Directional chromatic aberration: not centered, guided by cursor
vec2 toMouse = vUv - uMouse;

float influence =
    smoothstep(uRadius, 0.0, length(toMouse)) * uVelo;

vec2 offset =
    normalize(toMouse) * influence * uChromaticIntensity;

// RGB split sampling
float r = texture2D(tWater, baseUV - offset * 1.5).r;
float g = texture2D(tWater, baseUV + offset * 0.5).g;
float b = texture2D(tWater, baseUV + offset * 1.8).b;

// Rainbow kick when velocity is high: sin() with 120° phase offsets
if (uVelo > 0.01) {

    float hueShift =
        uTime * 0.01 + length(toMouse) * 2.0;

    r = mix(
        r,
        sin(hueShift) * 0.5 + 0.5,
        uVelo * uColorShift
    );

    g = mix(
        g,
        sin(hueShift + 2.094) * 0.5 + 0.5,
        uVelo * uColorShift
    );

    b = mix(
        b,
        sin(hueShift + 4.188) * 0.5 + 0.5,
        uVelo * uColorShift
    );
}

The rainbow uses the oldest trick in the book: three sin() calls separated by 2π/3 rad, mapped to R, G, B. It only kicks in when the cursor is moving fast enough, which keeps the effect quiet during idle hover and loud during fast swipes.

Mount once, swap textures. This is the optimization I’m proudest of. My first version mounted a fresh WebGL context for each project title. Clean in React terms, catastrophic in practice. GPU memory kept climbing; the rainbow stuttered by the fourth hover. The rewrite keeps a single FlowmapEffect mounted at the HomePage level and accepts the current target as an imageSrc prop. The context survives, only the texture swaps. Paired with an idle guard that stops the rAF loop after 90 frames without cursor input (and resumes on the next mousemove), the effect costs almost nothing when you’re not using it.

Next-Project Scroll Morph

At the bottom of every project page, the “Next project” preview expands as you scroll: a clipped, scaled-up background unclips into view while an SVG circle traces a 0→100% counter. Hit 100% and you’re automatically navigated to the next project. Scroll back up and everything reverses, and the navigation is cancelled.

How it works. A single ScrollTrigger with scrub: 1 drives the animation. Its onUpdate callback writes four values directly to the DOM every frame: no React state, no reconciliation.

onUpdate: (self) => {
  const progress = self.progress;
  const percent = Math.round(progress * 100);

  // Counter
  numberEl.textContent = String(percent >= 99 ? 100 : percent);

  // Background morph: scale + inset clip-path
  const bgScale = 1.3 - 0.3 * progress;

  const insetV = Math.max(0, 20 - 20 * progress);
  const insetH = Math.max(0, 40 - 40 * progress);

  bgEl.style.transform = `scale(${bgScale})`;
  bgEl.style.clipPath = `inset(${insetV}% ${insetH}% ${insetV}% ${insetH}%)`;

  // SVG progress circle
  circleEl.style.strokeDashoffset =
    String(CIRCUMFERENCE - progress * CIRCUMFERENCE);

  // Auto-navigation check
  if (percent >= 100 && state === "idle" && hasSeenLowProgress) {
    // trigger page change
  }
};

Writing element.style.* instead of calling setState() saves a full React render tree per frame. On a 120 Hz laptop, that’s the difference between butter and a slideshow.

A state machine, because scroll is unpredictable. The auto-navigation isn’t as simple as “reach 100% → go”. People flick-scroll past the section, land on a fragment reload at 100%, change their mind halfway. I ended up with a three-state machine (idle → triggered → navigating) plus two guards: a hasSeenLowProgress flag (you only auto-navigate if you actually scrolled from the top, not if you landed there) and a velocity ceiling (if scrollTrigger.getVelocity() > 2000 we skip the trigger). Scroll back up before the 250 ms commit timeout and onLeaveBack rolls everything back to idle. No phantom navigations.

The same animation, two drivers. The section is also clickable. A click spawns a GSAP tween from the current scroll progress to 1 and scrolls the page to match in parallel. Identical DOM mutations, identical visual result, just a different time source. Because the scrub path already writes everything through element.style.*, hijacking it with a GSAP tween took about ten lines.

Page Transitions (GSAP + View Transitions)

Leaving the homepage isn’t a hard cut. The WebGL background, grid overlay, side texts, and custom cursor fade together; a quarter-second later the content layer follows; then the browser’s View Transition API takes over for the final clip-path morph. Three technologies (GSAP, View Transitions, React) have to cooperate without stepping on each other.

Preload races the fadeout. The moment a link is clicked, two things start in parallel: the visual fadeout and the data fetch. The dynamic import() of the next route’s chunk and the hero image preload fire before GSAP paints a single frame. By the time the timeline finishes (~0.6 s), both have usually landed.

// Fire preloads first: they race the GSAP fadeout
const chunkReady = chunkPreloaders[routeChunk]().catch(() => {});
const imageReady = project?.heroImage
  ? preloadImage(project.heroImage)
  : Promise.resolve();

// Staged fadeout, all parallel with the network
const tl = gsap.timeline();

tl.to(
  [webglBg, gridOverlay, sideTexts, customCursor],
  { opacity: 0, duration: 0.3, ease: "power2.inOut" },
  0
).to(
  contentEl,
  { opacity: 0, duration: 0.35, ease: "power2.inOut" },
  0.25
);

await tl.then();
await Promise.all([chunkReady, imageReady]);

await startPageTransition(() => {
  flushSync(() => {
    navigate(path);
  });
});

flushSync is the detail that makes the View Transition work. document.startViewTransition takes a callback, captures the DOM before you mutate it, runs the callback, then captures the DOM after. React Router’s navigate() is asynchronous, so without intervention, the VT captures the old page twice and you get no animation. Wrapping navigate() in flushSync forces React to commit the new route synchronously inside the VT callback. Small detail, infuriating bug if you miss it.

Text Reveal: One Pattern Used Everywhere

The same reveal language plays across the entire site. Every block of text that appears uses the same combination: a GSAP SplitText for char or line structure, a scramble effect to resolve characters, and a clip-path wipe layered on top. Centralizing it in one utility means a tweak to the curve in one place changes the rhythm of the whole site.

The two effects run together, not in sequence. When you scramble a line from random characters toward the final string, the left edge resolves first. Layering a clip-path that opens left-to-right at the same speed means the user only ever sees the part of the text that’s already legible. Nothing reveals as visual noise; nothing reveals all at once.

gsap.to(lineEl, {
  duration,
  ease: "none",
  scrambleText: {
    text: lineText,
    chars: SCRAMBLE_CHARS,
    revealDelay,
    speed,
  },
  onStart: () => {
    // Wipe runs in parallel with the scramble resolve
    gsap.to(lineEl, {
      clipPath: "inset(0 0% 0 0)",
      duration: 0.6,
      ease: "power2.out",
    });
  },
});

Pre-scramble at the right length, lock the height. Two details that stop the layout from breathing during the animation:

  1. Before the tween starts, every char of the target string is replaced with a random character from SCRAMBLE_CHARS. Spaces are preserved. The line occupies its final width before resolving, so the gradual character swap causes no reflow.
  2. The parent height is locked to its measured getBoundingClientRect().height before SplitText runs. SplitText wraps each line in its own block; without the lock the wrapper briefly collapses and the rest of the page jumps.

The character set matters. SCRAMBLE_CHARS = 'A!B@C#D$E%F&G*H?J[K]L{M}N=O+P-QRSTUVWXYZ'. Mixing letters with punctuation gives the resolution that “decoding” feel rather than a smooth fade between alphabets. The text feels like it’s being pulled out of a buffer.

Visual & Interaction Design

A site for web people. I designed this portfolio for the people who’ll scroll through it with dev tools open. The visual language is meant to read as a technical handshake. If you know what “View Transition API” or “flowmap” means, the site is winking at you. That’s the audience I want to work with.

Effects I backed into rather than planned. Chromatic aberration didn’t start as a stylistic decision. It was a test to see how far I could push OGL’s texture sampling. Somewhere between the third and fourth iteration, it stopped looking like a tech demo and started looking intentional, so I kept it, and repeated it across the video transition and the flowmap hover. It’s become the visual thread tying the site together.

Reduced motion, handled properly. Early versions of this site broke hard on prefers-reduced-motion: reduce. Cassie Evans’ GSAP talks were what made me stop treating reduced motion as a disable-flag and start treating it as a parallel design: a real, degraded version that still conveys the same intent without the vestibular cost.

Architecture & Structure

Nothing revolutionary here. Disciplined more than clever.

src/
├── components/         // UI building blocks (custom cursor, minimap, mobile menu, intro, etc.)
├── contexts/           // AppStateContext, WebGLContext
├── data/               // projectsData.ts + image-dimensions.json + lqip-data.json
├── hooks/              // animation & transition logic
├── i18n/               // routes.ts + locales/{fr,en}/*.json
├── pages/              // HomePage, AboutPage, ProjectPage
├── providers/          // LenisProvider
├── services/           // lenisService: singleton synced with GSAP's ticker
├── shaders/            // GLSL, one folder per effect
├── styles/             // SCSS (BEM)
├── utils/              // scrambleText, prefersReducedMotion, imagePreloadCache, viewTransitions
└── webgl/              // Sketch.ts, FlowmapEffect.ts: raw OGL

Hooks vs services is the only split that mattered. Hooks own per-component lifecycle (setup, cleanup, ref juggling). Services are singletons that outlive any component. Lenis has to survive route changes because the scroll position belongs to the document, not the page.

Performance boils down to four habits I applied everywhere something ran every frame:

  1. Direct DOM mutations (element.style.*, textContent) instead of React state. At 120 Hz, a render tree reconciliation is ~8 ms I don’t have.
  2. Bound, persistent objects: boundRender = this.render.bind(this) cached once in the constructor; the flowmap mounted once at the page root; Vec2.set() instead of new Vec2().
  3. Idle guards: the flowmap’s rAF loop suspends after 90 frames without input; the video carousel falls back to native <video> outside transitions.
  4. Build-time image metadata: image-dimensions.json (zero CLS) and lqip-data.json (inline base64 blur-ups), plus per-route chunk preloading on hover.

Reflections

I poured energy into this. The point wasn’t only to ship a portfolio. It was to make the most finished thing I’d ever made, to push every detail (UI, design, accessibility, performance) until I felt I’d genuinely learned something along the way. The unspoken goal was to contribute, in a tiny way, to raising the bar for what people expect from the web. I don’t know if I succeeded. What I know is that I left it all on the field.

What worked

  • The persistent flowmap pattern: mounting the WebGL component once at the page root and swapping textures via a prop instead of remounting per project. Eliminated a memory-leak class entirely.
  • Driving every WebGL effect from a single GSAP-tweened uniform. Once that pattern clicked, the rest of the project was just choosing eases.
  • Treating reduced motion as a parallel design, not a fallback. Cassie Evans changed how I think about accessibility, and the project shipped better for it.

What was hard

  • Safari + View Transitions + clip-path. Safari caches clip-path values on GPU layers as long as ::view-transition-* pseudo-elements are active. Reset the value during the transition and Safari ignores it until you force a repaint. Diagnosed by accident, fixed with a 50 ms post-transition buffer and a forced reflow (void el.offsetHeight). It’s the kind of bug you can’t find on Stack Overflow because the right keywords don’t exist yet.
  • The preload pipeline didn’t converge on the first try. Looking at the git log, perf: appears ten times in a row over a few weeks. Preload everything → too aggressive, blocks initial render. Preload nothing → flashes. Eventually I landed on: preload fonts immediately, hero image on hover, first project during the intro, defer the rest. Ten commits to get there.

What I’d do differently

Start with OGL, skip the three.js detour. Already mentioned in the backstory, but with hindsight, the day I learned Mesh, Program, Texture from OGL’s source was the day this project actually started.

Reach for Sanity earlier. A few of the friction points I built workarounds for (project data in a typed file, translations spread across JSON, image and video assets handled by hand) are exactly what Sanity is designed to solve. It’s a genuinely exceptional tool, and the next iteration of the content layer will start there.