# Fullscreen layout with columns

> Equal fullscreen columns that expand into a full-stage panel with GSAP open/close motion — ideal for studio and portfolio landings.

**Definition:** Fullscreen layout with columns is a cards UI animation from collections.html. It is built with HTML, CSS, JavaScript, and GSAP, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Cards
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-06
- Canonical page: https://html.bosconvertpro.com/collection/fullscreen-layout-with-columns
- Full-page preview: https://html.bosconvertpro.com/collection/fullscreen-layout-with-columns/full
- Live demo: https://html.bosconvertpro.com/demos/fullscreen-layout-with-columns/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/fullscreen-layout-with-columns/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/fullscreen-layout-with-columns/full to preview the animation in full screen.
2. Copy index.html, styles.css, and script.js from the code tabs on https://html.bosconvertpro.com/collection/fullscreen-layout-with-columns.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is Fullscreen layout with columns?

Fullscreen layout with columns is a cards UI animation from collections.html. Equal fullscreen columns that expand into a full-stage panel with GSAP open/close motion — ideal for studio and portfolio landings.

### What is Fullscreen layout with columns built with?

Fullscreen layout with columns is built with HTML, CSS, JavaScript, and GSAP. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use Fullscreen layout with columns in my project?

Open the animation page (https://html.bosconvertpro.com/collection/fullscreen-layout-with-columns), preview it live, then copy index.html, styles.css, and script.js from the code tabs. Paste the files into your project and adjust timing, easing, and content as needed. A standalone demo runs at https://html.bosconvertpro.com/demos/fullscreen-layout-with-columns/index.html.

### Is Fullscreen layout with columns free to use?

Yes. The Fullscreen layout with columns demo code may be used in personal and commercial projects. You may not resell or redistribute the collection itself as a competing template pack. Full terms: https://html.bosconvertpro.com/terms.

## Source — index.html
````html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Fullscreen Columns — Minimal</title>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link rel="preconnect" href="https://wsrv.nl" />
    <link
      href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Instrument+Serif:ital@0;1&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <header class="site-header" data-header>
      <div class="site-header__brand">
        <span class="site-header__name">Atelier</span>
      </div>
      <p class="site-header__hint" data-hint>Select a column</p>
    </header>

    <main class="bl-main" id="bl-main" data-columns>
      <!-- 01 About -->
      <section class="bl-section" data-section>
        <div class="bl-box" data-box role="button" tabindex="0" aria-expanded="false">
          <div class="bl-box__bg" aria-hidden="true">
            <img
              src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1618005182384-a83a8bd57fbe&w=1400&h=1800&fit=cover&q=78&output=webp"
              alt=""
              draggable="false"
            />
          </div>
          <div class="bl-box__veil" aria-hidden="true"></div>
          <div class="bl-box__meta">
            <span class="bl-box__index">01</span>
          </div>
          <h2 class="bl-box__title" data-title>About</h2>
        </div>

        <div class="bl-content" data-content aria-hidden="true">
          <button class="bl-close" data-close type="button" aria-label="Close">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
            </svg>
          </button>
          <div class="bl-content__inner">
            <p class="bl-content__eyebrow">Studio</p>
            <h2 class="bl-content__title" data-split>Quiet design with presence.</h2>
            <p class="bl-content__lead">
              We shape digital experiences around restraint — clear type, calm
              motion, and space that lets the work speak.
            </p>
            <div class="bl-stats">
              <div class="bl-stat">
                <strong>12</strong>
                <span>Years</span>
              </div>
              <div class="bl-stat">
                <strong>180</strong>
                <span>Projects</span>
              </div>
              <div class="bl-stat">
                <strong>24</strong>
                <span>Awards</span>
              </div>
            </div>
            <p class="bl-content__body">
              A small practice between Lisbon and Tokyo. We partner with brands
              that value how things feel — not only how they look.
            </p>
          </div>
        </div>
      </section>

      <!-- 02 Works -->
      <section class="bl-section" data-section data-work-section>
        <div class="bl-box" data-box role="button" tabindex="0" aria-expanded="false">
          <div class="bl-box__bg" aria-hidden="true">
            <img
              src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1614850523459-c2f4c699c52e&w=1400&h=1800&fit=cover&q=78&output=webp"
              alt=""
              draggable="false"
            />
          </div>
          <div class="bl-box__veil" aria-hidden="true"></div>
          <div class="bl-box__meta">
            <span class="bl-box__index">02</span>
          </div>
          <h2 class="bl-box__title" data-title>Work</h2>
        </div>

        <div class="bl-content" data-content aria-hidden="true">
          <button class="bl-close" data-close type="button" aria-label="Close">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
            </svg>
          </button>
          <div class="bl-content__inner">
            <p class="bl-content__eyebrow">Selected</p>
            <h2 class="bl-content__title" data-split>Featured projects</h2>
            <p class="bl-content__lead">
              Identity, product, and editorial work defined by clarity and pace.
            </p>
            <ul class="bl-works" data-work-items>
              <li data-panel="0">
                <button type="button" class="bl-work-card">
                  <div class="bl-work-card__img">
                    <img
                      src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1618005198919-d3d4b5a92ead&w=900&h=700&fit=cover&q=78&output=webp"
                      alt="Lumen"
                    />
                  </div>
                  <div class="bl-work-card__info">
                    <span>01</span>
                    <h3>Lumen</h3>
                    <p>Identity</p>
                  </div>
                </button>
              </li>
              <li data-panel="1">
                <button type="button" class="bl-work-card">
                  <div class="bl-work-card__img">
                    <img
                      src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1558591710-4b4a1ae0f04d&w=900&h=700&fit=cover&q=78&output=webp"
                      alt="Northline"
                    />
                  </div>
                  <div class="bl-work-card__info">
                    <span>02</span>
                    <h3>Northline</h3>
                    <p>Product</p>
                  </div>
                </button>
              </li>
              <li data-panel="2">
                <button type="button" class="bl-work-card">
                  <div class="bl-work-card__img">
                    <img
                      src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1574169208507-84376144848b&w=900&h=700&fit=cover&q=78&output=webp"
                      alt="Orbit"
                    />
                  </div>
                  <div class="bl-work-card__info">
                    <span>03</span>
                    <h3>Orbit</h3>
                    <p>Campaign</p>
                  </div>
                </button>
              </li>
              <li data-panel="3">
                <button type="button" class="bl-work-card">
                  <div class="bl-work-card__img">
                    <img
                      src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1550684848-fac1c5b4e853&w=900&h=700&fit=cover&q=78&output=webp"
                      alt="Pulse"
                    />
                  </div>
                  <div class="bl-work-card__info">
                    <span>04</span>
                    <h3>Pulse</h3>
                    <p>Editorial</p>
                  </div>
                </button>
              </li>
            </ul>
          </div>
        </div>
      </section>

      <!-- 03 Journal -->
      <section class="bl-section" data-section>
        <div class="bl-box" data-box role="button" tabindex="0" aria-expanded="false">
          <div class="bl-box__bg" aria-hidden="true">
            <img
              src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1557672172-298e090bd0f1&w=1400&h=1800&fit=cover&q=78&output=webp"
              alt=""
              draggable="false"
            />
          </div>
          <div class="bl-box__veil" aria-hidden="true"></div>
          <div class="bl-box__meta">
            <span class="bl-box__index">03</span>
          </div>
          <h2 class="bl-box__title" data-title>Journal</h2>
        </div>

        <div class="bl-content" data-content aria-hidden="true">
          <button class="bl-close" data-close type="button" aria-label="Close">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
            </svg>
          </button>
          <div class="bl-content__inner">
            <p class="bl-content__eyebrow">Notes</p>
            <h2 class="bl-content__title" data-split>Field notes</h2>
            <p class="bl-content__lead">
              Brief thoughts on motion, type, and building less.
            </p>
            <div class="bl-articles">
              <article class="bl-article">
                <time datetime="2026-03-12">12 Mar 2026</time>
                <h3>Easing is half the story</h3>
                <p>How a curve turns a layout change into something you feel.</p>
              </article>
              <article class="bl-article">
                <time datetime="2026-02-01">01 Feb 2026</time>
                <h3>Less chrome, more presence</h3>
                <p>Removing UI until only the essential remains.</p>
              </article>
              <article class="bl-article">
                <time datetime="2026-01-18">18 Jan 2026</time>
                <h3>Space as material</h3>
                <p>Why empty areas do more work than another component.</p>
              </article>
            </div>
          </div>
        </div>
      </section>

      <!-- 04 Contact -->
      <section class="bl-section" data-section>
        <div class="bl-box" data-box role="button" tabindex="0" aria-expanded="false">
          <div class="bl-box__bg" aria-hidden="true">
            <img
              src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1614850715649-1d0106293bd1&w=1400&h=1800&fit=cover&q=78&output=webp"
              alt=""
              draggable="false"
            />
          </div>
          <div class="bl-box__veil" aria-hidden="true"></div>
          <div class="bl-box__meta">
            <span class="bl-box__index">04</span>
          </div>
          <h2 class="bl-box__title" data-title>Contact</h2>
        </div>

        <div class="bl-content" data-content aria-hidden="true">
          <button class="bl-close" data-close type="button" aria-label="Close">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
            </svg>
          </button>
          <div class="bl-content__inner bl-content__inner--contact">
            <p class="bl-content__eyebrow">Hello</p>
            <h2 class="bl-content__title" data-split>Let's talk</h2>
            <p class="bl-content__lead">
              Projects, collaborations, or a quiet hello — we read every note.
            </p>
            <div class="bl-contact">
              <a class="bl-contact__row" href="mailto:hello@atelier.studio">
                <span class="bl-contact__label">Email</span>
                <span class="bl-contact__value">hello@atelier.studio</span>
              </a>
              <div class="bl-contact__row">
                <span class="bl-contact__label">Studio</span>
                <span class="bl-contact__value">Lisbon · Tokyo</span>
              </div>
            </div>
            <form class="bl-form" data-form onsubmit="return false">
              <label class="bl-form__field">
                <span>Name</span>
                <input type="text" name="name" placeholder="Your name" autocomplete="name" />
              </label>
              <label class="bl-form__field">
                <span>Email</span>
                <input type="email" name="email" placeholder="you@studio.com" autocomplete="email" />
              </label>
              <label class="bl-form__field bl-form__field--full">
                <span>Message</span>
                <textarea name="message" rows="4" placeholder="A few lines…"></textarea>
              </label>
              <button class="bl-form__submit" type="submit">Send</button>
            </form>
          </div>
        </div>
      </section>

      <!-- Work detail panels -->
      <div class="bl-panels" data-panels aria-hidden="true">
        <div class="bl-panels__track">
          <article class="bl-panel" data-panel-item>
            <div class="bl-panel__media">
              <img
                src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1618005198919-d3d4b5a92ead&w=1400&h=900&fit=cover&q=80&output=webp"
                alt="Lumen"
              />
            </div>
            <div class="bl-panel__copy">
              <span class="bl-panel__num">01 / 04</span>
              <h3>Lumen</h3>
              <p>
                A quiet identity for a climate-tech studio. Custom type, a soft
                mark, and motion that never shouts.
              </p>
            </div>
          </article>
          <article class="bl-panel" data-panel-item>
            <div class="bl-panel__media">
              <img
                src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1558591710-4b4a1ae0f04d&w=1400&h=900&fit=cover&q=80&output=webp"
                alt="Northline"
              />
            </div>
            <div class="bl-panel__copy">
              <span class="bl-panel__num">02 / 04</span>
              <h3>Northline</h3>
              <p>
                Product design for logistics. Dense data, calm hierarchy, and
                micro-interactions that stay out of the way.
              </p>
            </div>
          </article>
          <article class="bl-panel" data-panel-item>
            <div class="bl-panel__media">
              <img
                src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1574169208507-84376144848b&w=1400&h=900&fit=cover&q=80&output=webp"
                alt="Orbit"
              />
            </div>
            <div class="bl-panel__copy">
              <span class="bl-panel__num">03 / 04</span>
              <h3>Orbit</h3>
              <p>
                Launch campaign for a wearable. One visual system from film to
                the landing page.
              </p>
            </div>
          </article>
          <article class="bl-panel" data-panel-item>
            <div class="bl-panel__media">
              <img
                src="https://wsrv.nl/?url=images.unsplash.com%2Fphoto-1550684848-fac1c5b4e853&w=1400&h=900&fit=cover&q=80&output=webp"
                alt="Pulse"
              />
            </div>
            <div class="bl-panel__copy">
              <span class="bl-panel__num">04 / 04</span>
              <h3>Pulse</h3>
              <p>
                A digital magazine. Type-led layouts and reading that feels
                unhurried on every screen.
              </p>
            </div>
          </article>
        </div>
        <nav class="bl-panels__nav" data-panels-nav>
          <button class="bl-panels__btn bl-panels__btn--close" data-panel-close type="button" aria-label="Close project">
            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
              <path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" />
            </svg>
          </button>
          <button class="bl-panels__btn bl-panels__btn--next" data-panel-next type="button">
            Next
          </button>
        </nav>
      </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/CustomEase.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/SplitText.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
````

## Source — styles.css
````css
/* ============================================================
   Fullscreen Columns — minimal premium
   ============================================================ */

:root {
  --bg: #0c0c0b;
  --surface: #141413;
  --surface-2: #1a1a18;
  --text: #f2f0ea;
  --text-soft: rgba(242, 240, 234, 0.68);
  --text-muted: rgba(242, 240, 234, 0.42);
  --line: rgba(242, 240, 234, 0.1);
  --accent: #c4b49a;
  --accent-soft: rgba(196, 180, 154, 0.16);
  --header-h: 56px;
  --font: "Inter", system-ui, sans-serif;
  --serif: "Instrument Serif", Georgia, serif;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
}

body {
  font-family: var(--font);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  line-height: 1.5;
  font-weight: 400;
}

img {
  display: block;
  max-width: 100%;
  user-select: none;
  -webkit-user-drag: none;
}

button,
input,
textarea {
  font: inherit;
  color: inherit;
  border: none;
  background: none;
}

button {
  cursor: pointer;
}

a {
  color: inherit;
  text-decoration: none;
}

/* ---------- header ---------- */
.site-header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 80;
  height: var(--header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 clamp(1.25rem, 3vw, 2rem);
  pointer-events: none;
}

.site-header__brand {
  pointer-events: auto;
}

.site-header__name {
  font-size: 0.8rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--text);
}

