TL;DR

  • 社内 AI ワークショップ用の3つのデモページ(お菓子店・美容室・飲食店)を 12 列モジュラーグリッドに乗せ替えた
  • 最初はヒーローだけ subgrid で「整数比だから乗ってるはず」と目視で済ませた → ユーザーから「写真の高さが合ってない」と突き返された
  • 検証ハーネスを走らせたら product-row で 16px、info-grid で 20px ズレていたgapvar(--gutter) に揃え忘れていた
  • 根本原因は3つ重なっていた: grid-template-columns1fr 問題box-sizing が content-box のままtranslateY の装飾が残骸として効いていた
  • 最終的に 3 ページ全要素 0.03px 以下まで詰めて、固定ナビ追加とサイドバー削除も同じ流れで整えた

きっかけ: 「グリッドのスキル使ってちょっと整えて」

朝、社内 AI ワークショップ用のデモページ3本のグリッドレイアウトを整える依頼が飛んできた。お菓子店・美容室・飲食店の3業種で、それぞれ独自のテーマ色とフォントを当ててある。

このお菓子のページとかデモページ作ってるんですけど、あと美容室と飲食店の3つですね。今日追加したグリッドデザインのスキルを使って、いい感じにグリッドレイアウトで整えてくれませんか。

今日追加した muller-brockmann-grid-systems スキルを当てる初の実戦。色やフォントは各ページの世界観として残し、骨格だけ 12 列モジュラーグリッドに乗せ替える方針で進めることにした。


序盤: 既存の grid-system.css に乗っかる

ディレクトリを見ると、すでに grid-system.cssuseGridOverlay.ts が用意されていた。誰も使っていない状態のスキャフォールド。自分で作りかけた tsukumi-grid.css を削除して、既存資産に乗ることにした。

最初の作業はこの順番で進めた。

  1. nuxt.config.tscss: を追加して grid-system.css を読み込ませる
  2. 各ページのルート要素に .gs-wrap .gs-grid を当てる
  3. ヒーローセクションだけ subgrid 化して、タイトルを col 1-5、写真を col 6-12 に並べる

ここまではスムーズに動いた。グリッドオーバーレイを当てるとコラム1-5にタイトル、コラム6-12に写真がきれいに乗る。「いい感じだな」と思いながら、3ページとも同じ手順で進めて報告を投げた。


ユーザーからの差し戻し: 「ちゃんと縦とか横とか揃えてくれていますか?」

報告を投げた直後、ユーザーから返ってきた。

ごめん、待って。ちゃんと縦とか横とか揃えてくれていますか?スキルの検証ハーネス使って、ちゃんと確認してくれていますかね。

ここで手が止まった。正直に言うとヒーローセクションだけ subgrid で 0px 一致を確認していて、それ以外(ギフトセクション、商品行、料理行、店舗情報、FAQ など)は「整数比で目視で乗ってるはず」というレベルで放置していた。スキルが用意している verify ハーネスは一度も回していなかった。

謝罪して、検証ハーネスを全セクションで走らせることにした。


verify を回したら 16px と 20px ズレていた

検証スクリプトで全主要グリッド要素の左右と基準コラム line の差分を測る。結果はすぐ出た。

  • お菓子店ページの .product-row16px ズレ
  • 美容室ページの .info-grid20px ズレ

原因は単純で、内部の gapvar(--gutter)(24px)になっていなかった。6:6 の対称分割でも、内部 gap が外側の 24px と違うとコラム line と一致しない。整数比で乗っているように見えても、ピクセル単位では合っていなかった。

ここで作業を切り替えた。

  • 非対称分割(7:5 など)は素直に subgrid 化する
  • 対称分割は gap を var(--gutter) に揃える
  • フルブリードのセクションは内側ラッパーを追加して、その中でグリッドを継続する

写真の高さが揃わない: 「これなんで写真の高さが合ってないんですか?」

verify が通ったと報告したら、ユーザーからスクリーンショットが飛んできた。

ごめん、だからマジでびっくりするんですけど、これなんで写真の高さが合ってないんですか?

美容室ページのスタイルカードが上下にガタついていた。verify は左右の leftright しか測っていなかったから、縦のズレを見落としていた。

ここでバグが3つ重なっていることが判明した。

原因1: translateY の装飾が残っていた

.style-card:nth-child(even) { transform: translateY(...) } で偶数番目のカードを意図的に下にずらすデザインが残っていた。グリッドに乗せたあと、これを消し忘れていた。さらに .style-card.reveal { translate: -28px 0 } の横スライドインが viewport 外で残っていて、初期状態のままだとカードが 28px 横にズレた状態で居座っていた。

縦スライドに変えて、is-visible 前でも横ズレが起きないように修正。

原因2: grid-template-columns: repeat(12, 1fr)1fr が拡張されていた

これが一番ハマった。1fr は内部的に minmax(auto, 1fr) として扱われる。子要素の min-content が大きいと、その列だけ拡張される。「スタイルと店内」の h2 が2段に折れていたのもこの影響で、box が狭くなって h2 の min-content と衝突していた。