.site-header__hint {
  font-size: 0.7rem;
  font-weight: 400;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  transition: opacity 0.4s var(--ease);
}

body.is-expanded .site-header__hint {
  opacity: 0;
}

/* ---------- stage ---------- */
.bl-main {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: var(--bg);
}

/*
  Columns are positioned entirely by JS (pixel geometry).
  Default styles only establish stacking context.
*/
.bl-section {
  position: absolute;
  top: 0;
  left: 0;
  width: 25%;
  height: 100%;
  overflow: hidden;
  z-index: 1;
  /* Never let transform-origin / scale shrink a “full” panel */
  transform-origin: 50% 50%;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

.bl-section.is-expanded {
  z-index: 30;
}

.bl-section.is-open {
  z-index: 30;
}

.bl-section.is-dimmed {
  pointer-events: none;
  z-index: 1;
}

/* hairline dividers via box edge */
.bl-section:not(.is-expanded):not(.is-dimmed) {
  box-shadow: 1px 0 0 var(--line);
}

.bl-section:last-of-type {
  box-shadow: none;
}

/* ---------- face ---------- */
.bl-box {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: clamp(1.25rem, 2.4vw, 1.75rem);
  cursor: pointer;
  outline: none;
  overflow: hidden;
  z-index: 2;
  -webkit-tap-highlight-color: transparent;
}

.bl-box:focus-visible {
  box-shadow: inset 0 0 0 1px var(--accent);
}

.bl-box__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--surface);
}