minmax(0, 1fr) に変えて、列幅が子要素に引っ張られないように固定。

原因3: box-sizing が content-box のまま

最大の地雷。プロジェクト全体に * { box-sizing: border-box } が当たっていなかった。.gs-wrap の中の要素はすべて content-box で計算され、padding を足すと box width が想定より大きくなる。.steps-section の box が 1703px になっていたのはこれが原因。

.gs-wrap 配下を border-box に統一して、ようやく計算と実測が一致するようになった。

修正後に再 verify を走らせて、お菓子店・美容室・飲食店すべて 0.03px 以下(事実上 0px)まで詰まった。


縦に並んでしまう: subgrid の継承が切れていた

ユーザーから次の指摘が飛んできた。

ここなんですが、レイアウトが普通に縦に並んでしまい、見づらいですね。

お菓子店ページを開くと、商品カードが横並びにならず縦1列で表示されていた。subgrid を指定しているのに2列扱いになっている。

詳細を追うと、.product-list の親 .menu-section が block レイアウトだった。subgrid は親が grid container でないと継承できない。途中に block 要素が挟まると、そこで subgrid のチェーンが切れて単独 grid 扱いになる。

修正は素直だった。

  • .menu-section を grid 化(.gs-grid を当てる)
  • 美容室ページの .info-section、飲食店ページの .access-section も同じ問題があったので、まとめて grid 化
  • 逆に .access-inner は自分が独立 12 列 grid container だったため、subgrid 指定を追加していたのが裏目に出ていた。subgrid 指定を消して元に戻す

3 ページとも再 verify を通して、横並びが復活した。


固定ナビとサイドバー削除

最後に2つ整えた。

各ページに固定ナビを追加

3 ページとも、上部に固定ナビを追加した。お菓子店ページなら「ギフト / 商品 / 店舗情報 / FAQ」のように、ページ内アンカーで section にジャンプできるようにした。scroll-margin-top を 88px ほど確保して、ナビ下にちゃんと余白が残るように調整。

dev server が途中で落ちて再起動したり、リビルドで数十秒待たされたりしたが、3 ページとも #gift などに top=87.875 で固定されてスクロールするのを確認できた。

index ページのサイドバー TOC を削除

index ページ(/tsukumi-ai-workshop 自体)にはスクショ付きで「ここはいらない」と指摘されたサイドバーの目次があった。

スクリーンショット 今回ここはいらないですね。

サイドバー TOC を消して、代わりに上部中央に他ページと同じ固定ナビ(チラシ文面 / 当日の進行 / 成果物 / プロンプト / 運用)を追加。メインコンテンツを 12 列フル幅に拡張して、メトリクスカードを col 1-3 / 4-6 / 7-9 / 10-12 でちょうど 4 等分に並ぶように組み直した。


学び

verify ハーネスを最初から回す

直接の敗因は、スキルが用意している verify ハーネスを最初から回さず「目視と理屈で済ませた」こと。ヒーローだけ subgrid で 0px 一致を確認した時点で「他も整数比だから乗ってるはず」と判断してしまった。実際は 16px と 20px ズレていた。

「整数比で計算上は乗っているはず」は、gap の値が違うだけで一発で破綻する。スキル側に検証ハーネスが用意されているなら、最初から全要素で走らせるのが正解だった。

1fr ではなく minmax(0, 1fr) を使う

grid-template-columns: repeat(12, 1fr) は内部的に minmax(auto, 1fr) 扱いになり、子要素の min-content で列幅が拡張される。Müller-Brockmann グリッドのように 12 列を厳密に守りたいときは、最初から minmax(0, 1fr) で書くのが安全。

box-sizing は最初に統一する

プロジェクトに * { box-sizing: border-box } が当たっていない状態でグリッドに乗せると、padding 付きの要素が想定より大きくなって計算が合わなくなる。グリッドシステムを導入する最初のステップで、ラッパー配下を border-box に統一しておくと後段の verify で詰まらない。

subgrid の継承は親の display で切れる

subgrid を効かせたいときは、上位の grid container から子孫まで全部 grid container でつないでおく必要がある。途中に block の section を挟むとそこで継承が切れ、子の subgrid 指定は単独 grid container として扱われる。今回の .menu-section.product-list の縦並びはこれが原因だった。

装飾の translate は viewport 外で残ることがある

スクロール演出で translate: -28px 0 のような初期状態を持たせていると、is-visible 前のカードが横にズレた状態で居座る。verify が「初期状態」を測定するときに、装飾が verify を汚すケースがある。グリッド規律を厳密に守るページでは、初期状態が viewport 内に入らない設計にしておくか、縦方向のスライドに切り替える。


残タスク

  • 同じグリッドシステムを /tsukumi-ai-workshop 配下の他のページにも展開する
  • verify ハーネスを CI で走らせて、リグレッションを自動検知できるようにする
  • minmax(0, 1fr)grid-system.css のデフォルトに昇格させる