.bl-box__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* subtle desaturate for premium minimal look */
  filter: saturate(0.55) contrast(1.02) brightness(0.92);
  transform: scale(1.06);
  will-change: transform;
}

.bl-box__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    180deg,
    rgba(12, 12, 11, 0.15) 0%,
    rgba(12, 12, 11, 0.35) 45%,
    rgba(12, 12, 11, 0.88) 100%
  );
}

.bl-box__meta {
  position: relative;
  z-index: 2;
  margin-bottom: auto;
  padding-top: calc(var(--header-h) + 0.35rem);
}

.bl-box__index {
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  color: var(--text-muted);
}

.bl-box__title {
  position: relative;
  z-index: 2;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(1.85rem, 3vw, 2.5rem);
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--text);
}

/* ---------- content (true full area of expanded section) ---------- */
.bl-content {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 5;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  overflow: hidden;
  background: var(--bg);
}

.bl-section.is-open .bl-content {
  pointer-events: auto;
}

.bl-content__inner {
  width: 100%;
  max-width: 720px;
  height: 100%;
  margin: 0;
  padding: calc(var(--header-h) + 2.5rem) clamp(1.5rem, 6vw, 5rem)
    clamp(2rem, 5vh, 3rem);
  overflow-x: hidden;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(242, 240, 234, 0.12) transparent;
  -webkit-overflow-scrolling: touch;
}

/* Work uses the full open panel width */
.bl-section[data-work-section] .bl-content__inner {
  max-width: none;
  width: 100%;
}

.bl-content__inner--contact {
  max-width: 560px;
}

.bl-close {
  position: absolute;
  top: calc(var(--header-h) + 0.5rem);
  right: clamp(1.25rem, 3vw, 2rem);
  z-index: 20;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  color: var(--text-soft);
  background: transparent;
  transition:
    color 0.25s ease,
    border-color 0.25s ease,
    background 0.25s ease;
}

.bl-close:hover {
  color: var(--text);
  border-color: rgba(242, 240, 234, 0.28);
  background: rgba(242, 240, 234, 0.04);
}

.bl-content__eyebrow {
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 1.25rem;
}

.bl-content__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(2.4rem, 5.2vw, 3.75rem);
  line-height: 1.08;
  letter-spacing: -0.025em;
  margin-bottom: 1.35rem;
  max-width: 12ch;
  color: var(--text);
}

.bl-content__title .char {
  display: inline-block;
}

.bl-content__title .word {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-bottom: 0.08em;
}

.bl-content__lead {
  font-size: clamp(1rem, 1.4vw, 1.125rem);
  font-weight: 300;
  line-height: 1.7;
  color: var(--text-soft);
  max-width: 36ch;
  margin-bottom: 2.75rem;
}

.bl-content__body {
  font-size: 0.98rem;
  font-weight: 300;
  line-height: 1.75;
  color: var(--text-muted);
  max-width: 42ch;
}

/* stats */
.bl-stats {
  display: flex;
  gap: 2.5rem;
  margin-bottom: 2.75rem;
  padding-bottom: 2rem;
  border-bottom: 1px solid var(--line);
}

.bl-stat strong {
  display: block;
  font-family: var(--serif);
  font-size: 2rem;
  font-weight: 400;
  letter-spacing: -0.02em;
  line-height: 1;
  margin-bottom: 0.4rem;
  color: var(--text);
}

.bl-stat span {
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* works — full width of open panel */
.bl-works {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0.75rem;
  width: 100%;
  max-width: none;
}

@media (max-width: 1100px) {
  .bl-works {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.bl-work-card {
  display: block;
  width: 100%;
  text-align: left;
  border: 1px solid var(--line);
  background: var(--surface);
  overflow: hidden;
  transition: border-color 0.3s ease;
}

.bl-work-card:hover {
  border-color: rgba(242, 240, 234, 0.22);
}

.bl-work-card__img {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--surface-2);
}

.bl-work-card__img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.6);
  transition: transform 0.7s var(--ease), filter 0.4s ease;
}

.bl-work-card:hover .bl-work-card__img img {
  transform: scale(1.04);
  filter: saturate(0.75);
}

.bl-work-card__info {
  padding: 0.9rem 1rem 1.05rem;
  display: grid;
  gap: 0.15rem;
}

.bl-work-card__info span {
  font-size: 0.65rem;
  letter-spacing: 0.16em;
  color: var(--accent);
}

.bl-work-card__info h3 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 1.15rem;
  letter-spacing: -0.01em;
}

.bl-work-card__info p {
  font-size: 0.78rem;
  color: var(--text-muted);
}

/* journal */
.bl-articles {
  max-width: 480px;
}

.bl-article {
  padding: 1.5rem 0;
  border-top: 1px solid var(--line);
}

.bl-article:last-child {
  border-bottom: 1px solid var(--line);
}

.bl-article time {
  display: block;
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 0.5rem;
}

.bl-article h3 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 1.35rem;
  letter-spacing: -0.015em;
  margin-bottom: 0.45rem;
}

.bl-article p {
  font-size: 0.92rem;
  font-weight: 300;
  color: var(--text-muted);
  line-height: 1.6;
}

/* contact */
.bl-contact {
  margin-bottom: 2.5rem;
  max-width: 400px;
}

.bl-contact__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: 1rem 0;
  border-top: 1px solid var(--line);
}

.bl-contact__row:last-child {
  border-bottom: 1px solid var(--line);
}

a.bl-contact__row:hover .bl-contact__value {
  color: var(--accent);
}

.bl-contact__label {
  font-size: 0.68rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.bl-contact__value {
  font-size: 0.95rem;
  font-weight: 400;
  transition: color 0.25s ease;
}

.bl-form {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.85rem;
  max-width: 480px;
}

.bl-form__field {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.bl-form__field--full {
  grid-column: 1 / -1;
}

.bl-form__field span {
  font-size: 0.65rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.bl-form__field input,
.bl-form__field textarea {
  width: 100%;
  padding: 0.85rem 0;
  border: none;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: transparent;
  outline: none;
  transition: border-color 0.25s ease;
  resize: vertical;
}

.bl-form__field input::placeholder,
.bl-form__field textarea::placeholder {
  color: rgba(242, 240, 234, 0.25);
}

.bl-form__field input:focus,
.bl-form__field textarea:focus {
  border-bottom-color: var(--accent);
}

.bl-form__submit {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 0.75rem;
  padding: 0.85rem 1.75rem;
  border: 1px solid var(--text);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bg);
  background: var(--text);
  transition:
    background 0.3s ease,
    color 0.3s ease,
    border-color 0.3s ease;
}

.bl-form__submit:hover {
  background: transparent;
  color: var(--text);
  border-color: var(--text);
}

/* ---------- panels (cover open work section) ---------- */
.bl-panels {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 50;
  pointer-events: none;
  visibility: hidden;
}

.bl-panels.is-open {
  pointer-events: auto;
  visibility: visible;
}

.bl-panels__track {
  position: absolute;
  inset: 0;
}

.bl-panel {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  background: var(--bg);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  overflow: hidden;
}

.bl-panel.is-active {
  visibility: visible;
  pointer-events: auto;
  z-index: 2;
}

.bl-panel__media {
  overflow: hidden;
  min-height: 0;
  background: var(--surface);
}

.bl-panel__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.65);
}

.bl-panel__copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: clamp(2rem, 5vw, 3.5rem);
  overflow-y: auto;
  background: var(--bg);
  border-left: 1px solid var(--line);
}

.bl-panel__num {
  font-size: 0.68rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 1.25rem;
}

.bl-panel__copy h3 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(2rem, 3.2vw, 2.75rem);
  letter-spacing: -0.02em;
  line-height: 1.1;
  margin-bottom: 1.15rem;
}

.bl-panel__copy p {
  font-size: 1rem;
  font-weight: 300;
  line-height: 1.7;
  color: var(--text-soft);
  max-width: 32ch;
}

.bl-panels__nav {
  position: absolute;
  top: calc(var(--header-h) + 0.5rem);
  right: clamp(1.25rem, 3vw, 2rem);
  z-index: 12;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  opacity: 0;
}

.bl-panels__btn {
  height: 44px;
  padding: 0 1.1rem;
  border: 1px solid var(--line);
  background: rgba(12, 12, 11, 0.7);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  font-size: 0.7rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-soft);
  transition:
    color 0.25s ease,
    border-color 0.25s ease;
}

.bl-panels__btn:hover {
  color: var(--text);
  border-color: rgba(242, 240, 234, 0.28);
}

.bl-panels__btn--close {
  width: 44px;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 50%;
}

.bl-panels__btn--next {
  border-radius: 999px;
}

/* ---------- responsive ---------- */
@media (max-width: 900px) {
  .bl-box {
    justify-content: center;
    padding: 1rem 1.25rem;
  }

  .bl-box__meta {
    position: absolute;
    top: 0.75rem;
    left: 1.25rem;
    margin: 0;
    padding: 0;
  }

  .bl-box__title {
    font-size: clamp(1.5rem, 5.5vw, 1.85rem);
  }

  .bl-works {
    grid-template-columns: 1fr 1fr;
  }

  .bl-panel {
    grid-template-columns: 1fr;
  }

  .bl-panel__media {
    max-height: 38vh;
  }

  .bl-panel__copy {
    border-left: none;
    border-top: 1px solid var(--line);
  }

  .bl-form {
    grid-template-columns: 1fr;
  }

  .bl-stats {
    gap: 1.5rem;
  }

  .site-header__hint {
    display: none;
  }
}

@media (max-width: 520px) {
  .bl-works {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
````

## Source — script.js
````javascript
/**
 * Fullscreen Columns — open / close
 * Vanilla JS + GSAP 3.14 (no jQuery)
 *
 * Columns stay position:absolute inside .bl-main.
 * Open → animate to full stage (0,0,100%,100%).
 * Close → animate back to equal column slots.
 * No !important locks fighting GSAP mid-tween.
 */
(() => {
  "use strict";

  function whenReady(fn, n) {
    n = n || 0;
    if (typeof gsap === "undefined") {
      if (n < 80) setTimeout(() => whenReady(fn, n + 1), 40);
      else console.error("[Columns] GSAP failed to load.");
      return;
    }
    fn();
  }

  whenReady(init);

  function init() {
    const plugins = [];
    if (typeof CustomEase !== "undefined") plugins.push(CustomEase);
    if (typeof SplitText !== "undefined") plugins.push(SplitText);
    if (plugins.length) gsap.registerPlugin.apply(gsap, plugins);

    gsap.config({ force3D: true, nullTargetWarn: false });
    gsap.ticker.lagSmoothing(500, 33);

    const hasCE = typeof CustomEase !== "undefined";
    if (hasCE) {
      CustomEase.create("open", "M0,0 C0.16,1 0.3,1 1,1");
      CustomEase.create("soft", "M0,0 C0.08,0.82 0.16,1 1,1");
      CustomEase.create("rise", "M0,0 C0.22,1 0.36,1 1,1");
    }

    const E_OPEN = hasCE ? "open" : "expo.out";
    const E_SOFT = hasCE ? "soft" : "power3.out";
    const E_RISE = hasCE ? "rise" : "power3.out";
    const E_OUT = "power2.inOut";

    const reduceMotion = window.matchMedia(
      "(prefers-reduced-motion: reduce)"
    ).matches;
    const mqMobile = window.matchMedia("(max-width: 900px)");

    const root = document.querySelector("[data-columns]");
    if (!root) return;

    const sections = gsap.utils.toArray("[data-section]", root);
    const count = sections.length;
    const header = document.querySelector("[data-header]");
    const workSection = root.querySelector("[data-work-section]");
    const workItems = gsap.utils.toArray("[data-work-items] [data-panel]", root);
    const panelsRoot = root.querySelector("[data-panels]");
    const panels = gsap.utils.toArray("[data-panel-item]", panelsRoot);
    const panelNext = panelsRoot?.querySelector("[data-panel-next]");
    const panelClose = panelsRoot?.querySelector("[data-panel-close]");
    const panelsNav = panelsRoot?.querySelector("[data-panels-nav]");

    let openSection = null;
    let animating = false;
    let panelOpen = false;
    let currentPanel = -1;
    let master = null;

    /* ===================== layout ===================== */

    function stage() {
      return {
        w: root.clientWidth || window.innerWidth,
        h: root.clientHeight || window.innerHeight,
      };
    }

    function homeOf(i) {
      const { w, h } = stage();
      if (mqMobile.matches) {
        const ch = h / count;
        return { top: i * ch, left: 0, width: w, height: ch };
      }
      const cw = w / count;
      return { top: 0, left: i * cw, width: cw, height: h };
    }

    function fullOf() {
      const { w, h } = stage();
      return { top: 0, left: 0, width: w, height: h };
    }

    function resetMotion(el) {
      gsap.set(el, {
        x: 0,
        y: 0,
        xPercent: 0,
        yPercent: 0,
        scale: 1,
        rotation: 0,
        transformOrigin: "50% 50%",
      });
    }

    function applyBox(el, box, extra) {
      resetMotion(el);
      gsap.set(
        el,
        Object.assign(
          {
            position: "absolute",
            top: box.top,
            left: box.left,
            width: box.width,
            height: box.height,
            right: "auto",
            bottom: "auto",
            margin: 0,
            maxWidth: "none",
            maxHeight: "none",
          },
          extra || {}
        )
      );
    }

    function layoutHome(opts) {
      const fadeIn = opts && opts.autoAlpha != null ? opts.autoAlpha : 1;
      sections.forEach((s, i) => {
        s.classList.remove("is-open", "is-expanded", "is-dimmed");
        applyBox(s, homeOf(i), {
          autoAlpha: fadeIn,
          zIndex: 1,
          clearProps: "clip,clipPath",
        });
        const box = s.querySelector("[data-box]");
        const content = s.querySelector("[data-content]");
        const closeBtn = s.querySelector("[data-close]");
        if (box) gsap.set(box, { autoAlpha: 1, clearProps: "transform" });
        if (content) {
          gsap.set(content, {
            autoAlpha: 0,
            visibility: "hidden",
            pointerEvents: "none",
          });
          content.setAttribute("aria-hidden", "true");
          if (content._split) {
            try {
              content._split.revert();
            } catch (_) {}
            content._split = null;
          }
        }
        if (closeBtn) gsap.set(closeBtn, { autoAlpha: 0, scale: 0.9 });
      });
    }

    function killMaster() {
      if (master) {
        master.kill();
        master = null;
      }
      gsap.killTweensOf(sections);
      sections.forEach((s) => {
        gsap.killTweensOf(
          s.querySelectorAll(
            "[data-box], [data-content], [data-close], .bl-box__bg img, [data-title], .bl-content__inner, .bl-work-card, .bl-stats, .bl-content__body, .bl-articles, .bl-contact, .bl-form, .bl-content__eyebrow, .bl-content__lead, .char"
          )
        );
      });
    }

    /* ===================== boot ===================== */

    layoutHome();
    gsap.set(sections, { autoAlpha: 0 });
    if (header) gsap.set(header, { autoAlpha: 0, y: -8 });

    if (panelsRoot) {
      gsap.set(panelsRoot, { autoAlpha: 0, visibility: "hidden" });
      gsap.set(panels, { yPercent: 100, autoAlpha: 0 });
      if (panelsNav) gsap.set(panelsNav, { autoAlpha: 0, y: -8 });
    }

    function playIntro() {
      if (reduceMotion) {
        gsap.set(sections, { autoAlpha: 1 });
        if (header) gsap.set(header, { autoAlpha: 1, y: 0 });
        return;
      }
      if (header) {
        gsap.to(header, {
          autoAlpha: 1,
          y: 0,
          duration: 0.65,
          ease: E_SOFT,
          delay: 0.05,
        });
      }
      gsap.to(sections, {
        autoAlpha: 1,
        duration: 0.8,
        stagger: 0.06,
        ease: E_SOFT,
        delay: 0.1,
      });
      sections.forEach((section, i) => {
        const img = section.querySelector(".bl-box__bg img");
        const title = section.querySelector("[data-title]");
        if (img) {
          gsap.fromTo(
            img,
            { scale: 1.12 },
            {
              scale: 1.06,
              duration: 1.25,
              delay: 0.08 + i * 0.05,
              ease: E_OPEN,
            }
          );
        }
        if (title) {
          gsap.fromTo(
            title,
            { y: 16, autoAlpha: 0 },
            {
              y: 0,
              autoAlpha: 1,
              duration: 0.7,
              delay: 0.22 + i * 0.06,
              ease: E_SOFT,
            }
          );
        }
      });
    }

    function setupHovers() {
      if (window.matchMedia("(pointer: coarse)").matches) return;
      sections.forEach((section) => {
        const face = section.querySelector("[data-box]");
        const img = section.querySelector(".bl-box__bg img");
        const title = section.querySelector("[data-title]");
        if (!face || !img) return;
        const qS = gsap.quickTo(img, "scale", {
          duration: 0.5,
          ease: "power2.out",
        });
        const qY = title
          ? gsap.quickTo(title, "y", { duration: 0.4, ease: "power3" })
          : null;
        face.addEventListener("pointerenter", () => {
          if (openSection || animating) return;
          qS(1.1);
          if (qY) qY(-6);
        });
        face.addEventListener("pointerleave", () => {
          qS(1.06);
          if (qY) qY(0);
        });
      });
    }

    /* ===================== content show / hide ===================== */

    function splitTitle(el) {
      if (!el || typeof SplitText === "undefined") return null;
      try {
        return SplitText.create(el, {
          type: "chars,words",
          charsClass: "char",
          wordsClass: "word",
          aria: "auto",
        });
      } catch (_) {
        return null;
      }
    }

    function showContent(section) {
      const content = section.querySelector("[data-content]");
      const closeBtn = section.querySelector("[data-close]");
      const inner = content?.querySelector(".bl-content__inner");
      const title = content?.querySelector("[data-split]");
      const eyebrow = content?.querySelector(".bl-content__eyebrow");
      const lead = content?.querySelector(".bl-content__lead");
      const blocks = content
        ? gsap.utils.toArray(
            ".bl-stats, .bl-content__body, .bl-works, .bl-articles, .bl-contact, .bl-form",
            content
          )
        : [];
      const cards = content
        ? gsap.utils.toArray(".bl-work-card", content)
        : [];

      if (!content) return gsap.timeline();

      // Clean previous split before re-opening
      if (content._split) {
        try {
          content._split.revert();
        } catch (_) {}
        content._split = null;
      }

      content.setAttribute("aria-hidden", "false");
      gsap.set(content, {
        visibility: "visible",
        pointerEvents: "auto",
        autoAlpha: 0,
      });
      if (inner) inner.scrollTop = 0;

      const t = gsap.timeline({ defaults: { ease: E_SOFT } });

      t.to(content, { autoAlpha: 1, duration: 0.35 }, 0);

      if (closeBtn) {
        t.fromTo(
          closeBtn,
          { autoAlpha: 0, scale: 0.85 },
          { autoAlpha: 1, scale: 1, duration: 0.4, ease: E_OPEN },
          0.1
        );
      }

      if (eyebrow) {
        t.fromTo(
          eyebrow,
          { y: 12, autoAlpha: 0 },
          { y: 0, autoAlpha: 1, duration: 0.4 },
          0.08
        );
      }

      const split = splitTitle(title);
      if (split?.chars?.length) {
        gsap.set(split.chars, { yPercent: 110, autoAlpha: 0 });
        t.to(
          split.chars,
          {
            yPercent: 0,
            autoAlpha: 1,
            duration: 0.65,
            stagger: 0.012,
            ease: E_OPEN,
          },
          0.1
        );
        content._split = split;
      } else if (title) {
        t.fromTo(
          title,
          { y: 18, autoAlpha: 0 },
          { y: 0, autoAlpha: 1, duration: 0.55 },
          0.1
        );
      }

      if (lead) {
        t.fromTo(
          lead,
          { y: 14, autoAlpha: 0 },
          { y: 0, autoAlpha: 1, duration: 0.5 },
          0.22
        );
      }

      if (blocks.length) {
        t.fromTo(
          blocks,
          { y: 16, autoAlpha: 0 },
          { y: 0, autoAlpha: 1, duration: 0.5, stagger: 0.04 },
          0.28
        );
      }

      if (cards.length) {
        t.fromTo(
          cards,
          { y: 18, autoAlpha: 0 },
          {
            y: 0,
            autoAlpha: 1,
            duration: 0.5,
            stagger: 0.05,
            ease: E_OPEN,
          },
          0.3
        );
      }

      return t;
    }

    function hideContent(section) {
      const content = section.querySelector("[data-content]");
      const closeBtn = section.querySelector("[data-close]");
      if (!content) return gsap.timeline();

      const t = gsap.timeline({
        onComplete: () => {
          content.setAttribute("aria-hidden", "true");
          gsap.set(content, {
            autoAlpha: 0,
            visibility: "hidden",
            pointerEvents: "none",
          });
          if (content._split) {
            try {
              content._split.revert();
            } catch (_) {}
            content._split = null;
          }
        },
      });

      if (closeBtn) {
        t.to(closeBtn, { autoAlpha: 0, scale: 0.9, duration: 0.18 }, 0);
      }
      t.to(content, { autoAlpha: 0, duration: 0.2, ease: "power2.in" }, 0);
      return t;
    }

    /* ===================== OPEN ===================== */

    function openCol(section) {
      if (!section) return;
      if (openSection === section && !animating) return;

      // Switch: close current first, then open target
      if (openSection && openSection !== section) {
        closeCol(openSection, () => openCol(section));
        return;
      }

      killMaster();
      animating = true;
      openSection = section;
      document.body.classList.add("is-expanded");

      const idx = sections.indexOf(section);
      const face = section.querySelector("[data-box]");
      const others = sections.filter((s) => s !== section);
      const from = homeOf(idx);
      const to = fullOf();

      // Pin every column to a clean home slot (absolute, no residual fixed/lock)
      sections.forEach((s, i) => {
        s.classList.remove("is-open", "is-expanded", "is-dimmed");
        applyBox(s, homeOf(i), {
          autoAlpha: 1,
          zIndex: s === section ? 20 : 1,
        });
      });

      // Active starts at its column
      applyBox(section, from, { autoAlpha: 1, zIndex: 30 });
      section.classList.add("is-expanded");
      others.forEach((s) => s.classList.add("is-dimmed"));

      if (face) {
        face.setAttribute("aria-expanded", "true");
        face.setAttribute("tabindex", "-1");
        gsap.set(face, { autoAlpha: 1 });
      }

      if (reduceMotion) {
        applyBox(section, to, { autoAlpha: 1, zIndex: 30 });
        section.classList.add("is-open");
        gsap.set(others, { autoAlpha: 0 });
        if (face) gsap.set(face, { autoAlpha: 0 });
        showContent(section);
        animating = false;
        section.querySelector("[data-close]")?.focus({ preventScroll: true });
        return;
      }

      master = gsap.timeline({
        defaults: { overwrite: "auto" },
        onComplete: () => {
          // Final full-stage snap (clean numbers, no CSS !important)
          applyBox(section, fullOf(), { autoAlpha: 1, zIndex: 30 });
          section.classList.add("is-open");
          animating = false;
          section.querySelector("[data-close]")?.focus({ preventScroll: true });
        },
      });

      // 1) Expand column to full stage
      master.to(
        section,
        {
          top: to.top,
          left: to.left,
          width: to.width,
          height: to.height,
          duration: 0.9,
          ease: E_OPEN,
        },
        0
      );

      // 2) Fade siblings out
      master.to(
        others,
        {
          autoAlpha: 0,
          duration: 0.4,
          ease: E_OUT,
          stagger: 0.02,
        },
        0.02
      );

      // 3) Hide column face
      if (face) {
        master.to(
          face,
          { autoAlpha: 0, duration: 0.25, ease: "power2.in" },
          0.2
        );
      }

      // 4) Content in after layout is mostly open
      master.add(() => section.classList.add("is-open"), 0.35);
      master.add(showContent(section), 0.38);
    }

    /* ===================== CLOSE ===================== */

    function closeCol(section, onDone) {
      if (!section) {
        if (typeof onDone === "function") onDone();
        return;
      }

      if (panelOpen) {
        closePanel(() => closeCol(section, onDone));
        return;
      }

      killMaster();
      animating = true;

      const idx = sections.indexOf(section);
      const face = section.querySelector("[data-box]");
      const others = sections.filter((s) => s !== section);
      const home = homeOf(idx);
      const full = fullOf();

      // Start from full stage (absolute), strip open-only classes that don't lock size
      section.classList.remove("is-open");
      applyBox(section, full, { autoAlpha: 1, zIndex: 30 });

      // Siblings ready at home, invisible
      others.forEach((s) => {
        const oi = sections.indexOf(s);
        applyBox(s, homeOf(oi), { autoAlpha: 0, zIndex: 1 });
      });

      if (reduceMotion) {
        hideContent(section);
        layoutHome();
        document.body.classList.remove("is-expanded");
        openSection = null;
        animating = false;
        if (face) {
          face.setAttribute("aria-expanded", "false");
          face.setAttribute("tabindex", "0");
          face.focus({ preventScroll: true });
        }
        if (typeof onDone === "function") onDone();
        return;
      }

      master = gsap.timeline({
        defaults: { overwrite: "auto" },
        onComplete: () => {
          layoutHome();
          document.body.classList.remove("is-expanded");
          openSection = null;
          animating = false;
          if (face) {
            face.setAttribute("aria-expanded", "false");
            face.setAttribute("tabindex", "0");
            face.focus({ preventScroll: true });
          }
          if (typeof onDone === "function") onDone();
        },
      });

      // 1) Content out quickly
      master.add(hideContent(section), 0);

      // 2) Face back
      if (face) {
        master.to(
          face,
          { autoAlpha: 1, duration: 0.28, ease: E_SOFT },
          0.08
        );
      }

      // 3) Shrink active column to home slot
      master.to(
        section,
        {
          top: home.top,
          left: home.left,
          width: home.width,
          height: home.height,
          duration: 0.85,
          ease: E_OPEN,
        },
        0.06
      );

      // 4) Siblings fade in
      master.to(
        others,
        {
          autoAlpha: 1,
          duration: 0.5,
          ease: E_SOFT,
          stagger: 0.03,
        },
        0.2
      );
    }

    /* ===================== work panels ===================== */

    function openPanel(index) {
      if (!panelsRoot || animating || !panels.length || panelOpen) return;
      index = gsap.utils.clamp(0, panels.length - 1, index);
      animating = true;
      panelOpen = true;
      currentPanel = index;

      // Keep work section full while overlay opens
      if (workSection && openSection === workSection) {
        applyBox(workSection, fullOf(), { autoAlpha: 1, zIndex: 30 });
        workSection.classList.add("is-open", "is-expanded");
      }

      panelsRoot.classList.add("is-open");
      panelsRoot.setAttribute("aria-hidden", "false");
      gsap.set(panelsRoot, {
        position: "absolute",
        inset: 0,
        width: "100%",
        height: "100%",
        zIndex: 50,
        visibility: "visible",
      });

      panels.forEach((p, i) => p.classList.toggle("is-active", i === index));

      const panel = panels[index];
      const media = panel.querySelector(".bl-panel__media img");
      const copyKids = panel.querySelector(".bl-panel__copy")
        ? gsap.utils.toArray(panel.querySelector(".bl-panel__copy").children)
        : [];

      gsap.set(panels, { yPercent: 100, autoAlpha: 0 });
      gsap.set(panel, { yPercent: 100, autoAlpha: 1, zIndex: 2 });
      if (media) gsap.set(media, { scale: 1.05 });
      if (copyKids.length) gsap.set(copyKids, { y: 14, autoAlpha: 0 });
      if (panelsNav) gsap.set(panelsNav, { autoAlpha: 0, y: -6 });

      if (reduceMotion) {
        gsap.set(panelsRoot, { autoAlpha: 1 });
        gsap.set(panel, { yPercent: 0 });
        if (panelsNav) gsap.set(panelsNav, { autoAlpha: 1, y: 0 });
        animating = false;
        return;
      }

      const t = gsap.timeline({
        onComplete: () => {
          animating = false;
          panelClose?.focus({ preventScroll: true });
        },
      });

      const workContent = workSection?.querySelector("[data-content]");
      if (workContent) {
        t.to(workContent, { autoAlpha: 0.2, duration: 0.4, ease: E_OUT }, 0);
      }

      t.to(panelsRoot, { autoAlpha: 1, duration: 0.15 }, 0);
      t.to(panel, { yPercent: 0, duration: 0.8, ease: E_RISE }, 0.04);
      if (media) t.to(media, { scale: 1, duration: 0.95, ease: E_OPEN }, 0.06);
      if (copyKids.length) {
        t.to(
          copyKids,
          { y: 0, autoAlpha: 1, duration: 0.45, stagger: 0.05, ease: E_SOFT },
          0.28
        );
      }
      if (panelsNav) {
        t.to(panelsNav, { autoAlpha: 1, y: 0, duration: 0.35, ease: E_SOFT }, 0.35);
      }
    }

    function goNextPanel() {
      if (!panelOpen || animating || !panels.length) return;
      animating = true;

      const from = panels[currentPanel];
      const nextIndex = (currentPanel + 1) % panels.length;
      const to = panels[nextIndex];
      const toKids = to.querySelector(".bl-panel__copy")
        ? gsap.utils.toArray(to.querySelector(".bl-panel__copy").children)
        : [];
      const toMedia = to.querySelector(".bl-panel__media img");

      to.classList.add("is-active");
      gsap.set(to, { yPercent: 100, autoAlpha: 1, zIndex: 3 });
      if (toKids.length) gsap.set(toKids, { y: 12, autoAlpha: 0 });
      if (toMedia) gsap.set(toMedia, { scale: 1.04 });

      const t = gsap.timeline({
        onComplete: () => {
          from.classList.remove("is-active");
          gsap.set(from, { yPercent: 100, autoAlpha: 0, zIndex: 1 });
          gsap.set(to, { zIndex: 2 });
          currentPanel = nextIndex;
          animating = false;
        },
      });

      t.to(from, { autoAlpha: 0, duration: 0.35, ease: E_OUT }, 0);
      t.to(to, { yPercent: 0, duration: 0.75, ease: E_RISE }, 0.06);
      if (toMedia) t.to(toMedia, { scale: 1, duration: 0.9, ease: E_OPEN }, 0.08);
      if (toKids.length) {
        t.to(
          toKids,
          { y: 0, autoAlpha: 1, duration: 0.4, stagger: 0.04, ease: E_SOFT },
          0.28
        );
      }
    }

    function closePanel(onDone) {
      if (!panelOpen || !panelsRoot) {
        if (typeof onDone === "function") onDone();
        return;
      }

      animating = true;
      const panel = panels[currentPanel];
      const workContent = workSection?.querySelector("[data-content]");

      const t = gsap.timeline({
        onComplete: () => {
          panelsRoot.classList.remove("is-open");
          panelsRoot.setAttribute("aria-hidden", "true");
          gsap.set(panelsRoot, {
            visibility: "hidden",
            autoAlpha: 0,
          });
          gsap.set(panels, { yPercent: 100, autoAlpha: 0 });
          panels.forEach((p) => p.classList.remove("is-active"));
          panelOpen = false;
          currentPanel = -1;

          if (workSection && openSection === workSection) {
            applyBox(workSection, fullOf(), { autoAlpha: 1, zIndex: 30 });
          }

          animating = false;
          if (typeof onDone === "function") onDone();
        },
      });

      if (panelsNav) t.to(panelsNav, { autoAlpha: 0, duration: 0.18 }, 0);
      if (panel) {
        t.to(panel, { yPercent: 100, duration: 0.55, ease: E_OUT }, 0.03);
      }
      if (workContent) {
        t.to(workContent, { autoAlpha: 1, duration: 0.45, ease: E_SOFT }, 0.1);
      }
      t.to(panelsRoot, { autoAlpha: 0, duration: 0.18 }, 0.32);
    }

    /* ===================== events ===================== */

    sections.forEach((section) => {
      const face = section.querySelector("[data-box]");
      const closeBtn = section.querySelector("[data-close]");

      face?.addEventListener("click", (e) => {
        e.preventDefault();
        if (animating) return;
        if (openSection === section) return;
        openCol(section);
      });

      face?.addEventListener("keydown", (e) => {
        if (e.key !== "Enter" && e.key !== " ") return;
        e.preventDefault();
        if (animating) return;
        if (openSection === section) return;
        openCol(section);
      });

      // Close always allowed — interrupts open animation cleanly
      closeBtn?.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        if (!openSection) return;
        closeCol(openSection);
      });
    });

    workItems.forEach((item) => {
      const btn = item.querySelector("button") || item;
      const idx = parseInt(item.dataset.panel, 10);
      btn.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();
        if (animating) return;
        openPanel(idx);
      });
    });

    panelNext?.addEventListener("click", (e) => {
      e.preventDefault();
      goNextPanel();
    });

    panelClose?.addEventListener("click", (e) => {
      e.preventDefault();
      closePanel();
    });

    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape") {
        if (panelOpen) {
          e.preventDefault();
          closePanel();
        } else if (openSection) {
          e.preventDefault();
          closeCol(openSection);
        }
      }
      if (panelOpen && (e.key === "ArrowRight" || e.key === "ArrowDown")) {
        e.preventDefault();
        goNextPanel();
      }
    });

    let resizeRaf = 0;
    window.addEventListener("resize", () => {
      cancelAnimationFrame(resizeRaf);
      resizeRaf = requestAnimationFrame(() => {
        if (animating) return;
        if (openSection) {
          applyBox(openSection, fullOf(), { autoAlpha: 1, zIndex: 30 });
          sections.forEach((s, i) => {
            if (s === openSection) return;
            applyBox(s, homeOf(i), { autoAlpha: 0, zIndex: 1 });
          });
        } else {
          layoutHome();
        }
      });
    });

    const form = root.querySelector("[data-form]");
    form?.addEventListener("submit", (e) => {
      e.preventDefault();
      const btn = form.querySelector(".bl-form__submit");
      if (!btn) return;
      const prev = btn.textContent;
      btn.textContent = "Sent";
      setTimeout(() => {
        btn.textContent = prev;
        form.reset();
      }, 1800);
    });

    document.addEventListener("visibilitychange", () => {
      if (document.hidden) gsap.globalTimeline.pause();
      else gsap.globalTimeline.resume();
    });

    playIntro();
    setupHovers();
  }
})();
````
