🤖 AI運用ルール

learning

最終更新 2026年08月24日 / 22_AI運用ルール/コマンド集/section/learning.md

/section 学習ログ

/section(TERASU Labセクション追加)で得た知見・大串FB・うまくいったパターンを溜める。
同じFBが2回出たら section.md 本体に昇格。

構築メモ(2026-06-29・初版)

  • 目的: 参考HPの好きな箇所をTERASUオリジナル化して、HP制作の有無に関係なく都度Labセクションに足せる体制。
  • 体制の土台(lab-terasu内):
  • tools/add_section.py … メタentries/<id>.jsonを規約通り生成(img/media/snippet自動補完・新種別は警告のみで許可)
  • tools/shoot.sh … Chrome headlessでimg/sections/<id>.pngを自動生成(手撮り撲滅)。--virtual-time-budget=3000で数秒。playwright不要。
  • build.py(既存)… entries/*.json → sections-data.json
  • スクショ確定コマンド: Chrome --headless=new --hide-scrollbars --virtual-time-budget=3000 --window-size=1440,H --screenshot=OUT file://...
  • プロファイル分離(--user-data-dir 新規)は初回起動でハングするので使わない
  • macにtimeoutコマンドは無い(Bashツール側のtimeoutで囲む)。ImageMagick無し(余白トリム不可・固定高で運用)。
  • データ単位: 1セクション=entries/<id>.json + code-previews/<id>.html + img/sections/<id>.png(+任意.mp4)。1件1ファイルで同時編集競合ゼロ。
  • snippetパスは拡張子なしcode-previews/<id>)が正。Cloudflare Pagesが.htmlを自動補完して200。.html付きは308。

見つかった既存の問題(2026-06-29・要対応メモ)

  • code-previews/cafe-fv.html中身がLabトップ(index)のコピーになっている=データ破損(head は petomo導線カードのコメント・bodyはHP Production Reference/TERASU LAB/section.sites)。entries cafe-fv のプレビューが別物になる。後日要修正。

重大な失敗と教訓(2026-06-29・コマンド設計を決定づけた)

失敗 = go-spiral.ai のFVを「動画から寄せて自作」して2回却下された

  • やったこと(NG): 大串が動画+URLを送ったのに、私は動画を見てそれっぽいコードを自作(位置依存モーフィングのマーキーをCSS/JSで実装)。1回目「全然違う」→ 2回目も手描き枠等で寄せたが「全く同じか?」「寄せるじゃ全く意味がない」と却下。最終的にこの自作セクション(flowmorph-fv)は削除。
  • 大串FB(恒久・最重要):
  • 完全再現じゃなくて寄せるじゃ全く意味がない
  • 「元のHP送ってるんだから、そこからコードを抜き取ってオリジナル化しないと」
  • 一回コードをそのまま完全再現、問題ないってなってからTERASUオリジナルコード化の流れが良い」(= hp-flowと同じ2段階+確認)
  • 教訓 → コマンドをv2に全面改訂:
    1. 「寄せる」自作は禁止。必ず該当箇所の実コード(レンダリング後DOM+CSS+アセット)を抽出して土台にする。
    2. 完全再現を先・確認①を取ってからオリジナル化(hp-flow Step1→Step4と同じ)。
    3. 抽出できない実装は最初に弾く:今回の go-spiral.ai は Studio Design(ノーコード)製で、流れるFVは<canvas>にJS描画=HTML/CSSとして抜き出せない=完全再現不可。こういう箇所はStep0で判定し「canvas描画で完全再現できない・別箇所にするか相談」と正直に止める。勝手に近似で進めない

抽出の技術メモ

  • レンダリング後DOM: Chrome --headless=new --virtual-time-budget=6000 --dump-dom URL > dom.html
  • 実装方式の見分け: DOMに該当の構造があり対応CSSがある→抽出可。<canvas>/WebGLしか無い→抽出不可(go-spiral.aiが該当)。
  • Nuxt/Next製はCSSが _nuxt/*.css _next/*.css に集約。curlで取得して該当セレクタを拾う。
  • 動画は補助(動きの確認用)。ffmpeg -i mov -vf fps=2,scale=1000:-1 frames/g_%02d.png。ただし動画だけで自作するのは禁止(上記失敗の通り)。

大串FB / 学び

2026-07-15 chillnn 横スクロールカードカルーセル追加で得た知見(重要4点)

1. section.md の手順書が古い(tools/add_section.py・shoot.sh は現存しない)
- 現行運用の正本=~/dev/lab-terasu/README_運用.md(2026-06-15 git移行)。
- 現行フロー: entries/<id>.json を新規作成 + img/sections/<id>.png(motionは.mp4も)+ code-previews/<id>.html を配置 → git add/commit/pushGitHub Actions が自動で build.py 実行→Cloudflare Pages デプロイ(約30-60秒)。ローカルで build.py を回す必要なし。tools/add_section.py tools/shoot.sh python3 build.py(手動)は使わない。
- entry schema: { id, cats[], img, url, caption, snippet, author, created_at }contributorはビルド未参照=入れても無害だが評価軸は現状authorで見る)。snippet"code-previews/<id>"(拡張子なし)。
- push衝突時: 未ステージ変更(.labserve.log/index.html等・自分の変更でない)があると pull --rebase が弾かれる → git stash push -u で退避してrebase→push→pop。remoteに.claude/launch.jsonがあってuntracked衝突する場合は一旦mv退避。

2. 実サイトが「SSR版」と「ハイドレーション版」で別実装のことがある(最重要)
- chillnnは Astro + Svelte islands。chrome --headless --dump-dom で取れたDOMは古いSSR版svelte-9ml990スコープ・CSSがJS注入で静的ファイルに無い)だった。
- だがユーザーが実際に見ているライブ版は Tailwind v4 ベースtext-zinc-900 w-(--width-card-featured) text-2xs等)で完全に別物。スクショはライブ版と一致。
- 教訓: headless dump-dom を鵜呑みにしない。in-appブラウザ(Claude Browser)でライブDOM+getComputedStyledocument.styleSheetsを正とする。スクショと照合してどちらの実装か必ず確認。
- Tailwind製サイトの忠実再現: ①実HTML(utilityクラス付き)をverbatim取得 ②getComputedStyleで全要素の実算出値を採取(カード幅/gap/font/color/radius/shadow等)③カスタムクラス(.card-img-hover=scale(1.03)/cubic-bezier(.23,1,.32,1) 等)は外部CSS(/_astro/*.css)から実ルールを抜く ④それらをt-<prefix>-*スコープCSSに実値で書く(=コンパイル済みTailwindと同値=寄せではない)。

3. font-family:inherit は Lab本番で明朝化する(貼り先ラッパーが明朝base)→ゴシック原本は明示指定
- Labの既存例は font-family:inherit(貼り先に馴染ませる思想)だが、code-preview単体表示だとLabラッパーの明朝を継承して原本と別物になる。
- chillnnはNoto Sans JP(ゴシック)→ font-family:"Noto Sans JP","Hiragino Kaku Gothic ProN",...,sans-serif を明示。原本が明確にゴシック/明朝の時はfont-familyを固定する(inheritにしない)。ローカル確認では自分のラッパーが正しいフォントを当てるので気づけない→必ず本番URLでも目視する。

4. JS駆動カルーセルは実サイトと同一ライブラリをCDN自己完結で使う
- chillnnのカルーセル=Embla CarouselSectionCarousel.E8hT4uz3.js内。options: align:'start', containScroll:'trimSnaps', slidesToScroll:1 + WheelGesturesプラグイン、slides=[data-carousel-slide])。矢印はcanScrollPrev/Nextがtrueの時だけ描画(先頭では「前へ」非表示)、lucide chevron、size-9(36px)丸ボタン。
- 再現=Embla UMDをjsdelivr CDNで読み、同一optionsで初期化。矢印はemblaApi.scrollPrev/NextcanScrollPrev/Next--offトグル。GSAP/Splideと同じ「JS-heavyは自己完結CDN」パターン(creo-section-jsheavy-techniqueと同型)。

5. /tmp は途中で消える環境がある → 作業ファイルはスクラッチパッド(session固有dir)に置く。minified DOMは改行ゼロの巨大1行なので grep -o/cat が壊れる→Pythonでパースしてファイル出力→TaskOutput --filter か Read で読む。

2026-07-19 大串FB: ダミー化しすぎで「内容が違う」(skewbanner/isomurass案件)

  • 事象: isomurass.jp の斜めバナーを再現。デザイン/レイアウト/アニメは実物と完全一致していたが、タグ・英字装飾・見出しをダミー語(サービス情報/SERVICE 等)に変えたら「再現できてない・内容が違う」とFB。
  • 学び: 斜めバナー等「英字大型装飾+タグラベル」が意匠の核のセクションは、タグ(技術・製品情報/保有設備)や英字装飾(TECHNICAL/FACILITY)、ボタンラベル、見出しの行構成を元のまま/元に沿って残す。逐語コピーの本文だけダミー化。ラベルまで別語に変えると「別物」に見える。
  • 切り分け手法: 実サイトがWAF(NinjaFirewall)やBrowserペイン白飛びで撮れない時は、元テキストそのままの忠実版をローカルに置きChrome headlessで撮影→動画フレーム(実物)と照合。デザイン一致を確認できれば「違い=文言」と特定でき、ラリー最小化。
  • 付随Tips: isomurassは --dump-dom が403、curl -A "Mozilla/5.0" は通る。GSAP scroll_view は is-show 付与で発火。CSSインライン時は相対 url(../images/) を絶対URLへ sed 補正必須。scroll_view初期opacity0→ html.reveal-all で最終状態即表示にしサムネ薄飛び防止。

2026-07-19 斜めバナー(skewbanner)の高さ制御・コンパクト化の型

  • 問題: 斜めバナーで「画像が縦長アスペクト側だと、画像が grid-row:1/3 で高さを支配し、テキスト下に青い余白が大量に残って高く見える」。max-height/固定remでは、ルートフォントが可変(vwで変化)のため幅により文字が見切れる/余白が出るで破綻。
  • 正解の型: 画像を position:absolute; top/bottom:0; width:56% にして高さ計算から外す→カード高さはテキスト(inner)依存になり、余白ゼロ・文字切れゼロで全幅安定。画像は object-fit:cover で斜めPNGごと収まる(斜めは画像PNGの透明カットが正体)。
  • 2枚を同じ高さに揃える: テキスト行数を合わせる(本文長を近づける+狭い側の列 padding-left を広げて行数を減らす)。高さ調整は .tsbanner--inner の padding-top/bottom で均等に増減(例:2→3.6→5rem)。
  • 付随: Labプレビューの登場アニメは scroll_view+GSAP、キャプチャ薄飛び防止に 2.5s で最終状態を setProperty(...'important') で確定。装飾(縦英字 deco--item / 大英字 deco_en / タグ tsbanner--tag)は要素ごと削除で対応。

2026-07-22 モードB(画像から100%再現)追加 — 実測で判明した知見

大串の要望「写真を送って、それと全く同じものをオリジナルコードで作りたい」に対応。
実装前に採点エンジンを実データで検証し、設計の前提が3つ覆ったので記録する。

実測1|フォント違いが再現度の最大の支配要因

同一レイアウトで書体だけ変えて比較したところ、輪郭スコア(edge_match)が
1.0 → 0.16 まで落ちた。他の指標(SSIM 0.97 / 一致率 0.99)はほぼ落ちないため、
SSIMだけ見ていると「フォントが違う」ことに気づけない

輪郭スコアは書体一致のセンサーとして使えると判明。これを利用して
reproduce_loop.sh --font-sweep で候補10種を総当たりする仕組みにした。
検証: 正解Antonを 輪郭1.0で一発特定(他候補は全て0.04〜0.09。判別力に10倍以上の差)。
フォントは目で見て決めない。機械に当てさせる。

実測2|SSIM単独では4pxのレイアウトズレを見逃す

画像を4px下にずらして採点したら SSIM 0.9637 でPASSしてしまった
余白(単色領域)が多い画像ほどSSIMが薄まるため。

→ 対策として位置ズレを直接測る指標を追加(行/列の輝度プロファイルの相互相関で
グローバルオフセットを算出)。再検証で 4pxズレ・2pxズレとも確実にFAILするようになった。
採点は必ず複合基準(SSIM+輪郭+位置ズレ+色差+高さ差)で行う。

実測3|色は測れば当たる。目測は外れる

measure_ref.py を既知のセクション画像にかけたところ、
背景 #242424・マーク #1f1f1f実際のCSS値どおりに的中
ロゴの帯高さ151px(実際のfont-size 155.6px)も精度良く逆算できた。
「たぶん#333」で書かない。測ってから書く。

設計上の判断(大串決定 2026-07-22)

  • 写真素材 → 参考画像から切り出して使う(見た目が完全一致し、採点スコアも最大化)
  • 合格ライン → SSIM 0.95(実用重視)。厳格0.97はフォント未所持時に到達不能になり頭打ちが増える
  • 入口 → /section のまま自動分岐(URLの有無で判定・コマンドを増やさない)

落とし穴

  • .reproduce/ が過去コミット(b10996c)で追跡対象に混入していた → .gitignore に追加して除外済み
  • 参考画像と再現画像の高さを揃えないと採点が無意味になる →
    reproduce_loop.sh が参考画像の縦横比から撮影高さを自動算出するようにした

2026-07-22 モードB(画像から再現)は廃止

大串FB: 「精度微妙すぎてダメ。コマンドから消して。URL送ってもらう前のやり方に戻す」
(同時に「今のURL送ってもらうやり方はめっちゃ完成度高いから残して」)

何をやって、なぜダメだったか

URLなし・スクショだけで再現する方式を実装し、採点ツール3本(measure_ref.py /
visual_diff.py / reproduce_loop.sh)で自動採点ループを回した。
HBA採用サイトFVで実地テストした結果:

指標 到達値 合格基準
SSIM 0.888 0.95
輪郭 0.42 0.75
一致率 0.52 0.90

位置・色・写真は実測でほぼ完全一致させられた(写真は髪の位置が1px以内、背景色は実値的中)。
それでも大串の目には「パッと見で全然違う」だった。

根本原因=フォント

  • 参考サイトは商用日本語フォント(モリサワ系・My Galano Grotesque 等)を使っており、
    Google Fonts に同等品が無い。候補7種を総当たりしても最良0.586止まり。
  • 書体が1種違うだけで輪郭スコアは 1.0 → 0.16 まで落ちる(実測)。
    フォントが手に入らない以上、画像だけからの完全一致は原理的に不可能
  • 見出しを画像化すれば見た目は一致するが、それはテキストではなくなり
    セクションとしての使い回しが落ちる(本末転倒)。

残った教訓(モードAでも有効)

  1. 数値と「パッと見」は乖離する。SSIM 0.79でも人間には「全然違う」。
    面積の大きい要素(背景・写真)が合っていると数値は上がるが、
    人が見るのは書体の印象と文字サイズ。数値だけを信じない。
  2. calc(100% / 144) は font-size に効かない。セクション幅基準にしたいなら
    cqwcontainer-type: inline-size + calc(100cqw / 144) を使う。
    vw はホストHPに貼ると画面幅を拾うのでNG。
  3. 切り出した画像は「切り出した範囲の実寸」で配置する
    範囲862pxの画像を947px幅に置いて横に引き伸ばしていた(SSIM 0.63→0.78 の最大要因)。
  4. 数値が悪化したら即戻す。実測のつもりがテキストのノイズを拾っていて、
    「実測で直したら逆に悪化」が起きた。採点はこういう自己修正に効く。

結論

URLをもらってから着手する。URLさえあれば実コード抽出で完成度は非常に高い(大串評価)。
URLが無いときはURLを求めて待つ。ゼロから作らない。

ツール3本(tools/measure_ref.py / visual_diff.py / reproduce_loop.sh /
grab_chat_image.py)は lab-terasu に残置。/section からは呼ばない。


2026-07-22 tsuklio.com「Brand Concept」= Astro+Tailwind v4・画像クロスフェード(成功)

成果物: concept-gallery-ogushi-0722(フード宅配コンセプト|左2列マスク画像+右テキスト2カラム)。

抽出(Astro + Tailwind v4 サイト)

  • Astroは静的HTML出力なので curl -L <URL> で完成HTMLがそのまま取れる(JS描画不要)。CSSは1本 /_astro/index.<hash>.css、アニメJSは /_astro/<Component>.astro_astro_type_script_*.js。全部 curl で取得可。
  • Tailwind v4は「コンパイル済みCSS全量をインライン」で完全再現できる。カスタムトークン(--spacing-more-* --text-more-* --radius-more-*.flex-2/3.aspect-7/12・色 --color-*-green 等)も同一ファイルに入っているので、全量入れれば全ユーティリティが解決する。個別抽出は不要・むしろ危険。
  • lg: は @media(min-width:64rem)

「左画像の選択できるアニメーション」の正体(再利用可能パターン)

  • 1スロットに写真2枚を absolute inset-0 で重ね、GSAP clipPath:"inset(0 0 100% 0)" のワイプで交互に入れ替える無限ループ(ScrollTriggerで画面内のみ再生)。opacityではなくclip-path。
  • 実値(そのまま移植): duration1・stagger2.2・delay1.5・ease"power2.out"・y±15・順序を配列で並べ替え(0,4,2,3,1,5)して斜めに波及。
  • テキストは別アニメ: gsap.from(opacity:0,y:15,duration:.8) のフェードアップ(Layout側)、見出し小ラベルはSplitTextの文字リビール。GSAP+ScrollTrigger(+SplitText)が要る。
  • ライブラリはcdnjs gsap/3.13.0/{gsap,ScrollTrigger,SplitText}.min.js(3.13でSplitTextも無料化・cdnjsに有り)。既存preview規約もcdnjs GSAP。

フォント

  • 見出しは FontPlus の TazuganeGothic(webfont.fontplus.jp のkeyがドメインロックで外部では読めない)。CSS変数のフォールバック連鎖に Noto Sans JP が入っているので、Google Fontsで Figtree + Noto Sans JP を読むだけで代替表示できる(見た目ほぼ同等)。

ハマった落とし穴(恒久対策)

  1. 画像URL絶対化は「CMS module scriptタグ除去」の後にやる。先に src="/_astro/src="https://.../_astro/ すると、除去用正規表現(/_astro/ 始まり前提)に一致せず <script type="module" src="/_astro/...js"> が生き残る。順序を逆にするか、除去regexを絶対URLにも対応させる。
  2. サムネはGSAPのopacity登場アニメで非決定になる--virtual-time-budget だけだと撮影タイミングでテキストが半透明のまま写る(feedback_preview_must_openの通り)。対策: サムネ撮影は --disable-javascript で撮る(レイアウトは純CSSなのでJS無しでも崩れない・テキストは常時可視・画像は上レイヤ静止=クリーン)。アニメ本体はライブpreviewで動くので問題なし。
  3. Chrome headlessがBashで終了せず居座る(screenshot/dump-domとも)。PNG/DOM生成後もプロセスが残り120sタイムアウトになる。→ バックグラウンド実行にして、成果物ファイルが出たら pkill -f <user-data-dir> で落とす運用が安定。--user-data-dir を毎回別名にすると競合しない。

2026-07-22 recruit.sunbelx.com = STUDIO製カルーセル(成功・STUDIOは抽出できる)

成果物: pjtstory-carousel-ogushi-0722(採用サイト|プロジェクトストーリー自動スライダー)。

まずSTUDIO=抽出不可、ではない

  • 2026-06-29 の go-spiral.ai は「STUDIO製+FVが<canvas>」だったから不可だっただけ。STUDIOでも通常のコンポーネントはHTML/CSS/JSで出力され抽出できる。Step0では「STUDIOか」ではなく <canvas>/WebGLがあるか で判定する。
  • STUDIO公開HTMLの構造: CSSもJSも全部インライン(外部cssファイル無し)。カルーセルは<sd-carousel>カスタム要素+<template>スライド、56KBのインラインランタイム(script#2)が customElements.define("sd-carousel") して駆動。ランタイムをそのまま同梱すれば動く。

STUDIO固有の3大ハマりどころ(全部これで詰まった)

  1. <style media="(max-width: 840px)"> を落とすな(「画像デカすぎ」の原因)
    STUDIOはレスポンシブCSSを <style media="...">タグ属性で出し分ける(840/1278/540px)。<style>中身だけ連結すると制約が消え、モバイル用ルールがPCでも効いて全幅になる。→ <style>タグを属性ごとそのまま出力すること。
  2. img[data-sd-img-src]{display:none}(画像が出ない原因)
    STUDIOはsdImg()が「srcを入れる+data-sd-img-src属性を削除」して初めて表示する。ビルド時にsrcを直書きするなら属性も必ず削除する。srcだけ入れてもdisplay:noneのまま。
  3. 祖先チェーンを省略するな(サイズがズレる原因)
    カルーセルの幅・余白は親が持つ(.sd-101{padding:60px 32px}等)。実サイトの祖先を全部再現する:div.sd-1 > main.section.sd-14 > div.section-inner.sd-15 > section.section.sd-99 > div.section-inner.sd-100 > div.sd-101 > div.sd-113 > sd-carousel

「完全一致」を目視でなく数値で証明する方法(今後これを標準にする)

保存した実サイトHTML自分の再現HTMLの両方に同じ診断スクリプトを差し込み、headless --dump-domgetBoundingClientRect() を比較する。

// <html data-diag="..."> に書き出して dump-dom で回収
var r=el.getBoundingClientRect(); // カルーセル/スライド/imgの w,h,x を比較

今回これで「car 1376x631 x32 / slide 1144x631」が実サイトと完全一致まで詰められた。目視では絶対に気付けないズレが取れる。

サムネ撮影は「JS要否」をセクションごとに判断する

  • 今回(STUDIO): スライドが<template>内でJSがcloneして描画 → JS必須。かつ --virtual-time-budget=12000 くらい取らないと初期化前(高さ90pxに潰れた状態)を撮る。shoot.sh既定の3000msでは失敗する
  • tsuklio(GSAP)の時: 逆に--disable-javascriptが正解だった(レイアウトは純CSS・JSはopacity登場アニメだけ)。
    「JSが無いと描画されない構造か?」で決める。テンプレclone/カスタム要素系はJS有効+長バジェット、CSSレイアウト+登場アニメ系はJS無効。

STUDIOのオリジナル化(どこまでやるか)

  • 除去する: 記事CMSパス(href="/pjt-story/..."#)、sendAnalytics("<projectId>")とその公開、data-scope-NN(CSS未参照なので安全)。
  • 汎用化する: sd-carouselstory-carousel(HTML/CSS/JS横断で一括置換。ランタイム内は全て完全一致の文字列リテラルで連結が無いので安全。カスタム要素名はハイフン必須)。
  • 触らない: 自動採番のsd-Nクラス群。ブランド情報を含まない匿名トークンで、370KB全置換はランタイム破壊リスクだけ大きく originalization の意味が無い。
  • 画像URLはルール通り維持(storage.googleapis.com/studio-design-asset-files/...)。

プレビュー提示(大串が「みれない」と言った件)

python3 -m http.server の裏起動はターン間で落ちる。大串に出すときは
サーバー不要の file:// パス(自己完結HTMLなのでそのまま開ける)を第一候補で渡すか、Bashツールのrun_in_backgroundで起動する。localhostだけ渡すのは事故る。

環境メモ(2026-07-22時点)

  • Bashはネット疎通あり(curlでcdnjs/対象サイト取得OK)。ただし ugokko.jp はサーバー側が接続拒否で全経路(curl/headless/WebFetch)不通だった(サイト側要因・時間をおくか実コードを直接もらう)。
  • in-appブラウザ(mcp__Claude_Browser__)は外部navが拒否されがち。claude-in-chrome拡張は断続的にDisconnectする。確実なのは Bashのcurl + Chrome headless(ローカルfile:// レンダリング)

2026-07-22 berg-onishi.co.jp「メニューバー」= 幅で別機構・2往復した失敗(最重要)

成果物: dropdownnav-navbar-ogushi-0722(グローバルナビ|メニュー選択でナビが下にheight展開+ハンバーガー×モーフ)。追加者=細川。

最大の失敗:同じサイトでも「幅」で全く別のメニューが動く

berg-onishi は1つのサイトに2機構あり、動画だけ見て取り違えた(デスクトップ版→誤ってモバイル版に作り替え→また戻す、で2往復)。
- デスクトップ(≥960px) = .lHeaderNavインライン展開height:0 → calc(201*px).4s var(--ease-out-expo)。細い右カラム(183px)がメニューボタンとお問い合わせの間にスッと伸びる。
-
モバイル(<960px) = .cMenuフルスクリーンclip-path:inset(0 0 100% 0) → inset(0).6s expo+opacity .4s。円形矢印アイコン・子リンク・SNS・住所付き。
-
JSも分岐vendor-CommonMenu.min.jsmatchMedia("(min-width:960px)") が真のときだけハンドラを張る=ロード時の幅で決まる。リサイズしただけでは切り替わらない(幅を変えたら必ずリロードしてから検証)。
-
恒久対策(Step0に追加):動画/スクショを受け取ったら、まずブラウザ幅を変えて両方の状態を開き、どちらが素材と一致するか照合してから抽出する*。「メニューバー」等あいまいな語は幅で意味が変わる。

GSAPにCSS変数を渡すと移植先で崩れる(×が「➤」になる)

  • 元コード: gsap.set(o,{y:"var(--line-top-y)",rotation:25,transformOrigin:"left center"})。実サイトではGSAPがinlineに translate(0px,var(--line-top-y)) rotate(25deg) と書き、ブラウザ側がvar()を解決して効いている。
  • 移植先ではこれが解決されず移動量ゼロ=線が交差せず「➤」状に崩れる
  • --px をJSで実測して数値で渡すparseFloat(getComputedStyle(inner).width)/16。GSAPは関数値を受けるので y:function(){return -2*u();} にするとリサイズにも追従。

参考元自体が「粗い」ことがある → 完全再現の後に整える

  • 実サイトの×は交点が左から約58%にあり非対称(top/bottomのmatrix・transform-origin・描画boxまで完全一致させても、大串FB「左側が埋まってる」は解消しない)。
  • 両線とも transform-origin:center center にして y=±(H/2-0.5) で箱の中心に重ね、±25°回転=交点50%の対称な×に整えた。角度・線長・線幅は元のままなのでトーンは崩れない。
  • 学び:まず座標レベルで完全再現→実測で「一致している」ことを示した上で、明らかな粗は整える。「合わせたのに変」なときは参考元を疑って実測する。

Labの code-previews は「登録」しないとURLで見られない

  • HTMLをpushしただけでは https://lab.terasu.scale-group.co.jp/code-previews/<id>SPAトップにフォールバックする(.html付きでも同じ)。
  • 確認URLを求められたら add_section.pyshoot.shbuild.py → push まで通す。登録後は同URLでプレビューが出る。
  • add_section.py は既存entryがあると上書き拒否--force必要)。contributorだけ後から入れるならentry JSONに "contributor" キーを直接追加→build.py が安全(既存内容を失わない)。

サムネはアニメ途中で写る → 撮影時だけ最終状態を固定する

  • shoot.sh(virtual-time-budget=3000)でも height/clip-path のCSSトランジションは描き切らない(1項目だけ開いた途中で写る)。setTimeoutの自動オープンも発火しないことがある。
  • cpで退避 → 撮影用に「開いた最終状態」をCSSで固定(is-open付与+transition:none+自動オープン無効)→ shoot → cpで復元。×の形も transform:translate(...) rotate(...)!important でCSS固定すると綺麗に写る。配信ファイルは触らずに済む。

in-appブラウザペインは非フォーカス時にアニメを間引く(検証が嘘になる)

  • GSAPタイムラインが途中で凍る/getComputedStyle が更新されない/element.style に!importantで入れてもGSAPが毎フレーム上書きする。「目視で崩れている」が誤判定になる
  • → 検証は次のどれかで確定させる:
    1. アニメ最終状態を数値で直接適用して getBoundingClientRect で計測(交点や対称性は座標で判定する)
    2. headless Chrome + CSSで最終状態固定してPNG化し目視
    3. 実サイト側も同じ手順で計測し、数値で突き合わせる(今回これで「完全一致」を確定できた)
  • ref指定クリックが当たらないことがある。document.querySelector('.js-menu-open').click() の方が確実。

併走セッションがある前提でgitを扱う

  • 作業中に他セッションのWIP(concept-gallery/footerset/pjtstory等)がworking treeに現れる。git add -A は使わず自分のファイルだけをstage
  • git pull --rebase は未staged変更があると失敗するので git -c rebase.autoStash=true pull --rebase で退避しつつrebase(他人のWIPを壊さない)。

2026-07-23 同じSTUDIOサイトから2本目(座談会・動画CTA)= zadankai-movie-ogushi-0723

追加で分かったSTUDIOの仕様

  • セクションの中に <style> が直接埋まっていることがあるdiv.frame の中)。今回の回転アニメの正体はそれ:
    css #loop_rotate{animation:rotation 5s linear infinite;transform-origin:center} @keyframes rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}
    → 円形「WATCH MOVIE」SVG(140x141)を5秒で1回転。セクションHTMLをverbatim切り出せば一緒に付いてくるので、body内の<style>を絶対に捨てないこと。
  • STUDIOのカスタム要素は3種(全部 script#2 が customElements.define):
    sd-carousel(スライダー)/ sd-loop-box(マーキー。speed="60" reverse 属性)/ sd-videomuted loop autoplay playsinlineの動画)。ランタイムを同梱すれば全部そのまま動く。
  • 動画セクションのheadless撮影は --autoplay-policy=no-user-gesture-required を付けると1フレーム目でなく再生状態が撮れる。

.appear は「クラスが外れる=表示」実装(見出しが消える原因)

  • STUDIOのランタイムはIntersectionObserverで .appear クラスを外すことで表示する。CSS側は .sd-164.appear{opacity:0} のようにクラスが付いている間は非表示
  • headless/キャプチャではIOが発火せず、見出し「座談会」と副文が丸ごと消えた状態で撮れてしまった(背景と動画は出るので気付きにくい)。
  • 恒久対策(毎回入れる): 末尾に表示保証のフォールバックを足す。実ブラウザでは通常どおり登場アニメが先に走る。
    js setTimeout(function(){document.querySelectorAll('.appear').forEach(e=>e.classList.remove('appear'));},2500);
    feedback_preview_must_open の「opacity依存の登場アニメで非表示のまま終わらせない」の具体形。

数値照合は2本目でも効いた(ダミー文の文字数ミスを検出)

実サイトと再現の getBoundingClientRect() を突き合わせた結果、
sec 1440x868 / bg 1264x640 x88 / title 274x104 x276 / btn 912x512 / badge 140x141 x711 / marquee 1440x48 は一致したが
副文だけ 274 → 292px(+18px) ズレた。原因はダミー文が元より1文字多かったこと。
→ ダミー化ルール「元と同じ文字数」は感覚でなく実測で検証できる。文字数を合わせたら274pxで完全一致。

同一サイトから2本目を取るのは安い

/tmp/belx_raw.html(1本目で取得済み)を再利用でき、Step0の取得をやり直す必要がない。
手順も確立済み(styleのmedia属性保持 → 祖先チェーン再現 → ランタイム同梱 → 画像のdata-sd-img-src削除 → appearフォールバック → 数値照合)。


2026-07-31(細川)正本リポは ~/dev/terasu-lab。section.md の「lab-terasu が正本」は古い

  • 事故った:section.md の記述どおり ~/dev/lab-terasu(scale-group-jp/lab-terasu)で作業→push したが本番に出なかった
    このリポの .github/workflows/deploy.yml は 2026-07-23 に push トリガーを外されon: workflow_dispatch のみ)、
    「TERASU Lab は scale-group-jp/terasu-lab へ移設」とコメントされている。lab-terasu へ push しても本番は動かない
  • 本番の正本=~/dev/terasu-lab(GitHub: scale-group-jp/terasu-lab)deploy.ymlon: push [main]
    wrangler pages deploy _lab_dist --project-name=lab-terasu --branch=mainlab.terasu.scale-group.co.jp に配信(2026-07-23 大串承認で本番切替済)。
    /section は今後 ~/dev/terasu-lab で作業する(section.md も直すべき)。

terasu-lab の追加フロー(lab-terasu と違う点・実測)

  1. git pull 時に unable to update local ref が出たら git gc --prune=now で直る。
  2. core/ は submodule(terasu-web-core)。未取得だと add_section.pyModuleNotFoundError: contributors
    .gitmoduleshttps URL で認証失敗するので SSH で入れる
    git config submodule.core.url git@github.com:scale-group-jp/terasu-web-core.git && git submodule update --init
    (または git clone git@github.com:scale-group-jp/terasu-web-core.git core)。
  3. エントリ生成は python3 tools/add_section.py --id … --kinds … --cats … --caption … --url … --contributor …
    - --kinds が必須(30分類。絞り込みの正。無いと一覧に出ない)。旧 --cats は互換で併記。
    - caption の「分類|会社名」部分(:の前)は18字以内(超えると「カードで切れる」と弾かれる)。--url は注記なしの純URL。
  4. サムネ:tools/shoot_section.mjsplaywright 依存で未インストールだと動かない
    → Chrome headless で撮って tools/normalize_thumbs.py <id>(4:3・2880x2160化)でOK。
    - 小要素(今回=バッジ単体)は normalize の自動トリムで中央がズレることがある。
    対策:4:3ぴったりの小窓(例 --window-size=440,330)で撮り、sips -z 2160 2880 で拡大(normalizeを通さない)と中央維持で大きく写る。
  5. push:他セッションの未コミット(例 maskcard)や node_modules の差分が混ざるので 自分のファイルだけを git add 明示
    sections-data.json は CI が build.py で全件再生成するのでコンフリクトは無視でよい。
  6. 反映確認は sections-data.json / api/meta を見ても0件で当てにならない(一覧は別経路)。
    確実なのは①code-previews/<id> が 200+中身反映、②本番 /sections/?kind=<kind> をブラウザで開き body.innerHTML<id> が入っているかを見る。CI反映は数分かかる。

「セクションの一部だけ」を抜く指示への対応(今回:中央のバッジのみ)

  • 大串「後ろの動画と背景はいらない、真ん中のアニメだけ」→ セクションから 該当コンポーネントのDOMだけ切り出す
  • このWATCH MOVIEバッジは 2枚重ねのホバークロスフェード:通常=.sd-177(白リング/赤文字/赤丸再生)、ホバー=.sd-182(赤リング/白文字が#loop_rotateで回転/白丸赤三角)。
    .sd-172:hover .sd-177{opacity:0} / .sd-172:hover .sd-182{opacity:1} で切替。回転は常時走っていてホバーで見える設計。
  • 単体化:sd-172 を 160x160・position:relative の器に上書きし、動画フレーム(sd-173)を捨てて sd-177/sd-182 だけ残す。sd-tb- にリネームしてオリジナル化。

2026-07-23 「デプロイしたのに反映されない」の誤認 — 原因は旧デプロイの残骸

何が起きたか

セクション追加後、https://lab.terasu.scale-group.co.jp/sections-data.json を見て
「95件のまま・今回分が無い=反映されていない」と判断した。これが誤りだった。
本番ページ /sections は正しく最新(今回分がNEWバッジ付きで掲載)だった。

真因(2階建て)

  1. Cloudflare Pages は過去デプロイの実体を残す。リポ分離(2026-07-23)前の旧デプロイが置いた
    *-data.json がURL上に生き続けていた。現在の配信物 _lab_dist には含まれていないのに 200 が返る。
  2. それが cache-control: public, s-maxage=6048001週間)でCDNにキャッシュされ、
    no-cache ヘッダを付けても剥がれず、13時間前の内容を返し続けていた。

対処(恒久)

core/tools/worker.jsRETIRED[a-z0-9-]+-data\.json を追加して 404 で塞いだ
- これらは ビルドの入力build.py が生成 → build_lab.py が読む)であって配信物ではない。
- 塞いでもページ表示に影響しないことを、正規表現の単体テスト17件(塞ぐ7・通す10)と
デプロイ後の実HTTP確認(JSON=404 / 各ページ=200)で検証済み。

教訓(他コマンドにも効く)

  • 反映確認は「人が見るページ」で行う。中間生成物のURLを見て判断しない。
  • 200が返る=最新とは限らない。Pagesは旧デプロイの実体を残すので、消したはずのURLが生き続ける。
  • 同じ構造の事故が過去にもあった(worker のコメントに reference-sites-saki layer-decomposition
    塞いだ記録あり)。URLを廃止したら RETIRED に足すのがこのリポの作法。

同時作業で気づいたこと

git status に自分が触っていない変更(別セッションの作業中ファイル)があった。
git commit <path>自分の変更だけを指定してコミットし、pull --rebase-c rebase.autoStash=true
で退避して実行した。git add -A を癖でやると他人の作業途中を巻き込むので、
同時作業日は必ず git status を見てからコミット対象を絞る。

2026-07-25 報告リンクは詳細ページ /section//

  • FB: 「①②送るときはそれぞれのセクションの詳細ページのリンクにしてほしい」
  • 対応: Step5報告の第一リンクを code-previews から /section/<id>/ に変更(section.md反映済)。

2026-07-26 並行セッションのID衝突=汎用名は担当プレフィックスで守る(ksart案件)

  • 起きたこと: 同じ大串アカウントで2つのセッションが同時に/sectionを実行。片方がUMICAHI案件、
    片方がksart案件。両方が汎用idの contact-form-ogushi-0726 を使い、後発が先発の
    code-previews/contact-form-ogushi-0726.html上書き。大串がプレビューを開いた時に
    別案件のフォームが出て「⑥仕様が全然違う」となった(実際は上書き事故)。
  • 真因: idの <担当>-<日付>別人前提で衝突回避しているが、同一担当が同日に並行すると
    <主題>(contact-form等の汎用名)が衝突する。担当名では守れない。
  • 対策(恒久): 同時作業日/並行の可能性がある日は、参考元を表す短いプレフィックスをidに付ける
    (例: ksart-contact-ogushi-0726)。social/社名をidに入れない原則より衝突回避を優先(実害=上書き事故)。
    着手時と push直前に git status で他セッションのファイルを確認し、git add <自分のpathだけ>
    コミット対象を絞る(-A禁止は既存学び)。集約(sections-data)はCIが再生成するので気にしない。
  • 付随の技術メモ:
  • スライダーの左右「線」矢印はサイト独自の slick-theme.css の .slick-prev/next::before(160×1pxの線)由来。
    CDNのslick-themeだと文字矢印になるので、実slick+slick-themeをスコープ化して同梱する(JSだけCDNでOK)。
  • stickyフローナビは side_columnalign-items:stretch で全高に伸ばして中の .fixside{position:sticky}
    汎用 .side_column{display:none}@max-width:1280(アーカイブ用)がフローにも波及するのでデスクトップで表示に上書き。
  • EFO等の外部フォームは実フィールドは静的DOMに入っている<script>/<link>/hidden を除去し action=# に。
    intro+TELだけの上部は淡くて shoot_section の内容判定に落ちる→フォーム項目が写る位置でカードを撮る。
  • CSSスコープ化ツールの落とし穴: f-string で @media{...} の閉じを }}(=}のつもりが2つ)にすると
    直後のルールを飲む。.single-works.single\b にマッチしてセレクタが壊れる((?=[\s>+~]|$)で回避)。

2026-07-26 KAUNIS CLINIC 12→8件 一括抽出(大串・並列ワークフロー)

  • 複数セクション一括は並列ワークフローが有効。まず全セクションのHTML/CSS/JS設定を機械抽出→extract_css.py(アンカー方式で使用ルールだけ抽出)+build_section.py(sec-スコープ化・reveal/swiper/accordion JS注入)で"崩れないベース"を機械生成→各エージェントは検証・ダミー化・レスポンシブに集中。品質・速度とも良好。
  • CSS抽出の文脈除去は body/html/.home だけでなく .clinic/.clinic2 等のページbodyクラスも外す。下層ページ(/clinic/等)はレイアウトCSSが .clinic .p-xxx ... とbody文脈前置で書かれ、cc⊆classset判定で落ちる→レイアウト総崩れ。CTXCLASSに追加で解決。
  • keyframe名リネームの罠: \bfadeInDown\b一括置換はクラス選択子 .fadeInDown まで壊す→JS付与クラスに対応ルールが消え全要素opacity:0で真っ白。keyframeは同一定義なら共有安全なので原則リネームしない。
  • 下層ページの画像は相対パス /wp/...https://kaunis-clinic.com前置しないと壊れる(トップは絶対URL)。
  • webp画像でheadless Chromeがハング→レンダは必ずサブプロセスtimeout付き。ブラウザペインは file:// も localhost もブロック→headless撮影 or openで実ブラウザに出す。
  • caption「分類|会社名」18字制限: 会社名が長い時(KAUNIS CLINIC=13字)は主要部分(KAUNIS)に統一すると分類が入る。
  • 固定幅(.t-cont:1280px 等)がタブレットで縮まない共通崩れ→@media(max-width:1279px){max-width:100%;min-width:0}+swiper/marquee器に overflow:hidden/clip。PC(>=1280)は不変。
  • ワークフローagentが構造化出力に失敗してもファイル編集は完了していることがある→レポートを鵜呑みにせずファイル実体(mtime+grep+render)で検証。今回2/12が出力失敗だったが1件は編集完了・1件は未着手だった。
  • 元DOMで複数セクションが同一ブロックに同居(p-index_doctorblog=医師紹介+ブログ)→重複を避け、大串が送った素材(動画)に合わせて単体sectionを切り出す。
  • 他セッション同時進行時: git add -A禁止。自分のidのファイルだけ明示add(zshは$VARを単語分割しないのでループはリテラル列挙)。CIがentries/*.jsonから全再生成デプロイなのでsections-data.jsonは非commitでOK。

2026-07-26 tomatec 6セクション一括抽出 — 抽出ツールの汎用知見(大串・恒久)

tomatec.co.jp トップ6セクション(ヘッダー/FV/製品Swiper/海外数字/研究開発/3カラム・id *-ogushi-0726)。minified CSS 310KB からの抽出で確立した、次回も効く型:
- サイト全体が親ラッパー付きセレクタ(.top .xxx{})のことが多い→ラッパー(.top等)をスコープルートとして読み替え(先頭から除去)しないと抽出が丸ごと空になる。tomatecは全ルール.top付きで、最初これに気づかず研究開発が真っ黒になった。css_extract側にWRAP=['top']で先頭除去を実装。
- 採用判定は「名前空間の部分一致」でなく「HTMLの使用クラス集合に、そのルールの全要求クラスが含まれるか」(KAUNISのcc⊆classsetと同じ)。部分一致だとfooter/recruit等の無関係ルールを大量に拾い肥大(30KB→本来5KB)。動的状態クラス(is_/slick/swiper/inview)だけ許可接頭辞で通す。
- CSS内の相対 url(../images/…) は必ず絶対URL化(CSS基準/css/でurljoin)。背景画像・矢印SVGが404で消える(⑤研究開発の矢印欠けはこれが原因、大串指摘で発覚。CSSを絶対URL化前に生成していた)。
- font-sizeは継承モデルにする(最重要)。reset で .sec-<id> *{font-size:14px} を敷くと継承チェーンを切る。「親要素にfont-size・テキストは子<p>」構造(FVの巨大英字.top_mv_txten{160px}>子p)でpが14pxに潰れ、slick中間要素も切られてvertical高さ計算まで壊れる。正解=ルートにだけfont-size(.sec-<id>{font-size:14px})・子孫は継承。tomatecのhtml{62.5%}+*{1.4rem}の実挙動と一致。remはpx併記が多いのでNrem→N*10 px変換でhtml宣言を残さず崩さない。
- 単独プレビュー用の“打ち消し”(FV上に重なる/ロードでフェードイン前提のCSSが単独だと崩れる): ヘッダーopacity:0(#wrapper.is_loaded前提)→opacity:1position:absolute(FVに重なる前提)→relative+min-height確保/.sp要素のPC表示は@media(min-width:768){display:none}。FVtop:-100px(ヘッダー潜り込み)→top:0。hover大背景(lgbg)はスコープ化でz-index順が変わり手前に被る→row{z-index:1} lgbg{z-index:0}明示。
- アニメ確認: 背景画像が巨大(tomatec FVは8.9MB)→ロード待ち必須・slick fadeの現slideは遷移中opacity:0(撮影/確認は数秒待つ)。countUpはプレビューのrAF間引きで途中停止(11で止まった)→setTimeout(dur+α)で最終値保証の自前カウントが確実。hover(mouseover)はブラウザペインのcomputer hoverで発火しない→jQueryで強制addClassして見た目確認・実ブラウザ側で動く(メガメニュー確認はこれ)。
- nodeがPATHに無い/Applications/Codex.app/Contents/Resources/cua_node/bin(Codex同梱)をPATHに。timeoutコマンドも無い(macOS)。
- 大串FB: ヘッダーは「項目にカーソルでメガメニュー(ドロワー)展開」まで含めて再現(js_headerMegaOpenホバー→data-pageで製品/企業を切替)。ナビバー単体で終わらせない。矢印/アイコンSVGが相対URLで欠けやすい→絶対URL化を必ず確認。

2026-07-27・ACOT(ヘアサロン)5セクション一括追加で得た知見(大串「プレビューで見せてから格納」)

参考: https://www.acot-hairsalon.link/ (WordPress・制作リーピー・NinjaFirewall有)。実コード抽出を機械化する build_section.py(cssutilsでCSSをパースしsec-にスコープ化) を自作して回した。

抽出の技術(ここが肝・再利用価値大)

  • cssutils は CSS変数(--var)の定義を全部落とすvar(--x)参照は値として残るが --x:#2C2C2C 定義が消える → var(--clr-main,#1D7FB7)のフォールバック(青)が出て色が全崩壊。対策=:root{…}ブロックを正規表現で生テキスト抽出し.sec-<id>{…}にして先頭に足す
  • CSSの型判定は isinstance(rule, CSSStyleRule/CSSMediaRule/CSSKeyframesRule)rule.type==CSSRule.KEYFRAMES_RULEはcssutilsのバージョンで属性名が無く落ちる。
  • rem基準は要確認。standard_sass(リーピー系)はhtml{font-size:1rem}=16px基準(62.5%/10pxではない)。.sec-id{font-size:10px}を足すとremが0.625倍で全崩壊。html/body/:rootルールはスコープ化して採用(is_base)し、font-size:10pxは入れない。
  • レイアウトがbody class依存(.home .header--logo, .archive .posts-style{display:grid})だと抽出時にそのクラスが無くグリッド定義ごと漏れる→ルート要素に--root-class archive等を付けて抽出。styleギャラリーの3列gridは.archive依存だった(ヘッダー大ロゴは.home依存)。
  • CSS採否はキーセレクタ+セレクタ内の全クラスがセクション内に存在するかで判定すると、.flex-col2 > *{width:50%}のような> *終わりも拾えて貼っても崩れない粒度になる。

レスポンシブ(Step3.5)

  • 5件全部が同じ⛔「タブレットで横はみ出し(1088>768)」。原因は元サイトがbody{min-width:1080px}(→.sec-id{min-width:1080px})でPC版をタブレットでも横スクロールさせる設計。min-width:1080px0に打ち消しで5/5合格(コマンド定義Step3.5の表どおり)。

ヘッダーの状態依存(判断ポイント)

  • ACOTヘッダーは最上部=丸い予約ボタン(.header:not(.header-sm))/スクロール後=四角(.header-sm)。ユーザーが見せたスクショは四角版だったが、四角版(header-sm付与)はロゴがflexで幅0に潰れる(状態遷移JS前提のCSSのため)。→破綻なく完全再現できる最上部(丸)版を採用し、大串に選択肢提示して「丸でOK」を確定。状態で見た目が変わる要素は、静的に完結する状態を選ぶ。

id衝突・他人の作業保護(重大・事故一歩手前)

  • 同日・同名idが別案件で既に存在(私のfaq-accordion-ogushi-0726がKAUNIS案件の同idと衝突)。add_sectionが⛔停止=事故防止が効いた→ユニークなqa-blackbar-ogushi-0726に変更。
  • 配置のcp code-previews/<id>.htmlで既存(KAUNIS)ファイルを上書きしていた(git statusM)→git checkoutで復元。教訓=(1)配置前に既存id衝突を確認 (2)git add -Aせず自分の-ogushi-<MMDD>だけ明示add(同日に走る他セッションの作業を巻き込まない)。

テキストダミー化の落とし穴

  • el.textだけ抽出すると<br>後・複数<p>の後半(el.tail)を見落とす。FAQ回答の続き(「洗い流さないトリートメント」「ブリーチ」等の実コピー)が残った→抽出はel.textel.tail両方。固有語(白髪ぼかしハイライト)だけでなく実コピー全文をダミー化。
  • 一般的なヘア用語(「ハイライト」「白髪が気になる方」等の髪型説明)は店舗特定情報でないので残してよい。ロゴ画像は実物のまま(コマンド定義:画像は元URL)=テキスト社名はダミーでもロゴはACOTのまま、は許容。

画像・プレビュー・環境

  • lazyload/フル解像度画像でClaude Browserのプレビューは初回グレー→数秒後に描画shoot_section.mjs(Playwright)は画像ロードを待つのでサムネは問題なし。慌てて「画像が出ない」と誤判断しない。
  • ローカルプレビューのサーバーは空きポートをOSに選ばせる(socketserver.TCPServer(('',0))で採番)。8080/8899/8911等は他プロセスが埋めていてAddress already in useになる。
  • Claude Browserはスクロール操作でペインがhidden化しタイムアウトしやすい。個別navigate+screenshotの方が安定。
  • nodeはPATHに無い→/Applications/Codex.app/Contents/Resources/cua_node/bin/nodeを使う(check_responsive.mjs/shoot_section.mjs)。python3にbs4/lxml/cssutils/tinycss2は有るがcssselectは無い(lxmlの.cssselect()不可→xpath/iterで自作)。ffmpeg/swift(SDK不整合)/PyObjCも無く、動画フレーム抽出はqlmanage(ポスター1枚)止まり=URL必須(実サイトを直接見て確認)が正解

2026-07-27 ギークリー corp.geekly.co.jp 6セクション一括(大串・FVあり)

うまくいった型

  • CSS抽出はブラウザ非依存のPythonツール(extract_section.py を /tmp に自作): セクションHTMLの使用クラス/id/タグを集め、minified CSSからセレクタがマッチするルールだけ抽出→sec-<id>スコープ化。@media/@keyframes/@font-face対応。CSSOM(ブラウザ)経由は大串のタブ操作と競合するので却下した。
  • fade-in の発火クラスは is-visible(base.min.css: .fade-in.is-visible{opacity:1;filter:grayscale(0)})。IntersectionObserverで付与して再現。extractは初期HTMLにis-visibleが無く落とすので、発火スタイルは手動追記が要る。
  • CSS内 url(/...) の絶対化を忘れない(HTMLのsrc/hrefだけでなく)。sedで url((["']?)/([^/]) → 絶対化。下層は url(images/...)/about/images/ に別途変換。
  • 素タグのルート要素(footer{})はextract漏れ(クラス/idが無くneed空→除外)→ 背景等を手動補完。id付き(section#news, #hero)は拾える。

FV(canvas/動画blob)の扱い=抽出不可を正直に告げる

  • 実際のトップFVは3シーン(scene1文字アニメ / scene2動画 / scene3 canvas物理演算=HTML/CSS抽出不可)。大串了承のうえcanvas省略。
  • 動画はblob(元mp4取れず)→ playwrightで #hero を element.screenshotし幾何学完成フレームをキャプチャ(currentTime=3.75が完成形・ヘッダーとCookieバナーはpage.evaluateで非表示・deviceScaleFactor2)。data URIを自分で受け取らずnodeが直接png保存=コンテキスト軽量。
  • 文字アニメ(scene1)は実サイトのchar span(--enter-delay/--exit-delay付き)をそのまま流用+keyframes slideInFromRight/slideOutToLeftを抽出。JSでscene1→scene2切替ループ。
  • 送られた画像=目的のFVと誤認しやすい。集合写真スクショをFVと判断したが、実際のFVは幾何学動画で集合写真は別ページだった。URLで実物を必ず確認してから作る。

複数まとめフロー(6件)実地メモ

  • DOM/CSS取得・push・CI待ちは各1回、抽出/検品/サムネは各件。品質は1個ずつと同じ。
  • ファイル名は<id>.html(sec-なし)、scope classがsec-<id>。最初sec-付きファイルにして直した。
  • node PATHはCodex同梱(/Applications/Codex.app/Contents/Resources/cua_node/bin)。check_responsive.mjs/shoot_section.mjsもこれで動く。
  • 「見送り」と早合点しない:大串の番号FB「1 完璧じゃない」はFV除外でなく"直して入れて"の意だった(6件目として追加)。

2026-08-10 STUDIO製サイト(growth-next.com)からのフッター抽出で得た知見

対象: Fukuoka Growth Next のフッター → dark-linkgrid-footer-saki-0810。STUDIO(Nuxt)製サイトの抽出は通常と勘所が違う。次回同型サイトで再利用する。

  1. headless --dump-dom では本文が出ない(Nuxtハイドレーション前のシェルのみ)。playwright(terasu-labのnode_modules同梱)でレンダリング後に抽出する。実スクロールを挟むと安定。
  2. CSSはdata-s-UUID属性セレクタ+:where(.render-canvas)スコープ済みリセットの構成。.render-canvassec-<id>に置換すればリセットがそのままスコープ版になる(html/body直指定は元から無い)。
  3. ** el.matches()で使用ルールを判定すると :before/:hover 付きセレクタが全部落ちる**(疑似要素はmatchesに掛からない)。判定前にセレクタから疑似要素/疑似クラスを剥がすこと。今回これで写真・ホバーアニメが丸ごと欠けた。
  4. ** @mediaを条件ごとにグループ化すると元のカスケード順が壊れる。STUDIOはdesktop-first(1160→840→480→320の順で後勝ち)。media出力は必ず幅の広い順**に並べ直す。順序を間違えると「1160のdisplay:noneが768でも勝つ」型の崩れ+横スクロールが出る。
  5. サイト共通トークン --s-color-* / --s-font-* は:root相当にあり抽出網に掛からない。原本ページで getComputedStyle().getPropertyValue() により解決し、sec-<id>{ --…: 値 } で埋め込む(欠けると「白文字が白背景に溶ける」全崩れに見える)。
  6. 遅延ロードで placeholder(data:svg) のままの img の実URLは、公開されている symbol-views/*.json(storage.googleapis.com/studio-publish/...)から uuid→content.src で引ける。ホバー用二枚目のimgはスクロールでもホバーでもロードされないのでこれが確実。
  7. PC/タブレット/スマホでカード等の要素が複製されており、非表示変種の <style></style>(:before背景の遅延注入枠)は空のまま。symbol JSON の src から .sd[data-s-UUID]:before{background-image:…} を補完しないと、モバイル幅で写真だけ消える。
  8. 検証は目視より「原本と抽出版を同幅で開き全data-s要素のcomputedスタイルをdiff」が速くて確実(今回88差分→0構造差まで機械で収束確認)。残るのはダミー文の文字数由来のテキスト幅のみ。
  9. フォントは fonts.studio.design のCSS(130KB×2)をインライン化せず <link> 参照で軽く保つ(画像と同じ遠隔参照扱い・表示は同一)。

2026-08-16 大串FB「実際にサイトに入れ込んだら精度が低い」→ 実サイト埋め込み耐性の型(カクトクFV)

対象: photo-mantra-fv-saki-0816(kakutoku.co.jp corporate FV)。単独プレビューは完璧でも、実HPに貼った時の再現度は別問題。次回から下記を標準装備する。

  1. ** 出現アニメをJS必須にしない(最重要)。元サイトが「初期 opacity:0 → JSで .is-in-view 付与 → 表示」型だと、scriptを入れずに貼った/CMSがscriptを剥がした環境で要素が永久に消えたまま(今回は写真だけ表示=「精度が低い」の最有力原因)。→ .js-anim ゲート方式**に組み替える: 素のCSSは全表示・scriptが最初に .js-anim を付けた時だけ初期非表示になり、スクロール到達で元と同じアニメ再生。JSあり=原本と同一・JSなし=静的だが完全表示。
  2. フォントは <style> 先頭の @import でも読み込む。Google Fontsの <link> はhead側なので、styleブロックだけコピーされると書体が変わる。@import併記ならstyleコピーだけで追従する。
  3. リセットはホストCSSに負けない特異性で:where() は特異性0なのでホストの p{margin-bottom:1.6em} 等に負ける。sec-<id>.sec-<id> :is(p,img){margin:0;padding:0}(二重クラス)で上書きされにくくする。※sectionを強リセットに含めるとセクション自身のmargin指定(例 margin:0 0 65px)を潰すのでp/imgに限定する。
  4. 検品に「ホスト埋め込みシミュレーション」を足すと事前に捕まえられる: 典型的グローバルCSS(*{margin:0;padding:0}img{max-width:100%;display:block}p{margin-bottom:1.6em}section{padding:80px 0}・明朝body)を持つページに style+body を埋め込み、①script有り ②script剥がしの2通りをplaywrightで数値検証(photoX/opacity/margin等が単独版と一致するか)。今回この方法で修正版の一致を機械確認した。
  5. 小ネタ: ブラウザペインは非表示状態だと innerWidth=0 になり IntersectionObserver が発火しない=「アニメが動かない」ように見える。埋め込み検証はplaywright(ビューポート確定)でやる方が確実。

2026-08-17 Vivoo(大塚製薬 otsuka-plus1.com/vivoo/about) 6箇所指定→5件抽出(Astro製サイトの型)

  1. ** 大串FB「ダミーテキストじゃなくて本文そのままにして」(確認①提示後)。本コマンドv2.2の「テキストは同文字数ダミーに差し替え」を大串指示で上書きし原文のまま格納**。同じFBが2回来たらコマンド定義本体のダミー化手順を書き換えること(今回は1回目)。
  2. AstroサイトはCSSが3層に分散: ①<link>の@import先4ファイル(セクション本体CSS)②JSが後から注入する<style>(CircleArrow/SectionTitle/SecondaryPageMv等・headless --dump-domでは取れない)③断片内のインライン<style>(:where(.img-hash)のサイズ指定)。→ ②は実ブラウザで document.styleSheets から cssRules を収集する(@import先シートは document.styleSheets に出ない=ローカルDLで補完)。
  3. astro-iconのsymbol定義は「最初に使った箇所」にインライン埋め込みされる。href="#ai:local:*" の参照だけでなく id="ai:local:*" の定義側もリネームしないと grep 残存する。断片に定義が無いアイコンはページ先頭側にあるので、live DOMから <symbol> を回収して同梱(断片内定義と重複させない)。
  4. rem基準が html{font-size:calc(.0111*100vw)}(PC)/4.10vw(SP)型の流体設計sec-<id>{font-size:10px} 移植ではremは直らない(remは常にdocument rootを見る)。→ 全CSS/インラインstyleの Nremcalc(N * var(--sec-rem)) に機械変換し、--sec-rem を sec- 上でPC/SPの@media付きで定義する方式が完全再現になる。
  5. フォントCORS: 元サーバーにAccess-Control-Allow-Originが無い → Lab別ドメインでは@font-face遠隔参照が読めない。.woffをbase64埋め込み(今回 .woff2 の実体はPostScriptの壊れファイル・format()表記も逆だった。fileコマンドで実体確認してから埋める)。
  6. マーキーの設定値はDOMにあった: data-loopflow='{"duration":30,"direction":"left"}' → CSS keyframes(30s linear infinite / translateX -50%・トラックは複製wrapper×2)に転写。inner/track/wrapperのflexはJS由来なのでスコープCSSで補完。
  7. 出現アニメ(SlideInText)はCSSで初期非表示+GSAPで表示型 → IntersectionObserver+transition-delay(35ms/文字)のscoped JSで再現(カクトクFVの教訓通りJS無しでも全表示になるゲートは今回 .SectionTitle.-secShow 方式)。
  8. ブラウザペインが非表示だと visibilityState=hidden でスクショが古いフレームのまま・innerWidth=0。視覚照合は headless Chrome --screenshot --window-size=1440,11500 --virtual-time-budget で原本フルページを撮り、帯分割(sips -c)して同幅比較が確実。
  9. 添付動画はDesktopから消えることがある(今回4本とも作業中に消えた)。受領直後に qlmanage -t -s 1400 で先頭フレームをscratchpadへ保存しておくと特定作業が継続できる。ffmpeg/swiftc/pyobjcはこのPCでは全滅なのでqlmanage一択。
  10. FV背景が three.js WebGL canvas(LowerKvGL)=抽出不可 → 2026-07-25ルール通りその件だけ外して他5件は続行。

2026-08-17・Maison Orchidée(b-exgroup.com/orchidee/)2セクション(咲輝PC・saki)

スクショ=FVとは限らない。まずDOMと照合してから抽出(今回の最重要)

  • 大串/咲輝から届いた「FVっぽいスクショ」(ネイビー地に写真が散り、中央に大きな Orchidée)は、現在の本番FVとは別物だった。サイトが「2nd line 2026.4.8 debut」でリニューアル済みで、今のFVは白背景+大判写真スライド。
  • スクショの中身(写真6枚・不規則配置・中央の白い大ロゴ・ヘッダーが白文字=mix-blend-mode:exclusion)をDOMと突き合わせたら、ページ下部のギャラリー .p-index__galleryphoto--a〜f__logo)と完全一致。→ そこを抽出して正解だった。
  • 教訓: 「FVを」と言われてもDOM照合を省略しない。今回は写真の枚数と配置が決め手。リニューアル済みサイトでは古いスクショが別セクションと一致することがある。

CSS mask-image のクロスオリジンはChromeがブロックする(再利用価値・大)

  • <img src> の画像は他ドメインでも普通に出るが、mask-image:url(https://別ドメイン/x.svg) は描画されない(今回ロゴが消え、ボタンの矢印も消えた)。b-exgroup は CORSヘッダ無し。
  • maskに使うSVGだけは実ファイルを落として data:image/svg+xml;base64, に埋める。画像そのものは参考元URLのまま(コマンド定義の原則は維持)。ロゴ5.6KB/矢印295Bなので容量影響はほぼ無い。
  • 判定の速い見分け方: 「<img>は出るのに、mask由来の図形(ロゴ・アイコン)だけ出ない」=これ。

切り出すと margin-top が背景の外へ抜ける(margin collapse)

  • 元は .p-index__inversion(ネイビー背景)の中に複数セクションが入っており、gallery{margin-top:200px} は帯の内側の余白として見えていた。単独に切り出すとマージンが帯の外へ出て上に白帯が出る
  • → 背景を持つ親に display:flow-root を足すだけで元と同じ見え方に戻る(overflow:hidden と違い中の sticky を壊さない)。

「前セクションとの間隔」はセクション単体では空きすぎになる(大串FB)

  • 元CSSどおり margin-top:200px で出したら 「上余白空きすぎ」(2026-08-17 大串FB)。元値はあくまで“前のセクションとの間隔”で、単体セクションの上下余白ではない。
  • PC 60px / SP 40px 程度に圧縮して合格。恒久ルール化: 抽出物の先頭に来る margin-top(前セクション間隔)は、セクション単体用に詰めてから見せる。理由をコメントで残す。

同PCに他セッションの未コミット entries があると build.py が巻き込む

  • build.pyentries/*.json全部集約するので、別セッションが作業中の未コミット4件まで sections-data.json に入った(=本体が無いのに参照だけ入る)。
  • 他人の entries を /tmp に一時退避 → build.py → 自分の7ファイルだけ git add → commit → 退避を戻すgit add -A は厳禁(今回も _afloat_work/ 等が大量にあった)。
  • git pull --rebase は他作業の未ステージ変更で失敗するが、リモートに新コミットが無ければ git push はそのまま通る。

環境・道具(このPC=咲輝PC)

  • node は PATHに無い/Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node(旧 Codex.app は2026-08-11の改名で消滅)。check_responsive.mjs / shoot_section.mjs はこれで動く。
  • minify された1行CSSは grep -n が役に立たない → ブレース追跡の自作パーサ@media/@supports は再帰、@font-face/@keyframes は生保持)でルール単位に分解し、セレクタ正規表現で必要分だけ抜くと速い。
  • GSAP実装はバンドル(webpack 151KB)に埋まっていても、js- フック名で検索すればタイムライン定義がそのまま読める。今回は gsap.fromTo(mask,{scale:.6},{scale:1,maskPosition:'0% 0%'}) + ScrollTrigger scrub をそのまま移植(CDNは cdn.jsdelivr の gsap 3.12.5)。
  • ScrollTriggerの start:'top '+innerHeight は単独プレビューだと開いた時点で progress=1=完成状態で出る。サムネ撮影も正しく撮れるので、そのままでよい。

2026-08-17 glänta(グランピング)|背景固定クロスフェード

ページ級の position:fixed 背景レイヤーは sticky に置き換える

  • 元サイトは .section-back{position:fixed;z-index:-1} をページ全体で共有し、各セクションのスペーサーが ScrollTrigger で img1..img6.on(opacity 1s)を切り替える設計。
  • セクション単体に切り出すと fixed 背景がセクションの外まで出て居座る(=貼ると崩れる)。
  • → 背景レイヤーをセクション内の先頭に置き、position:sticky; top:0; height:100vh; margin-bottom:-100vh に変換すると、見た目は完全に同じまま器の中で収まる。margin-bottom:-100vh を忘れるとセクション高さが1画面ぶん余分に伸びる。
  • このとき器に overflow:hidden は使わない(sticky が死ぬ)。overflow-x:clip を使う。

GSAP ScrollSmoother はセクション単体では外す

  • PC版は ScrollSmoother.create({smooth:1,effects:true})ページ全体を #smooth-wrapper/#smooth-content で包む前提の有料プラグイン。セクション単位では成立せず、CDNでも配れない。
  • → 慣性スクロールだけ落とし、背景切替とフェードインは GSAP core + ScrollTrigger(cdnjs 3.12.5) で再現すれば見た目は一致する。

Browser paneのスクショは sticky+will-change で真っ黒に写ることがある

  • position:sticky × will-change:opacity の合成レイヤーで、Browser pane の screenshot が全面黒を返した(DOM上は .on が正しく付いていた)。pane 自体もその後 30s タイムアウトで固まった。
  • Playwright(terasu-lab/node_modules)で直接スクロール撮影に切り替えると正しく撮れる。判断が割れたら Playwright を正とする。

完全再現の確認は「computed値の機械比較」が速い

  • 実サイトと自作プレビューを同じ 1440×810 で開き、対応要素の fontSize/lineHeight/letterSpacing/getBoundingClientRect を突き合わせて ALL MATCH を確認。目視だけより速くて確実。
  • 実サイトを headless で開くと読み込み演出の途中が撮れることがある(別画像に見えて焦る)。waitForTimeout を2.5秒以上取る。

動画から箇所を特定する(ffmpeg が無いPC)

  • このPCに ffmpeg は無いが、Swift + AVFoundation の10行スクリプトAVAssetImageGenerator)で .mov から任意間隔のフレームを書き出せる。swift script.swift <mov> <outdir> <秒間隔>
  • Chrome headless の --dump-dom0バイトで固まるサイトがあった(--user-data-dir を分けても再発)。WordPress等のサーバーレンダリング型なら curl の生HTMLで足りる(今回はインライン <style> 7個+ style.min.css で全部揃った)。

2026-08-17 JR西日本コミュニケーションズ 新卒採用サイト|1サイトから6セクション

include.js 型サイトは assets/includes/*.html を直接取りにいく

  • ヘッダー/フッター/スライダーを include.js が後から差し込む作り。レンダリング後DOMを使うと Swiper の loop 複製スライドが混入して二重になる。
  • curlassets/includes/header.html footer.html interview-swiper.html を直接取得すると初期状態の綺麗なマークアップが手に入る。素の index.html(curl)+ includes の組み合わせが最短。

元サイトが ScrollTrigger.matchMedia('(min-width:768px)') に入れているアニメは、その条件ごと再現する

  • 横スクロール(pin)や拡大アニメが PC幅だけの実装だったのに、無条件で移植してSPで真っ白になった(pinがSPレイアウトを潰す)。
  • ScrollTrigger.matchMedia({ '(min-width: 768px)': function(){ ... } }) ごと持ってくる。元JSの matchMedia は「見た目の一部」なので削らない。

GSAP の pin は window.load 後に張る(幅0で固定される)

  • スクリプト実行時に画像未ロードだと pin-spacer が width:0 を記録し、セクションが潰れて真っ黒になる。
  • window.addEventListener('load', ...) で張り、最後に setTimeout(()=>ScrollTrigger.refresh(), 100)

js-fadeup はスクロール計算でなく IntersectionObserver に置換する

  • 元サイトの「scroll位置で is-fadeUp を付け外し」方式をそのまま移植すると、GSAP の ScrollTrigger.refresh() が一瞬スクロールを動かした拍子に else 側でクラスが剥がれ、見出しが opacity 0 のまま残る。
  • IntersectionObserverrootMargin:'0px 0px -10% 0px'・付けたら unobserve)にすると挙動は同じでタイミングに強い。

参考元の自前フォント(.ttf)は CORS で読めない → Google Fonts の同名に差し替える

  • @font-face の src が参考元ドメインの .ttfaccess-control-allow-origin が無い=別オリジンの Lab からは読めない(画像はCORS不要なのでそのままでOK)。
  • → Google Fonts にある書体(今回 Hanken Grotesk / Noto Sans JP / Outfit)は fonts.googleapis.com のリンクに置換すれば同じ見た目。未使用の商用フォント(DIN Next 等)は宣言ごと捨てる。

ヘッダー単体はサムネ検品に落ちる(余白が多すぎ)

  • min-height:100vh の白い器+細いナビ=「中身が写らない(輪郭0.8%)」で shoot_section.mjs が2回とも不採用。
  • 器を position:relative; min-height:150px 程度に詰めると一発で通る(既存の header 系セクションも min-height:var(--sec-h,120px) で同じ考え方)。

caption 18字制限は「サイト自称の略称」で回避し、正式名称は説明側へ

  • ヘッダー|JR西日本コミュニケーションズ は20字で add_section.py が停止。
  • → サイト自身が使っている略称(今回「Jコミ」)を会社名に採用し、正式名称は「:」以降の説明に入れると、カードも詳細ページも成立する。

他セッションが同じリポで作業中は git add -A 厳禁・集約JSONも staged しない

  • code-previews/ に他セッションの未コミット作業が200件以上あった。
  • 自分の id のファイルだけを個別 git add(entries / code-previews / img×3)。sections-data.json 等の集約は CI が build_core.pybuild.py で再生成するので staged 不要=他人のエントリを巻き込まない。

動画のフレーム抽出は環境で壊れることがある → qlmanage で足りる

  • このPCでは swiftc も JXA の AVFoundation も使えなかったSwiftBridging の redefinition エラー/$.AVURLAsset が undefined)。ffmpeg も無し。
  • qlmanage -t -s 1400 -o <dir> <mov> でポスターフレームが1枚取れる。「どのセクションの動画か」を特定するだけならこれで十分。

2026-08-18 KALPA(kalpa-wajima.com/wajimanuri・WordPress)制作工程セクション

対象: craftflow-steps-saki-0817。素材=スクショ1枚+画面収録+URL。静止画と動画で指している箇所が別だった(静止画=イントロ、動画=03 制作工程)ので両方作って一括提示→大串が②だけ採用。素材が複数ある時は「同じ箇所を指しているか」を先に疑う

抽出ツールの落とし穴(今回すべて実地で踏んだ・次回は最初から回避する)

  1. セレクタはカンマ単位で採否を判定する.js-scroll__anime .js-text__anime, .js-visual__anime .js-text__anime{opacity:0} のようなリストを「1つでも断片外なら不採用」にすると、初期非表示だけ残って表示側が落ちる(=要素が永久に消える)。パートごとに残す。
  2. 全称セレクタ *.sec-<id> に変換してはいけない。reset由来の *{font-size:inherit;line-height:inherit} がスコープルート自身に当たり、こちらが書いたベース値(font-size/line-height)を丸ごと潰す。*.sec-<id>, .sec-<id> * に展開する。
  3. html / body / :root 由来のページ級リセットは採用しない:root は疑似を剥がすと空文字になり除外判定をすり抜けるので、剥がす前に文字列で弾く)。必要なベース値は原本の computed を実測して自前で明示する(今回 root=10px / body=6.25px。body{font-size:62.5%} が二重に効いていた)。
  4. スコープ内リセットは :where() で特異性0にする:is() や二重クラス(.sec-x.sec-x)だと元CSSの margin-bottom:50px 等を潰して余白が消える。ホスト耐性より先に「元の見た目」を壊さないこと。
  5. rem は calc(N * var(--sec-rem)) に機械変換(remはdocument root基準なので .sec-x{font-size:…} では直らない)。Vivoo で得た型が今回もそのまま効いた。
  6. .js-anim ゲートは「初期非表示側」と「.active 側」の両方に付ける。片側だけに付けると特異性が変わってアニメの終状態が原本とズレる(今回クレジットだけ translateY(30px) のまま残った)。

クラス名リネーム(オリジナル化)の事故

  1. URL とスコープ名を保護してから置換するwjmnr-flowflowsec の一括置換が 画像パス .../label/wjmnr-flow.png まで書き換えて404(高さ48px縮んで発覚)。https?://…sec-[\w-]+ をプレースホルダに退避→置換→復元。
  2. 置換対象は参考元固有のプレフィックスだけに絞る(今回 wjmnr-* のみ)。js-scroll__anime / flow-step / f-amg / bg-b などは汎用命名なので触らない。広く置換するほど衝突・巻き込みで壊れる。リネーム後は必ず「リネーム前後で computed 差ゼロ」を機械確認する。

書体(Adobe Fonts の有料書体を使っているサイト)

  1. 参考元CSSの日本語フォント名が文字化けしていることがある"貂ク繧エ繧キ繝ック Medium"=游ゴシック)。=原本でも効いていないので、正しい日本語名を書くとこちらだけ字幅が変わる。computed を見て実効している英字名だけに揃える。
  2. Typekit(use.typekit.net)の書体は他ドメインでは読めない前提で設計する。見出しゴMB31は字幅0.94em、代替の Noto Sans JP は 1em で、そのままだと見出しが1行→2行に折り返す。→ font-feature-settings:"palt"(0.968emまで詰まる)+ letter-spacing を実測から逆算して補正(今回 .27em→.245em で原本と同じ3行組みに一致)。

検証の型(これが決定打)

  1. 原本と抽出版を同幅で開き、全要素の computed を index 順に diff する。今回 msg 30ノード→差ゼロ / flow 150ノード→差1件(書体由来の8px)まで機械で収束させた。目視より速く確実。PROPS に transform を必ず入れる(アニメ終状態のズレは transform にしか出ない)。
  2. 画像の遅延ロード(data-src)は src に変換。img.lazyloaded のクラス差はlazyload JS由来なので無視してよい。

運用

  1. id 衝突は entries だけでなく code-previews/ も見るflow-steps-saki-0817 は entries 未登録だが code-previews に他の人の確認①待ちファイルが実在し、上書きしてしまったgit statusM で気づき git checkout -- で復元)。ビルド先を書いたら必ず git status を見る
  2. git pull --rebase は他の人の未コミット差分があると失敗するgit fetch + git rev-list --count HEAD..origin/main で差0を確認してから push すれば、他人の作業に触れずに済む。git add は必ず自分のファイルだけを個別指定(-A 厳禁は既知だが、sections-data.json も他人の未登録entriesを巻き込むのでコミットしない=CIが生成する)。
  3. 確認①は code-previews だけ先に push して本番URLで見せる(entries 未登録なので一覧には出ない=「Labに載せる前に確認」を守れる)。採用されなかった方は git rm(CDNキャッシュで数日200が返るが実体は削除済み)。
  4. 動画フレーム抽出はこのPCではやはり不可(ffmpeg無し・playwright/Chrome は headful でも HEVC の .mov を loadedmetadata すら発火させない)。qlmanage のポスター1枚で「どのセクションか」の当たりを付け、URLの実物で確定する。

2026-08-17〜18 work kit(hitoba-office.com)FV→実績一覧の連動:GSAP pin を sticky で完全再現

成果: kv-shrink-works-saki-0817(導入事例|work kit)/fv-slider-auto6-saki-0817(FV|work kit)。
Nuxt風WordPress+Tailwind任意値+Alpine+GSAP ScrollTrigger の重い構成だが、実測値が全部リニアだったので数値一致で再現できた

  1. 大串FB「画像の下に左下のテキストが来るように」=z順の話。参考実装は一覧が z-index:1
    ロゴが z-auto で、カード画像がワードマークの上を通る。自作で z-index:2 にしていて手前に出ていた。
    重なり物(sticky ロゴ・固定ナビ)は必ず原本の z 順を確認してから決める
  2. 大串FB「もう一つのセクションも同じ動きだとしてもやって」。動画が2本来たら、
    同じ一連の動きに見えても動画の本数ぶんセクションを作るのが期待値(1本にまとめない)。
    今回は ①FV→縮小→一覧の連動 ②FV単体スライダー、の2本立てにした。
  3. スムーススクロール(lenis等)のサイトは window.scrollTo が効かない
    computed値は動くのにスクショだけ古いままという食い違いが出て、比較を丸ごと誤る。
    page.mouse.wheel で小刻みに送る(今回120pxずつ+待ち)。撮影と同時に scrollY を読んで突き合わせる。
  4. GSAP pin は pin-spacerposition:absolute なので通常フローに寄与しない。
    自作では .track{height:100svh+PIN}.pin{position:sticky;top:0;height:100svh} に置き換えれば
    ホストHPに貼っても壊れない(fixed を使わずに済む)。
  5. ピン距離は「縮み切ったFVの上端=一覧1件目の上端」で決まる(実測 1475 = 60svh+1000+60−125)。
    この式で組むとどの画面サイズでも受け渡しがズレない。
  6. transform: scale() translateY() は順序で意味が変わる。原本の matrix は translate 成分が等倍
    matrix(0.7986,0,0,0.7986,0,125))= translateY() scale() の順が正。逆にすると 125→99.9 にズレる。
  7. 受け渡しの正体: 一覧は order で回転し、FVに出ている物件のカードだけ opacity:0
    縮んだFVがちょうどその空きスロットに収まり、FVがそのままカードになる。
    これを知らずに作ると「画像が2枚重なる/情報行が二重に出る」。スクロール後は自動送りが止まるのもセット。
  8. 文字は 1/var(--fv-scale) で逆補正されている(縮んでも級数が変わらない)。移植時も同じ手当てが要る。
  9. ロゴ色はピン進行に対する #fff→#000 の線形補間(実測 y=300→203 / 700→134 / 1100→65 / 1475→0)。
    「重なり判定で白黒を切り替える」と推測すると外れる。まず数値を取る
  10. FONTPLUS配信のフォント(fpbf_xxxx)は再利用不可 → Inter / Noto Sans JP 等で代替する(@font-face を持ち出さない)。
  11. このPCの動画フレーム抽出は決定打が無い(ffmpeg/swiftc/pyobjc 全滅、qlmanage は先頭1枚のみ、
    ブラウザcanvasはペイン非表示だとseekしてもフレームが更新されない=全コマ同一の罠)。
    先頭フレーム(qlmanage)で箇所を特定 → 以降は playwright で実サイトを直接計測が最短。動画の解析に時間を使わない。
  12. scratchpad(/private/tmp/claude-501/...) はセッション中に消えることがある(今回2回消失)。
    作業ファイルは ~/dev/.section-work-<案件>/ に置く。
  13. 同時作業中は push が弾かれるcannot lock ref)。ただし相手の push に自分のコミットが取り込まれている
    ことがあるので、git merge-base --is-ancestor <自分のcommit> origin/main で先に確認する(無駄なrebase防止)。
  14. CI(deploy.yml)が build_core.pybuild.pybuild_lab.py を自前で回すので、
    集約JSONはコミットしない。entries と img だけ add すれば良い(他セッションの未コミット分を巻き込まない)。

2026-08-18 大串FB 7点 → コマンド本体 v2.4 に恒久ルール化(最重要)

大串から同一セッションで2通。「これルールとして保存しておいて/他のセッションでも同じ指摘ないように」。

①テキストをダミーテキストじゃなくて、本文をそのまま使う
②コマンドとURL、スクショ、動画を送った後に「これで合っていますか?」などの確認はいらない。すぐに作り始める。
③スクショ、動画を送っても動画だけ作られてないことが多いからスクショと動画、送ったやつは全て作る。
④作り終わったら一つ一つChromeで開けるプレビューのURLを送る。
⑤勝手にLabに格納しない
⑥精度が低いのが多い。修正しないで一発で格納できるのは10%〜20%ほど。修正に時間はかけたくないから精度をあげて。
⑦いろんなHPを一気にセクションコマンド使った時に違うHPのプレビューが送られてくることが結構あった

真因分析(ここが本質・"気をつける"では直らないものだった)

  • ⑥精度が低い真因=「学びが本体に昇格していなかった」。崩れる原因は毎回ほぼ同じ10パターンで、
    この learning.md に実証済みで全部書いてあった(CSS変数落ち/rem基準取り違え/*にfont-sizeで継承断/
    相対url絶対化漏れ/bodyクラス文脈依存の取りこぼし/登場アニメ発火クラス/keyframeリネーム事故/
    単独プレビュー用の打ち消し/素タグのルート漏れ/ライブラリのテーマCSS差異)。
    learning.md は"読まれる保証がない" ので、毎回同じ所を踏んでいた。→ v2.4 で本体に「崩れる原因トップ10」表として昇格
    同様に、2026-07-22 に「今後これを標準にする」と書いたまま未昇格だった getBoundingClientRect() 数値照合も、登録前ゲートに昇格
    教訓=learning.md に「今後これを標準にする」と書いたら、その場で本体 .md にも反映する(書きっぱなしは効かない)。

  • ⑦違うHPのプレビューが混ざる真因=2つとも本体の書き方が原因の構造バグ(注意力の問題ではなかった):
    1. 旧 Step0 が取得先を /tmp/dom.html / /tmp/site.css に固定。2サイト目を取得した瞬間に上書きされ、
    前のサイトのDOMを見て作る状態になっていた。→ /tmp/section_<site>/ にサイト別分離。
    2. 旧 id 規則が <主題>-<担当>-<MMDD>「参考元社名は使わない」と明記していた。汎用主題(contact-form 等)が
    別サイト間で衝突して既存プレビューを上書き(2026-07-26 ksart×UMICAHI で実際に発生し大串が別案件のフォームを見た)。
    この事故の対策は当時「恒久」と learning.md に書かれたが本体は旧規則のままだった。
    id は <site>-<主題>-<担当>-<MMDD> でサイト識別スラッグ必須に本体を変更(衝突回避 > 社名を出さない原則)。

  • ④と⑤は両立する(一見矛盾するが構造で解ける)。build_lab.py:3055("img","assets","code-previews")
    丸ごと _lab_dist にコピーするため、entries/<id>.json を作らなくても code-previews/<id>.html は公開URLで開ける
    実測: entries 205件に対し code-previews 465件(=未登録プレビューの運用は既に一般的。コミット a9fcb30 も「一覧登録はまだ」と明記)。
    確認①は https://lab.terasu.scale-group.co.jp/code-previews/<id>.html を1件ずつ送る(Chromeで開ける・localhost禁止)。
    /sections 一覧には出ないので「勝手に格納した」ことにならない。格納=entries登録は大串のOK後

  • ③動画だけ作られない真因=動画を「動きの参考資料」と解釈し、作るべき1件として数えていなかった
    → 受領直後に素材を番号付きで棚卸しし、動画にも id を割り当てる。完了時に受領件数と作成件数を照合してから報告。

  • ②着手前確認の禁止は、canvas等で抽出不可の時も同じ(返事を待たず、その件だけ外して残りを作り切る)。

反映先

  • section.md v2.4(冒頭に「2026-08-18 大串FB(7点・最優先ルール)」表を新設+各Stepを改定)
  • memory feedback-section-command-7rules.md(他セッション用)/Vault Claude_学びストック

2026-08-18 STUDIO(studio.design/ja)4セクション = Studio製サイトの抽出ノウハウ

参考元=ノーコードCMS「STUDIO」で作られたサイト。自動生成クラス(sd-NNN / symbol-N__sd-NNN)+<style>22枚という特殊構造で、素直に抜くと必ず崩れる。次に Studio 製サイトを見たらここを最初に読む。

抽出時に踏んだ4つの罠(全部「見た目が壊れる」形で出る)

  1. ブレークポイントが <style media="(max-width: …)"> のシート単位@media ブロックではない)。
    document.styleSheets[i].media.mediaText を見ずに集めると、モバイル用CSSがPC用を後勝ちで上書きし、2カラムが縦積みに崩れる
    → 抽出時にシートの media を読み、空でなければ @media <cond>{…} で包み直す。これでレスポンシブも本物になり check_responsive.mjs が素で合格する。
  2. アイコンが [data-material-symbols-loaded="true"] ゲートで消える
    Studio は「アイコンフォント読込完了」を html/body の data 属性で示し、CSSが [class*="material-symbols-"]{visibility:hidden} → 属性が付いて初めて visible
    html/body の data-* 状態属性を「祖先ラッパー」へ移植する。.sec-<id> 自身に付けても、スコープ化後は .sec-<id> [data-…] … なので効かない(ここで1回ハマった)。
  3. 遅延画像が data:image/svg+xml のプレースホルダーのまま抜ける
    → 抽出前に対象を scrollIntoView し、srcdata:image/svg でなくなるまで待つ(最大10秒くらいリトライ)。
  4. 開閉は <sd-toggle open> 属性1つで切り替わる(CSSは sd-toggle:not([open]) .x{height:0} 等)。
    → 参考元のランタイムJSは持ち込めないが、属性を付け外しする20行のJSを自分で書けば、CSSは実コードのまま動きが完全再現できる。FAQのアコーディオンもヘッダーのメガメニューも同じ仕組み。

抽出の型(今回うまくいった手順・他サイトでも使える)

playwright で参考ページを開き、CSSOMを走査して「対象サブツリーの要素に実際にマッチするルールだけ」を集めるのが速くて正確(el.matches() で判定・:hover 等の擬似クラスは判定時だけ除去し、出力セレクタは元のまま残す)。
- 祖先はクラス名だけのラッパーとして再現し、祖先自身のCSSは持ち込まない(.sd-1 .x のような子孫セレクタを効かせるため)
- getComputedStyle(root) から --* を総取りして .sec-<id>{} に載せる(変数の解決漏れ防止)
- 使う @font-face / @keyframes は、集めたルール本文に名前が出るものだけ残す
→ この方式だとルール数がフッター747/ヘッダー684と多くても、外来グローバルCSSはゼロ(html/body/* の機械検証も0件)。

撮影・運用

  • ヘッダー単体はサムネ検品に落ちるshoot_section.mjs が「中身が写らない=色14種/輪郭1.5%」で弾く)。
    ヘッダーは閉じているとバーだけ=ほぼ空白のため。→ 見どころ(メガメニュー展開)を開いた状態で自前撮影して img/sections/ に置き、normalize_thumbs.py で4:3化する(main 1440×1080@2x / __detail 1440×1000@2x / __sp 390×760@2x の3枚必須)。
  • position:fixed のセクションは単独プレビューだと高さが出ない → COPY範囲の外<div style="background:#fff;min-height:620px"> の下地を置く(body{} を書くと「スコープ漏れ」機械検証に引っかかるのでインラインstyleの外側ラッパーでやる)。

事故:ロック未確認による id 衝突(同じことを繰り返さない)

  • 着手前に ai_session_lock.sh list を必ず見る。この日は同じMacの別Claude Codeセッションが terasu-lab のロックを保持していた(_afloat_work/*-saki-0817 が多数未コミット)。
  • 見ずに書き始めた結果、同じ id header-megamenu-saki-0817 を同時に使い、自分の抽出物が上書きされて消えた<内容>-<担当>-<日付>同担当・同日・同種だと普通に衝突する(→ header-floating-mega-… のように内容を具体化)。着手時に code-previews/<id>.html の存在チェックを入れる
  • さらに向こうの git add -A に自分の作りかけHTMLが巻き込まれて push された。→ リポの外(~/dev/_section_work_0817/)で作り、完成後に自分のファイルだけコピー。作業スクリプトもリポ直下に置かない。
  • 集約は entries/ 全部を舐めるので、他人の未コミット entries は一時退避してから build_core.py && build.py(一覧に他人の未完成分が出るのを防ぐ)。add も1ファイルずつ明示。

確認用の実URL

Cloudflare Pages のscratch project に上げて渡す(大串は毎回Chromeで開ける実URLを求める)。
CLOUDFLARE_API_TOKEN=$(cat ~/.creo_cf_token) CLOUDFLARE_ACCOUNT_ID=$(cat ~/.creo_cf_account) npx wrangler pages project create <name>pages deploy <dir>。今回は lab-section-preview.pages.dev
node は ChatGPT.app 同梱/Applications/ChatGPT.app/Contents/Resources/cua_node/bin。Codex.app は改名で消滅)。


2026-08-18 日テレアート(ntvart)about から3件(Reasons / Flow / Q&A)

動画で指定されたとき、ffmpeg 無しでフレームを抜く

このMacに ffmpeg / pyobjc は無い。Playwright(chromium) で <video> を再生してフレームを撮るのが最速。
- file:// の mov は about:blank から src 指定すると読めないreadyState が上がらずタイムアウト)。
mov を作業ディレクトリにコピーし、同じフォルダに置いた html を file:// で開いて相対srcで読むと通る。
- v.currentTime = tonseeked を待って locator('#v').screenshot() を等間隔で数枚。これで「どのセクションか」の特定は十分できる。
- スクリプトを /tmp に置くと playwright が解決できない(node_modules はリポ側)。リポ直下に置いて実行する。

実サイトから「そのセクションが実際に使うCSSだけ」を機械抽出する

document.styleSheets を全走査 → セレクタから疑似クラス/疑似要素と状態クラス(.is-* / .--*)を剥がして、対象サブツリー内の要素に matches() するものだけ採用@media は条件文字列ごと保持。
- これで 289KB の app.css から 1セクション分 50〜70ルール(80〜110行)まで落ちる=「外来グローバルCSS混入で数千行」を構造的に防げる。
- :root / html / body にマッチしたルールは別枠に集め、必要な物だけ sec-<id> に寄せる。
- クロスオリジンのCSSは cssRules が読めない(今回モリサワ morisawafonts.net は 0件)。日本語は Noto Sans JP、英字は Google Fonts の Ubuntu で代替(既存 ntvart セクションと同じ方針)。
- rem はホストの html{font-size} 依存。抽出値を 16px基準で px に直して書くとホストHPに貼っても崩れない(sec-<id>{font-size:10px} を置いても rem は解決しない=過去の void 系の弱点)。

minified バンドルからでも「実装のロジック」は取れる

webpack のモジュール索引("./section-stack-cards.js":"kqu8" のような map)を grep → その id の function(t,e,n){…}波括弧の対応を数えて切り出すと、Alpine コンポーネントの中身がそのまま読める。
- 今回の section-stack-cardsintroProgress = clamp(0,(vh - top - vh*i)/vh,1) を毎フレーム delta += .3*(target - delta) で追従させ、--stack-intro-progress としてCSS変数に流すだけ。CSS側が scale / border-radius / 写真の translateY・rotate を全部やる。GSAP無しの素JSで完全再現できた。
- decoration-marque:original の scrollWidth ぶんを 20秒 linear で無限ループ。CSS keyframes + --marque-shift(JSで実測値を代入)で等価。フォント読み込み後に測り直すdocument.fonts.ready.then(layout))のを忘れると幅がズレる。
- 外部SVGスプライト(<use xlink:href="…sprite.svg#id">)は該当 symbol を丸ごとインライン化path を途中で切ると別の字形になる(Qが黒丸になった)=必ず </symbol> まで正規表現で取り切る。

モバイル専用の1行を取りこぼしやすい

.page-about .section-stack-cards__indices{top:-17%} のような body クラス起点のモバイル専用ルールは、サブツリーへの matches() では拾えるが見落としやすい
PCだけでなくモバイル幅でも実サイトと並べて撮る。今回これでカウンター位置のズレ1件を登録前に発見(check_responsive.mjs は通っていた=検品は横スクロールしか見ない)。

同じMacで別セッションが同じリポを触っている前提で動く(8/17の事故の続き)

今回も着手時点で 127ファイルが未コミット(別セッションの作業)。
- git add -A は絶対にしない。自分の code-previews/ / entries/ / img/sections/フルパス列挙で add。
- sections-data.jsonCIが build_core.py → build.py → build_lab.py を再実行して作り直すので、コミットしなくていい(他人の未コミット entries を巻き込むリスクの方が高い)。
- 逆に向こうの git add -A に自分の HTML が巻き込まれて先にコミットされていることがある(今回 366e597 に3件とも入っていた)。git add しても git status に出ない時は事故ではなくこれなので、git log -- <file> で確認してから残り(entries / img)だけコミットする。
- git pull --rebase他人の未ステージ変更があると失敗するcannot pull with rebase)。push 自体は通るので、git fetch && git log origin/main で載ったことを確認する。

2026-08-18 groxi.jp/company(会社概要・アクセス・沿革の3件)

  • 動画/スクショからの箇所特定: ffmpegpyobjc(AVFoundation) も無い環境では qlmanage -t -s 1200 -o <dir> <mov> で先頭フレームのサムネが撮れる。動画から「どのセクションか」を特定するにはこれで十分だった(swift の AVAssetImageGenerator まで書く必要なし)。
  • node が PATH に無い: /Applications/ChatGPT.app/Contents/Resources/cua_node/bin を PATH に足す(旧 Codex.app のパスは 2026-08-11 改名で消滅)。check_responsive.mjs / shoot_section.mjs はこれで動く。
  • PC前提の固定幅サイト(min-width:768px で 954px 固定)はタブレットで必ず検品に落ちる。元サイト自身が横スクロールする設計のため。直し方=ブレークポイントを 768→1024 に上げ、元サイトのSPレイアウトを1023px以下に適用する(PC1440の見た目は完全に不変・元サイトに無いCSSを足さずに済む)。3件ともこれ一発で ✅ になった。
  • 罫線テーブル系の長いセクションはサムネ検品に落ちやすい(白地+細い罫線=輪郭率が低い)。MAX_H=2600 で切られたうえ4:3に縮むと「中身が写らない(輪郭2.8%)」判定になる。実物は問題なく読めるので、目視で確認してから --force で保存してよい(今回 info-table が該当・history 2271px は通過)。
  • 有料ブランドフォントの代替: groxi は英字見出しが Campton(自社サーバー直リンク不可)。Poppins Bold で置換して大串OK。幾何学サンセリフ系はこれが第一候補。
  • 並行作業への配慮: この日 terasu-lab は別セッションが数十件を同時編集していた。git add -A は絶対に使わず自分の15ファイル(html/json/png×3種)だけを個別add。ルート sections-data.jsonCIが再生成する残骸なのでコミットしない(他人の未コミットentriesを巻き込まないため)。git pull --rebase は他セッションの unstaged 変更で失敗するが、リモートが進んでいなければ push はそのまま通る。

2026-08-18 表参道高柳矯正歯科(j-ortho.jp/flow/)3箇所一括:WordPress自作テーマ+bodyクラス出し分けの型

対象: photo-hero-flow-saki-0817 / step-cards-arrow-saki-0817 / sticky-step-detail-saki-0817。動画1本+画像2枚+URLで来た3箇所。

動画フレーム抽出はこれで解決(このPCの正解)

  • 送られた .mov(画面収録・H.264)は playwright の chromium.launch({channel:'chrome'})=システムChromeなら再生できる。file://<video src="rec.mov"> の小さなHTMLを置いて gotocurrentTime を進めて canvas に drawImage → toDataURL で任意枚数のフレームが取れる。
  • 全滅だった手段: playwright同梱chromium(H.264非対応)/playwright同梱ffmpeg(movを demux できず "Invalid data")/JXAのObjC.import('AVFoundation')$.AVURLAsset が undefined)/qlmanage(ポスター1枚だけ)。
  • setContent の about:blank から file:// の動画は読めない(goto でHTMLを開くこと)。onloadedmetadata はイベント登録前に発火済みでタイムアウトするので waitForFunction(readyState>=2) で待つ。
  • scratchpad(/private/tmp/claude-501/...)は作業中に消える。素材と中間ファイルは ~/work-<案件>/ に置く。

.pc / .tablet / .sp を body に付けるサイトは @media に変換して抽出する

  • このサイトは matchMedia("min-width:1025px") 等で body クラスを差し替える方式(@media をほぼ使っていない)。素直に抜くとPC固定になりレスポンシブ検品で落ちる。
  • 抽出時に .sp X@media screen and (max-width:767px){ .sec-<id> X }.tablet X768〜1024px.pc X → 無条件(PC既定)に機械変換する。これで検品3件とも一発PASS・SP実機も元と同じ縦積みになった。

セレクタから「ページ級文脈」を剥がすときの2大事故

  1. 祖先ラッパーの直下(>)ルールを剥がして誤爆させない.sticky_container>.left{flex-direction:column;height:calc(100vh-16px)}.left として拾い、セクション内の別の .left に当たって高さが倍(4318px→8553px)になった。→ CTX > で始まるルールは丸ごと捨てる(ページ枠専用のため)。剥がしてよいのは子孫(空白)だけ。
  2. 文脈クラスを正規表現で広く取らない\.wp-[\w-]+ で剥がしたら WordPress ブロックの .wp-block-button__link{color:#fff} が「残り空=sec自身」に化けて全テキストが白になった。→ CTXは body/html の実クラス列+実在する祖先ラッパー名だけを列挙する。残り空になった時に sec 自身へ寄せてよいのは html/body/:root 由来のときのみ。

日本語フォント代替は「paltが効くか」で選ぶ(測って決める)

  • 元は Adobe Fonts の秀英ゴシック銀(ドメイン紐付けで持ち出し不可)+商用 Canela(CORSヘッダ無しで @font-face 不可)。
  • 同一文言・同一 font-size/letter-spacing で幅を実測: 元743px / Zen Kaku Gothic New 827px(palt非対応)/ Noto Sans JP 773px(palt対応)。見た目の「字間が広い」は palt の有無が主因。→ 和文代替は Noto Sans JP、Canela 代替は Cormorant Garamond。
  • 測り方: 抽出版ページで document.createElement('span') に対象要素の computed(fontSize/fontWeight/letterSpacing/fontFeatureSettings)をコピーして white-space:nowrap で幅比較。

Labリポは他セッションが並走している前提で commit する

  • 今回 git status に別作業(アフロート案件)の未コミット62ファイル+ステージ済みentriesがあった。git add -A はもちろん、素の git commit も index 全体を巻き込む
  • git add <自分のファイル> の後 git commit -m "..." -- <同じパス列>(pathspec付きcommit)で自分の分だけ確定する。zshでは変数展開が1語になるのでパスは直接列挙する。
  • 集約JSON(sections-data.json 等)はコミットしない。CI(deploy.yml)が build_core.py → build.py → build_lab.py を回して再生成するので、他人の未コミットentriesを巻き込むリスクだけが残る。
  • CIが cancelled になるのは後続pushのconcurrency。後続CIがmain全体をビルドするので自分の分も入る(失敗と誤認しない)。

確認①は「code-previewsだけ先にpush」で本番URLで見せられる

  • entries/ を登録しなければ /sections 一覧には出ないまま、https://lab.terasu.scale-group.co.jp/code-previews/<id> が200で開ける。localhostでなく実URLで確認①を出せる(2026-08-01 大串FB「プレビューは毎回Chromeで開ける実URLで送る」に適合)。OK後に add_section.py+サムネ+pushで一覧へ。

その他の小ネタ

  • headless --dump-dom はUA判定で body に sp が付いた状態のDOMになる。PC版の構造・CSSが要るので playwright(1440幅・実UA)で document.styleSheets から収集するのが正。
  • FVが position:fixed + 次セクションが重なる設計は、単体化時に sec-<id>{position:relative;padding:8px;background:#e6eef0}.fv{position:absolute;top:8px;left:8px;width:calc(100% - 16px);height:640px} に置き換えると元の縁(8px余白+角丸8px)ごと再現できる。
  • JSが書いた inline style(transform: translate3d(...)z-index:-1)が outerHTML に残る。断片から style="..." を機械的に落とす(残すと画像が背景に潜って消える)。パララックスは data-y を読む自前JSで再現し、JS無しでも表示は壊れないようにする。

2026-08-18 hitoba-office(work kit)Contact = Alpine.js製フォームの抽出

新規ファイルの commit は「add → pathspec付き commit」まで通す(今回やらかした)

  • 既存の学び通り pathspec commit を狙ったが、新規ファイルは untracked なので git commit -m ... -- <path>did not match any file(s) known to git で弾かれる
  • そこで git add <3ファイル> → 素の git commit にしたら、別セッションが同時に index を触っていて他人の4ファイル(entryと画像だけ・プレビューHTML無し)が混入して push された(=一覧にカードが出て詳細404になる事故)。git add 直前の git diff --cached は0件だったのに、commit時には増えていた=並走セッションがある前提で「直前確認」は当てにならない
  • : git add <自分のパス列>git commit -m "..." -- <同じパス列>(add済みならpathspecが効く)。そして commit直後に必ず git show --name-only --format="" HEAD で中身を検証する。混入したら git rm --cached <混入分> で本番から外す(手元のファイルは消えないので他セッションの作業は壊さない)。

Alpine.js(x-show/x-data)駆動サイトの抽出手順

  • 除去してよい: <template>x-show="confirming"(確認画面用マークアップ・今回105KB→16KBに激減)、x-show="errors.*"_formrun_gotcha(ハニーポット。CSSで消えているだけなので残すと「If you are a human…」が表示される)、空の form-file-name
  • style="display:none" の一括除去は危険。タブパネル(x-show="activeId===…")まで消えて2枚目以降が空になる。→ パネル系クラスは除去対象から除外し、非表示は自前で持たせる。
  • 動きは Alpine を持ち込まず元のtransitionクラス(is-enter/is-enter-start/is-enter-end)を付け外すだけの素のJSで再現できる(クラスもCSSも元のまま=見た目完全一致)。
  • 初回アニメの発火条件が html.is-ready .xxx / html:not(.is-ready) .xxx の形。→ sec-<id>.is-ready … に寄せ、rAF2回でスコープ根に is-ready を付ける。

Tailwind製サイトのCSS機械抽出(使用クラス照合方式)

  • CSSをトップレベルからトークナイズ→ (メディア文脈, セレクタ, 本文) に分解 → セレクタ中の全class(\.((?:\\.|[^\s>+~,:\[\]()#.])+) で拾ってバックスラッシュを除去)が使用クラス集合に含まれる時だけ採用。Tailwindのエスケープ(sm\:\[--text-fluid-var\:18\])もこれで一致する。
  • 落とし穴3つ(全部これで崩れた):
    1. :root{--…} は class を含まないので素直に書くと落ちる → カスタムプロパティ定義だけは root系セレクタでも拾う(rem基準・fluid計算の土台)。
    2. <main> 自身のクラスl-wrapper 等)は find_all(True) に入らない → 使用クラス集合に足す。忘れるとラッパー余白が消える。
    3. is-ready / is-enter などJSが付ける状態クラスは使用集合に無い → 「使用クラスと同居している時だけ許可」(単独 .is-enter{} はページ遷移用で別物なので拾わない)。
  • preflightを捨てる時、:before,:after{--tw-content:""} を移植し忘れると content:var(--tw-content) の疑似要素が全部消える(selectの矢印が消えた)。select{appearance:none}button,input,select,textarea{font:inherit} も同様にスコープ版で残す。

「完全再現」は目視でなく実測で証明する

  • playwrightで元サイトと抽出版を同じ幅で開き、主要要素の rect(root相対)とページ高さを突き合わせるのが速くて確実。今回は Δy 全項目0.0px・高さ差 −1.0〜0.0px(4タブ全部)まで詰めた。
  • 単体プレビューは body の既定margin 8px 分だけ幅が狭くなる(1380→1364)。既存セクションも同じなので許容。ホストに貼れば差は出ない。
  • 契約フォント(FontPlus=TWK Lausanne / FOT-ニューセザンヌ)はドメイン紐付けで持ち出し不可 → Inter+Noto Sans JP に差し替え(オリジナル化の一環)。

2026-08-18 groxi株式会社 お問い合わせフォーム(contact-form-progress-saki-0817)

白基調セクションは必ずサムネ採用ゲートに落ちる(誤検知・原因の見分け方)

  • shoot_section.mjs の不採用条件は core/tools/shot_common.mjsisPoorShot()。3本目の
    flatRatio >= 0.92 && edgeRatio < 0.03(画面の9割超が同一色) が白フォーム系を必ず巻き込む。
  • 今回の実測: 色24種 / 輪郭1.89% / 最頻色97.6% → 不採用。
    上余白を104→48→0pxに変えても flatRatio は 97.6→97.6→97.5% とほぼ不変=トリミングでは絶対に抜けられない。
    白地に1px罫線のフォームは、忠実に再現している限り数値が下がらない(参考元のgroxi自体が同じ白基調)。
  • 「落ちた=壊れている」ではない。切り分け手順を先にやる:
    1. 候補クロップ(1440×1080)を自前で撮って目視する。中身が写っていれば描画は成功している。
    2. measureShot() を直接呼んで colors / edgeRatio / flatRatio を出す(measureShot(page, buf)
    第1引数はディレクトリではなく Playwright の page。ここを間違えると scratch.evaluate is not a function)。
    3. flatRatio が余白調整で動かない=デザインが白基調と確定 → --force を使ってよい。使った事実は必ず報告する。
  • 逆に flatRatio が余白で大きく動く/文字数0なら、それは本物の撮影失敗なので --force してはいけない。

並行セッションの git add -A に成果物を巻き込まれる(実害あり・対策必須)

  • 同じ ~/dev/terasu-lab で別セッションが走っていると、向こうの git add -Aこちらの未コミットの
    code-previews / entries / img を丸ごと巻き込んでcommit+push
    する。今回は161ファイルの一括コミットに
    自分のHTMLが入り、続く別コミットで entry+サムネ3枚も入った。
  • さらに悪いことに、向こうが「プレビューHTMLが未コミットだから本番404になる」と誤解して
    Revert accidental inclusion of …こちらの entry とサムネだけを消した(HTMLは既にコミット済みだった)。
  • 対策(次回から必ず):
  • add_section.py を打った直後に、自分の4〜5ファイルだけを個別 git add → 即 commitする(放置しない)。
  • git add -A は絶対に使わない。パスを列挙して add する。
  • git add が何もステージしない時は gitignore を疑う前に git ls-files <path> で「既に他人にコミットされていないか」を見る。
  • revert された時は git log --oneline -- <ファイル>前提が正しいか裏取りしてから入れ直し、
    commit message に「なぜ revert が誤りか」を書いて再発を止める。
  • sections-data.jsonCI(.github/workflows/deploy.yml)が build_core.pybuild.pybuild_lab.py
    再生成する
    ので、コミットしないのが正解。ローカルで build すると他セッションの未コミットentryまで混ざる。

抽出メモ(groxi.jp/contact のフォーム)

  • 見た目の要はCSSではなく配置トリック: 同意チェックの枠は table tr:first-child
    position:absolute で浮かせ、.inner{margin-top:280px}(SPは300px)で空けた隙間に置いている。
    PCは #vue-form、SPは .inner が基準(SPだけ .innerposition:relative が付く)ため、
    PC=top:125px / SP=top:-272px と符号が逆になる。ここを読み違えると上下がまるごとズレる。
  • ステップ表示の数字は契約フォント Campton(自社配信・持ち出し不可)→ Poppins 700 に差し替え
    この差でだけ実測が ±1px ズレる(01の字幅 85px→84px)。他は全項目0px一致。
  • チェックの動きはVueが label.isCheck を付けていただけ → 素のJSで is-checked をトグルすれば同じ挙動。
    label:aftertransform:scale(0)→scale(1) は元CSSのまま。

STUDIO製サイトの抽出手順(NewsPicks corp / 2026-08-18・再現性高い)

STUDIO(studio.design)製サイトは他CMSと構造が違うので、この型どおりに進めれば1発で完全再現できる
見分け方=DOMに class="sd appear"data-s-<UUID> 属性が並んでいたらSTUDIO。

  • CSSは2か所に分かれている。両方を拾わないと崩れる。
  • ① ページ内 <style>(今回85KB)= 各要素の実スタイル。セレクタは .sd[data-s-<UUID>] の形。
  • /_nuxt/entry.*.css(今回25KB)= .sd のベース挙動(flex/縦並び/transition)。
    ここを抜くと全要素が横並びになって崩壊するので必ず拾う。ただし :where(.render-canvas) …
    巨大リセットが含まれるため、union セレクタは「,」で分解して1本ずつ判定して要る物だけ残す
    (セレクタ全体をまとめて判定すると、テーブル/input用まで巻き込むか、丸ごと落として崩れる)。
  • DOM構造を器ごと再現するdiv.render-canvas > div.StudioCanvas > div[ページルートUUID] の3層が
    無いと ① のCSSが効かない。セクションだけ抜いても再現しない。
  • フォントはCSS変数経由font-family: var(--s-font-298b8a52):rootMontserrat 等に解決)。
    :root の変数定義ブロックを拾って sec-<id>{…} に寄せる。拾い忘れるとフォントだけ別物になる。
  • 画像は lazy-load でDOMダンプからは絶対に取れないsrc が空SVGのプレースホルダのまま・
    data-src も無い=JSが差し替える方式)。実ブラウザで開き、各 <img> を1枚ずつ
    scrollIntoView してから currentSrc を回収
    する(まとめてスクロールでは発火しない画像が残る)。
    UUID→実URLの対応表を作り、抽出後のHTMLに差し込む。
  • appear アニメは class 付け替え方式.appear{opacity:0} → 表示時に appear-active へ。
    IntersectionObserver 5行で同じ挙動を再現できる(元のJSは不要)。
  • UUIDのリネームは2段階data-s-<UUID> だけでなく、CSS変数名(--gap-h-<UUID> 等)にも
    裸のUUIDが埋まっているので、属性を置換したあと「裸のUUID」も同じトークンへ置換する。
    なお画像URLに含まれるUUIDは残ってよい(アセット名=参考元URLのまま使うルール)。

見出しだけのセクションはサムネ検品に落ちる(誤検知・--force が正解)

shoot_section.mjs は「色数/輪郭量」で中身が写っているかを見るが、白地に見出し1行+バーだけの
セクションは本物でも "中身が写らない" 判定になる
(今回 services-heading が該当)。
実際に撮れているかを目視で確かめてから node tools/shoot_section.mjs <id> --force で通す。
中身が本当に空の時との区別は人がやる(forceを常用しない)。


2026-08-18 音更電設(otoden.co.jp)— sns-info / intro-poem

「ずれる」の真因は属性セレクタ [class*=…] の抽出漏れ(最重要・再発必至)

このサイトの共通レイアウトは .c-inner-md{} ではなく [class*=c-inner].-contents-xl{} の形で書かれていた。
クラス名(\.([\w-]+))だけでセレクタ判定しているとこの種のルールが丸ごと落ちて、中身が左寄せ・
1カラムになる
(=大串の「ずれてる」FB)。単独プレビューでも一見それらしく見えるので気づきにくい。
- 対策: 抽出フィルタに \[class[*^|~]?=["']?([\w-]+) を足し、部分一致トークンを USED/MODS へ前方一致で展開する。
- 検証は目視でなく機械で: 本番と自作を同じビューポート幅で開き、主要要素の
getBoundingClientRect()fontSize を JSON で並べて比較する。今回これで
「内側コンテナ x=120 / 幅1200 / カード577.5×278」まで一致を確認できた。目視だと絶対に取りこぼす。

rem基準は --rem 変数化して calc() へ機械変換する

html{font-size:62.5%}+画面幅連動(calc((10/1440)*100vw) 等)のサイトは、remがルート基準なので
sec-<id> にスコープ化できない
。そのまま貼るとホストHPのルート値で全サイズが狂う。
- : html の font-size を sec-<id>{--rem:…} に変換 → CSS全体の \d+rem
calc(var(--rem) * N) に一括置換。メディアクエリごとの --rem 再定義もそのまま効く。

動画FBの扱い(ffmpeg無し環境)

  • 動画は Desktop 直下ではなく ~/Desktop/名称未設定フォルダ/ に入っていることがある
    見つからなければ mdfind -name "画面収録 <日付>" で探す(「ファイルが無い」で止まらない)。
  • ffmpegが無いので、python3 -m http.server で配信 → ブラウザで再生。ただし
    python http.server は Range 非対応でシークできないcurrentTime を代入しても 0 に戻る)。
    fetch(url).then(r=>r.blob())blob化して URL.createObjectURL を src に入れ直すとシークできる。
  • 動画は最初に必ず見る。今回は見ないまま進めて「動画のは?」と指摘された。
    さらに大串の指す箇所が1つ上のセクションだった(波カーブは境界装飾で、本命は home-intro
    「背景stickyで詩が流れる」演出)。動画=別セクションの可能性を最初に潰す

ブラウザペインのスクショが真っ白になる時は headless Chrome で確認する

ページは正常なのにペインのキャプチャだけ白飛びすることがある(DOM計測では要素が正しく存在)。
「壊れた」と早合点せず --headless=new --screenshot で実レンダリングを撮って確かめる。

他セッションの未コミット作業がある時の格納(データ事故の防止・恒久)

リポに他セッションの未コミット作業(今回は entries 16件・img 56件・tmp スクリプト多数)
残っていることがある。ここで git add -A はもちろん、build.py の集約JSONをそのまま commit するのも危険
sections-data.json は entries ディレクトリ全件から再生成されるので、他人の未完成分(画像が未追跡)まで
一覧に載り、本番でサムネ404
になる(過去に caafb4c で同じ理由の revert 実績あり)。
- : 集約後に 「git管理済みの entries」+「自分の新規id」だけに絞ってJSONを書き戻す
git add は自作ファイルだけを明示列挙 → commit。他人のファイルには一切触らない。
今回はこれで12件を除外して公開した。
- git pull --rebase は他セッションの unstaged 変更があると失敗する。
git fetch して origin/main...HEAD の差を見て、origin側が0なら pull 不要(無理に autostash しない=他人の作業を触らない)。
- もっと簡単で確実な型(2026-08-18 実地)=集約JSONは commit しない
.github/workflows/deploy.ymlCI側で core/build_core.pybuild.pybuild_lab.py を毎回実行するので、
sections-data.json はローカルで commit する必要が無い(=他人の未完成分が混ざる経路をそもそも作らない)。
ローカルでの build は「登録できたかの確認用」と割り切って、作業ツリーに置いたままにする
- git add すら競合する(他セッションが同時に add していて、自分の6ファイルだけ add したのに
ステージに他人の29ファイルが乗っていた)。→ ステージを介さない git commit -m "..." -- <パス列挙> を使う。
パス指定 commit はインデックスの状態を一切変えずに指定パスの作業ツリー内容だけを確定できるので、
他セッションのステージング作業を壊さない。git reset で剥がそうとするのは逆に危険。
- 同時進行だと push も他セッションが先に済ませていることがあるgit pushEverything up-to-date)。
慌てず git merge-base --is-ancestor <自分のSHA> origin/main自分のコミットが載ったかだけを機械判定する。
CI run も同様に自分の分が cancelled になり後続 run に引き継がれるので、最新 run を watch すればよい

抽出したHTML/JSでハマる2つの「元サイトでは成立していた」罠(2026-08-18)

  1. <p> の中に <div> は静的HTMLでは書けない(パーサが p を自動で閉じ、中身が外に出る)。
    元サイトは GSAP SplitText 等が JSでDOM挿入しているので成立していただけ。DOMダンプをそのまま貼ると
    文字が全部はみ出して二重表示になる。→ char入れ物の <p><div> に変換して解決(見た目は不変)。
  2. CSSの初期非表示(transform:translateY(-100%))を残すとGSAPと二重管理になる
    gsap がアニメ終了時にインライン transform を消した瞬間、CSS側が再適用されて要素が消える
    → 初期状態は CSSから外し、JS側の gsap.set(el,{y:'-100%'}) に一本化する。

node が PATH に無い(このPC)

node 未インストール。export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"
check_responsive.mjs / shoot_section.mjs が動く(v24)。毎回これを付ける。

reninc.jp(株式会社ren)で確定した5つの罠(2026-08-18・fv-video-scroll / impact-cards-stack)

  1. CSS mask画像とWebフォントは CORS で落ちる(最重要・見た目が消える)
    curl は 200 なのに、ブラウザからは requestfailed。丸ボタン・矢印・波形が全部透明になり
    「要素はある(visible / サイズあり)のに何も描かれない」状態になる。通常の <img> / background-image
    CORS 不要なのでそのまま参考元URLでよい(=ルール5は守れる)。mask と @font-face だけ data URI に埋め込む
    → 切り分けは playwright の page.on('requestfailed') が一発。目視で悩まない。

  2. body.is-xxx のような状態クラス付きルールをスコープ化してはいけない
    body{opacity:0}(ローディング演出)や body{position:fixed;overflow:hidden}(スクロールロック)が
    無条件で .sec-<id> に適用され、セクションが真っ白・スクロール不能になる。
    → 抽出器は 素の html/body/:root だけ採用し、直後に . # [ : が続くものは捨てる
    保険として、ルート自身に効くルールからは position/overflow/opacity/height/width 等を落とす。

  3. rem基準がビューポート連動のサイトは --sec-rem に写して rem を絶対値化する
    ren は html{font-size:calc(var(--vw)*16/1440*100)}。スコープ内に font-size を書いても
    rem は root 基準のままなので効かないNremcalc(var(--sec-rem) * N) に機械変換し、
    --sec-rem を元と同じメディアクエリで定義する。@media の条件式内の rem は変換しない(var不可)。

  4. sticky FV は「後続コンテンツ」が無いと動かない(大串FB「動きが違う」の真因)
    position:sticky の可動域=親の高さ−自身の高さ。セクション単体では 0 になり、
    動画が画面に貼り付かず上へ流れて出ていく。→ 元サイトでFVの次に来るセクション(renなら .p-home-lead)を
    同じ実装のまま置く+元のパララックス(.js-parallax / num=0.3)も入れると実サイトと完全一致する。

  5. 同じ変数がSPとPCで別値になっている(大串FB「下の形が違う」の真因)
    波形の高さ --_top は SP 2.375rem / PC 6.4375rem。SP値のまま出すと
    画像(2880×154)の幅が711pxにしかならず波が小さく中央に寄る。メディアクエリ違いの値を必ず拾う。

動き系セクションの検証は「実サイトと数値で突き合わせ」が最速・最確実(2026-08-18)

スクショ比較より、playwright で実サイトと再現版を同時に開き、scrollY 別に computed 値を並べる方が速く確実。
ren では width / border-radius / padding-top / コピーの opacity / 動画の画面Y位置を 6 点で比較し、全一致で確定した。
「動きが違う」と言われたら、まずこの表を作る(画像を何枚も撮るより安い)。

作業ファイルの置き場所とローカルプレビュー(2026-08-18)

  • スクラッチパッド(/private/tmp/...)は作業途中で消えることがある(抽出器もCSSも全部消えた)。
    → 作業ファイルは ~/dev/<案件>_work/ に置く。消えて困るものを /tmp に置かない。
  • 提示前にローカルサーバーを立て直す。古い http.server プロセスが残っていると大串の画面で「見れない」になる。
    lsof -ti:8080 | xargs kill -9 → 起動し直し → ?v=<時刻> を付けたURLで渡す(キャッシュ回避)。
  • 大串が送ってくる .mov は Chromium で seek できないことがある(画面収録のコーデック)。
    フレーム抽出に時間を使わず、実サイトを直接操作して比較する方が速い。

Tailwind+Alpine+GSAP サイトからの抽出は「ブラウザで実マッチCSSを機械収集」が正解(2026-08-18・hitoba-office/work kit)

Tailwind の任意値クラス(sm:[--text-fluid-var:49] 等)はCSSをテキスト検索しても拾いきれない(\[ エスケープだらけ)。
実サイトを開いて document.styleSheets を全走査し、対象要素に matches() するルールだけ集めるのが最速・漏れゼロ。
- @media / @supports は再帰で辿り、conditionText を保持したまま出す
- :hover / ::before は擬似部分を剥がしてから matches() に掛ける(そのままだと一致しない)
- cssText はグラデーション等の値が欠けるbackground-image: ; になる)。該当ルールは元CSSファイルから正規表現で拾い直す
- :root 変数(--base-content-w 等)は @media 別に定義が変わるので、PC幅にリサイズしてから computed を取る
74ルールまで絞れて、完成品は331行に収まった(外来CSS混入ゼロ)。

有料フォントは外部から読めない=代替に切り替える(2026-08-18)

TWK Lausanne は 200 で落とせるが Access-Control-Allow-Origin が無いため他オリジンからは適用されない。
curl -H "Origin: https://lab.terasu..." -D - でCORSヘッダを先に確認し、無ければ近い系統の代替に置換する
(TWK Lausanne / Helvetica系ネオグロテスク → "Helvetica Neue", Helvetica, Arial)。画像と違いフォントはCORS必須

GSAP ScrollTrigger は素のJSで数値一致まで再現できる(2026-08-18)

scrollTrigger:{start:"top 90%", end:"bottom 10%", scrub:0} の進行度は
p = (innerHeight*0.9 - rect.top) / (rect.height + innerHeight*0.8) を 0..1 にクランプするだけで一致する。
CDN依存を持ち込まずに済む(ホストHPのGSAPと衝突しない)=貼っても安全。実測で期待値と完全一致を確認してから出す。

ブラウザペインが隠れていると rAF が止まる=「動かない」と誤診する(2026-08-18)

requestAnimationFrame を使った視差は、ペイン非表示だとコールバックが走らず transform が固まる。
スクショを1枚挟むとフレームが進むので、scrollTo → screenshot → transform を読む の順で測ること。
同じ理由で 1枚目のスクショは前フレーム(画像未描画)が返ることがある。もう1枚撮れば正しい絵になる。

並行セッションの git add -A に自分のファイルが巻き込まれる(2026-08-18・実害あり)

別セッション(同じPCの別作業)が git add -A すると、まだ確認①前の code-previews/<id>.html が勝手にコミット・pushされる
今回は中身が完成版だったので実害なしだったが、未完成版が本番に出る事故になり得る
- 症状:git add しても git status に出ない=既にHEADに入っているgit log -- <file> で犯人のコミットが分かる)
- 対策:terasu-lab では自分のファイルだけを明示的に git addする。git add -A は絶対に使わない
- sections-data.jsonCIが build.py で作り直す.github/workflows/deploy.yml)のでコミットしない
ローカルで build.py を回すと他セッションの未push entries が38件混入する(=実体の無いカードが本番一覧に出る)
11. ** 同一PCで別セッションが並行作業していると git add -A が事故になる(2026-08-18 実地)。今回 git status が179件差分・他セッションの未コミット作業(既存セクションのpadding調整・未完成entry6件)が混在していた。→ 自分のファイルだけを明示 git add(entries/code-previews/img の3点セット×件数)。また集約 sections-data.json はコミットしないのが安全(CIの deploy.yml が core/build_core.pybuild.pybuild_lab.py を毎回実行して再生成するため。ローカル版を入れると他セッションの未コミットentryを巻き込み、preview不在で本番404になる=過去commit caafb4c と同じ事故)。
12.
CI runが cancelled でも失敗とは限らない。並行pushがあるとconcurrencyで古いrunがキャンセルされる(今回10分間で7回入れ替わった)。gh run watch の X 表示に釣られず、conclusion を API で見る最終判定は本番URLの200と一覧掲載で行う
13. 自分の作業ファイルが
別セッションのコミットに巻き込まれることもある(今回5件のpreviewが先に他コミットへ入っていた=git log -- <file>で「既存」に見えてid衝突と誤認しかけた)。中身の<title>とsecクラスを見れば自分の物か判別できる**。

2026-08-17 AI model(ai-model.jp)CASE STUDY

「傾いたカードが流れて巨大文字が重なる」は WebGL=抽出不可(判定手順)

ai-model.jp トップの #comparisonDOMを見ても文字が無いので探し回りがちだが、3点を測れば即断できる。
1. document.querySelector('.comparison__canvas canvas').getContext('webgl') が返る=WebGL描画
2. 見えている画像の元 .comparison__imagesvisibility: hidden(canvasに渡す下敷き素材)
3. 画面に見える文字(例「TYPE 001」)が DOM・CSS content・画像ファイル名のどこにも無い
→ この3点が揃ったら その箇所は諦めて大串に3択(別の箇所/近似/やめる)を出す。全ページ grep に時間を溶かさない。

流体rem サイト(html{font-size:○vw})の移植は --sec-rem 方式

ai-model.jp は html{font-size:0.0694444vw}(PC=1440基準)/ 599px以下 0.2777778vw(SP=360基準)。
remは常にdocument rootを見るので sec-<id>{font-size:10px} では直らない。
→ 全 Nremcalc(N * var(--sec-rem)) に機械変換し、--sec-rem をPC/SPで定義する。
JS側の '…rem' 文字列も同罪(ホストのroot依存で移動量が16倍になる)。今回 -360rem は 1440px基準で 25vw なので
window.innerWidth * 0.25 の px に直した。文字列長を変えても壊れない利点もある。

font-feature-settings:"palt" が抜けると日本語が1行増える

body側の指定は「#how配下にマッチするセレクタ」だけを拾う抽出網から漏れる
本文の高さが 108px→129px(5行→6行)になり、連鎖でセクション全高が67pxズレた。
→ 数値照合で ffs の差として出る。ルートに font-feature-settings:"palt" を補完して一致。
同様に body 由来の font-size:1.4rem / line-height:1 / color:#000ルートに移す(img高さ500→506px の犯人)。

リセットをスコープ化すると本体CSSに勝ってしまう

.sec-x.sec-x :is(p,img,…){margin:0}(0,2,1)は .sec-x .caseMedia__ja(0,2,0)より強いので、
セクション本体の margin-left が消えて幅がズレる。
本体セレクタも二重クラス .sec-x.sec-x <sel> に統一する(ホストCSSにも勝てて一石二鳥)。

置換は「長い順」に当てる(短い社名が長文を殺す)

("三越伊勢丹","ハルカ百貨店") を先に当てると、後段の本文まるごと置換がマッチしなくなり
本文中の AI model ISETAN STUDIO MIオンラインストア が残ったまま公開されかけた。
sorted(TEXTS, key=lambda x: -len(x[0])) で長い順に適用。検証grepも社名だけでなく本文内の固有名詞を対象に含める

並行セッションの一括処理に @font-face を破壊された(実害あり・要警戒)

code-previews/ に置いた自分のファイルが、別セッションの一括処理で
base64埋め込みフォント4つ→同一データの3つに差し替えられた(書体もレイアウトも崩れ、サムネが素のHTMLに見えた)。
- 気づき方:grep -c '@font-face'ファイルサイズが自分のビルド結果と合わない/git diff がフォント行だけ±
- 対策:サムネを撮る直前に自分のビルド版をコピーし直す。撮影後もサイズとフォント数を確認してからcommitする

2026-08-18 NewsPicks Brand Design(ページネーション / イベント一覧カード)

参考元のSVGを url() で直リンクするとCORSで消える(矢印が出ない)

mask-image/background-image に参考元ドメインのSVGを絶対URLで指定すると、
localhost から見たとき CORSブロックで画像だけ描画されない(要素自体は存在するので気づきにくい)。
- 気づき方:疑似要素に background-color:red を一時的に当てると赤は出るのに画像が出ない
console に blocked by CORS policy: No 'Access-Control-Allow-Origin'
- 対策:そのSVGだけ data URI にインライン化する(urllib.parse.quote でエンコード)。
ラスタ画像(png/jpg)は <img> なのでCORS無関係=ルール通り参考元URLのままでよい。
影響を受けるのは「CSSから読むSVGアイコン」だけ。

疎なセクション(ページネーション等)はサムネ検品を誤検知で落ちる

shoot_section.mjs は「色の種類×輪郭の量」で中身を測るので、
白背景+小さい要素が両端に散る構成は 中身が写らない(色8種/輪郭1.5%) で落ちる。
- 先に効くのは上下余白の適正化(元サイトの余白は"一覧との間隔"で、単体セクションでは過大)。
ただしそれでも輪郭量は足りないことがある。
- 最終判断は --force で撮って実物を目視する(FORCE=1 は効かない。引数 --force が正)。
既存の circle-pagination 等も同じくらい疎で通っているので、写っていれば採用でよい
- 逆に「落ちた=必ず不良」ではないので、目視せずに諦めないこと。

他セッションの未コミットが大量にあるPCでは git add -A が致命的

このPCには常時 45 modified + 129 untracked(他セッションの作業途中)がある状態。
- 自分のファイルだけをフルパス列挙して add する(code-previews / entries / img/sections の3種×各id)。
- sections-data.jsonadd しない。CIが core/build_core.py → build.py → build_lab.py
最新mainから実行して生成するため、ローカル版をコミットすると他人の未コミットentryを巻き込む。
- git pull --rebase は unstaged changes で失敗する。git fetch して behind 0 なら rebase 不要

同時作業が多い日はCIが cancelled になる(失敗ではない)

concurrency deploy-terasu-labcancel-in-progress:false)のキューに複数溜まると、
待機中の自分のrunがキャンセルされる。ただし後続runは自分のcommitを含むmainからビルドするので実害なし。
- 確認手順:git merge-base --is-ancestor <自分のsha> origin/main含まれていることを確認し、
最新runの成功を待つ。自分のrunがXでも慌てて再pushしない。

大串の指示で背景を変えたら、サムネの見え方まで確認する

「背景白にして」→ 一覧カードでほぼ真っ白になりやすい(セクションは画像で選ぶので体験に直結)。
今回は実物が視認できる範囲だったのでそのまま採用したが、白背景×疎な構成は要注意

動画しか無いときは ffmpeg で全フレームをコンタクトシート化して特定する

ffmpeg は未インストールでも python3 -c "import imageio_ffmpeg; print(imageio_ffmpeg.get_ffmpeg_exe())" で同梱版が使える。
fps=5 で抜き→12枚ずつ連結して一覧化すると、どのセクションを指しているか一発で確定できる。
(今回:1本目の判断では「スクショと同じページネーション」と誤読み。全フレーム化で
スクロールで通過するイベントカード一覧が本命だと分かった。動画は必ず全フレームを見る

2026-08-18 JILLION(jillion.co.jp/business) 4件抽出(WordPress自社テーマ=素直な型・ただし置換事故が多発)

対象: mission-parallax / mission-circlegraph / businessfield-cards / roadmap-timeline
サイト構造は素直(headless --dump-dom でDOMが取れ、CSSは common.css + business.css の2本)。
にもかかわらず 「置換」と「詳細度」で4回踏んだ。次回は下記を最初から組み込む。

置換事故(3つとも1回のビルドで同時発生した)

  1. クラス改名は "スコープ接頭辞を付ける前" にやる。後からCSS全文に .replace() を掛けると
    scope名そのもの(.sec-roadmap-timeline-…)が置換されて全ルールが死ぬ。しかも se+c-roadmap のような
    部分一致まで拾う(.sec-roadmap の中に c-roadmap が含まれる)。→ セレクタ単位で改名→その後スコープ付与。
  2. 改名置換からURLを退避するimage_roadmap_01.jpgimage_steproad_01.jpg になり画像が全滅した。
    src|srcset|data-src="…" と CSS の url(…) をプレースホルダに逃がしてから置換し、最後に戻す。
  3. 検証greps も scope名を除いてから数える(sed "s/sec-<id>//g" してから grep)。しないと
    「旧クラス名97件残存」のような偽陽性で無駄に直しに行く。

詳細度(カクトクの教訓をそのまま適用すると逆に壊れる)

  1. スコープ用リセットを .sec-x.sec-x :is(h1..h6,p,…){font:inherit} にしてはいけない。(0,2,1) が
    セクション本体の .sec-x .sectitle-en{font-family:Bebas Neue} (0,2,0) に勝ってしまい、見出しが本文フォントになり
    レイアウトごと崩れた(元サイトは :where(:not(...)) =特異性0なので絶対に勝たない設計)。
    リセットは .sec-x :where(...) の (0,1,0) に固定。ホストHPの素の要素指定 (0,0,1) には勝ち、
    セクション本体の .クラス 指定には負ける=元のカスケードが保たれる。
    ※2026-08-16の「二重クラスで強く」は p/img など限定的な打ち消しの話。要素リセット全体に広げない。
  2. 出現アニメのJSゲートは「初期状態」と「.is-shown」の両方を同じ .sec-x.js-anim に寄せる
    片方だけゲートすると詳細度が逆転して「JS無しで永久に消える」or「JSありでも出ない」になる。
    両方同じ (+1クラス) にすれば、JS無し=どちらも当たらず完成形/JSあり=原本通り。実測で clip-path:none を確認する。

抽出漏れ

  1. 状態クラス(.is-hover 等)のCSSは el.matches() では絶対に拾えない(今のDOMに付いていないため)。
    疑似要素/疑似クラスを剥がすのと同じ場所で \.is-[\w-]+ も剥がしてから判定する。
    これを忘れてリンクの「1語ずつ入れ替わるホバー」が丸ごと落ちていた(静止画では気づけない)。
  2. 親グリッドから切り出した要素は、親の幅制約も一緒に持ってくる.mission-graph は親 .mission{width:118.4rem}
    前提の設計で、単体にすると横に広がり 中心ロゴが背景画像の同心円からズレる(背景の点線円は画像に焼かれている)。

その他

  1. rem基準は Vivoo と同型の流動設計(html{font-size:calc(vw/144)} PC / /40 SP)→ 全rem値を
    calc(N * var(--sec-rem)) に機械変換。--sec-vw をJSで clientWidth にすると原本と完全一致(無ければ100vw)。
  2. Morisawa等ライセンスフォントは Noto Sans JP に差し替え(0720のjillion既存2件と同じ扱い)。
    family変数を差し替えるだけでは太さが出ないので、--font-family-*-db/-b/-r/base を使う宣言に
    font-weight を機械注入する(db/b→700, r→400, base→500)。
  3. 外部スプライトSVG(<use xlink:href="別ドメイン#id">)は別ドメインでは描画されない。使うsymbolだけ
    インライン化し、idico-* にリネームする(参照側だけ直すと定義が衝突する)。
  4. 検証は「原本と抽出版の全要素を getBoundingClientRect で総当たりdiff」が最速。ツリー階層を揃えるため
    抽出版は .sec-<id> > * から測る(wrapper分ずれて全要素が差分に見える)。
    最終的に残る差分が font-weight だけになれば完全再現とみなせる。

同時作業まわり(他セッションが100件級の未コミット作業を抱えていた日)

  1. sections-data.json はコミットしない。CIが build_core.pybuild.py で再生成する。
    他セッションの未コミットentriesを含んだ集約JSONを push すると本番に実体の無いカードが出る
    git add は自分のidのファイルだけを列挙する(git add -A は当然禁止)。
  2. CIが cancelled でも失敗ではない。他セッションの連続pushで concurrency により中断されるだけで、
    自分のcommitは後続runでデプロイされる。→ runを追いかけず本番URLの200と /sections 掲載で確認するのが正。

環境

  1. scratchpad がセッション途中で消えることがある(今回2回消えた)。中間スクリプト・抽出JSONは消える前提で、
    成果物(code-previews/.html)は早めに ~/dev/terasu-lab 側へ書き出しておく。消えた後は
    生成済みHTMLを直接編集する方が速い*(余白追加はこの方式で対応)。
  2. .mov はこのPCでは中身を読めない。ffmpegなし/qlmanageは先頭1フレームのみ/Playwright+実Chromeでも
    file://・http:// どちらも MediaError code 4受領直後に qlmanage -t -s 1400 でフレーム退避が必須
    (Desktopの元動画も作業中に消える)。先頭フレームだけでも「どのセクションか」の特定には足りた。

2026-08-18 jillion.co.jp から5セクション(news / header / business / recruit / stairnav)

15の訂正 — .mov は Playwright で読める

  1. 前回「.movは読めない」と記録したが読める。手順は3つだけ:
    ① 動画と player.html<video id=v> だけのページ)を同じディレクトリに置き python3 -m http.server で配信
    page.goto でその player.html を開く(動画と同一オリジンにするのが肝
    v.currentTime = tonseeked を待って element.screenshot()
    canvas.drawImagetoDataURLTainted canvas で必ず失敗する。要素スクショなら通る。
    これで6フレーム×4本を1分で抜けて、どのセクションかを正確に特定できた(BUSINESSカード/RECRUIT/CONTACT/SPのFVナビ)。

rem が可変なサイト(jillion系)の移植

  1. html{font-size:calc(var(--vw)/144)}(SPは /401440px以上は 1440/144=10px 固定)という設計。
    rem は html を参照するのでスコープ化できない → --u に置き換え、全数値を calc(var(--u)*N) にする。
    3分岐(--u:calc(100vw/144) / @min1440{10px} / @max767{calc(100vw/40)})を必ず書く。
  2. --border-radius--font-size-base@media 内の :root で定義されている
    トップレベル :root だけ拾うと角丸が消える(実際に消えた)。@media内の :root も集めてスコープに再現する。
  3. 本文の継承値(PC 1.4rem/行間2、SP 1.3rem/行間1.75)は body から来る。sec ルートに書かないと
    全要素が 16px/normal になって全体が狂う

文字分割アニメ(GSAP SplitText)は静的に再現できる

  1. app.js の qi({...}) 呼び出しを grep すれば設定が全部わかる。jillion の場合:
    .c-ttl→words / .c-link→words(disableOnMobile) / l-header-navimv-navichars(baseClass指定)。
    blockClass は lineClass の -line-block に置換した名前。clone は -block-clone
    → 同じ入れ子(block > line > char)を静的HTMLで書けば JS 無しで動く。hover は is-hover を残しつつ
    :hover を併記(元はJSがclassを付けるので、そのままだと静的HTMLで動かない)。
  2. headless の --dump-dom では hover系の分割は走っていないdata-split-hover-initialized="true" だけ付く)。
    実DOMが欲しければ Playwright で page.hover() してから outerHTML を取る。

「完全再現」を機械で言い切る方法

  1. 元サイトと抽出版を同条件(1440/390・Playwright)で computed style 総当たり比較が効く。
    今回ヘッダーは 差分 0/105。残差はダミー化した英字の字形幅と 0.4px 以下のサブピクセルだけ、と説明できる。
  2. ホバーの一致は静止画では判定できないis-hover 付与後に transform を時系列サンプリング
    (100/200/300/450/650/900ms)してカーブごと突き合わせる。元 -8.2→-24.2→-29.1→-30.9→-31.2 に対し
    自作 -4→-22.6→-28.6→-30.8→-31.2 で一致と判定できた。
  3. 比較セレクタは必ずセクション配下に限定する。document.querySelector('.c-btn') はページ先頭の別ボタンを拾い、
    「色が違う」と誤検出した(実際は一致していた)。

サムネ検品に構造的に落ちるセクションがある

  1. ヘッダー/ナビのようなミニマルな帯は 色数・輪郭率 の閾値を超えられない(黒地に白文字だと色6種)。
    既存の header 系サムネも同じ見え方なので、高さを詰めて密度を上げる→それでも落ちるなら --force+目視でよい。
    ただし詰めてよいのは単独表示用に自分が足した余白だけ。元サイト由来の余白は動かさない。

貼っても崩れない形にするための追加ポイント

  1. FV系は .mv-cover .mv-logo .mv-naviposition:fixed。そのまま貼るとホストHP全体に居座るので
    セクション内 absolute に変える(見え方は同じ)。変更点は必ずコメントに残す。
  2. 元サイトは FV(固定背景)の中に次のブロックを内包していることがある(jillion は .mv の中に .philosophy)。
    </section> の対応だけで切り出すと後続セクションが丸ごと混入する。切り出し後に不要な子ブロックを削る。
  3. SVGアウトライン化されたタグライン・社名は文字を差し替えられない。ダミー化できない以上そのセクションからは外す
    (今回 FV のタグラインを外し「階段ナビだけ」にした ← 大串の指示とも一致)。

反映確認

  1. push直後の /sections は前の版が出る。今回 283件(未反映)→ 数分後 291件。
    gh run watch が success でもデプロイ反映にラグがあるので、件数か自分のidで判定する。

2026-08-18 表参道髙柳矯正歯科(j-ortho.jp)4セクション

FVに重なるヘッダーは「暗幕レイヤー」も一緒に抜く(大串FB「メニューがない」)

  1. 透過ヘッダーを抜いたのにメニューが読めない=暗幕の抜き漏れ。j-ortho の FV は
    <video> の上に <div class="layer">black.svg =全体30%の黒+上部240pxのグラデ)が乗っていて、
    白いナビ文字はこの1枚で読ませている。動画だけ下地にすると明るいフレームで文字が完全に溶けて
    「メニューがない」と見える。→ ヘッダー/FV系を抜くときは、下地の動画・画像だけでなく
    重なっている layer / overlay / ::after の暗幕を必ずセットで持ってくる

    チェック法: 明るいフレームで止めて白文字が読めるかを実機スクショで見る(測定値だけで判断しない)。

position:fixed の背景動画をセクション単体に落とすとき

  1. 元サイトが背景動画を position:fixed でページ全体に敷いている場合、セクション単体では
    position:absolute(親に position:relative; overflow:hidden)に置き換える。このとき、
    兄弟のコンテンツ帯に position:relative; z-index:1 を付け忘れると動画の裏に隠れる

    厄介なのは「静的な要素だけ消えて、position:relative を持つ子(ボタン等)は見えている」ため
    崩れに気付きにくいこと。今回はフッターのロゴとサブリンクだけが消えていた。
    背景メディアを absolute 化したら、同階層のコンテンツ帯すべてに position+z-index を明示

.sp / .pc の body クラスで出し分けるサイトの抽出

  1. j-ortho はメディアクエリではなく JS が body に sp / tablet pc / pc を付け替える方式
    max-width:767pxsp / 768〜1024pxtablet pc かつ viewport meta を width=1100 にしてPC版を出す / それ以上→pc)。
    → 抽出時は .sp X@media (max-width:767px) に、.pc/.tablet X@media (min-width:768px) に変換する。
    注意: 元は .sp の1クラス分だけ詳細度が高くて順序に依存しなかったが、メディアに移すと同詳細度になるので
    .sp 由来のブロックは base の後ろに置く(前に置くと効かない)。

有料フォント/Typekit は持ち込めない(今回の実測)

  1. Canela Web(参考元の自前ホスト woff2)は access-control-allow-origin が無く hotlink 不可
    (@font-face は CORS 必須・画像と違って落ちる)。Typekit の和文(dnp-shuei-gothic-gin-std)も
    https://use.typekit.net/<kit>.css がドメインロックで precondition failed
    → 英字見出しは Google Fonts で代替(Canela には Cormorant Garamond weight300 が近い)、
    和文は元CSSのフォールバック(游ゴシック/ヒラギノ)をそのまま残すのが素直。代替したことは報告に書く。

CSS抽出はブラウザで「サブツリーにマッチするルールだけ」を機械収集する(型として推奨)

  1. playwright で対象URLを開き、document.styleSheets を全走査して
    「セクションのサブツリーのどれかに matches するセレクタ」だけを残すと、
    reset.css・サイト全体CSS・未使用@font-face が構造的に混入しない(今回 40〜103ルール/実測99〜248行)。
    • :hover ::after 等は判定前に外す(マッチしないので落ちてしまう)
    • 1440 / 768 / 390 の3幅で回して和集合を取る.sp クラスやJS分岐で出る指定を取りこぼさない)
    • CSSOM の cssText相対URLを解決しないまま返ってくる → url(img/…) をテーマURL基準で絶対化する。
      (今回、絶対化の正規表現で \./? と書いて「. 必須」になり url("img/…") を素通りさせた。(?:\./)? が正)

他セッションと並行しているときの格納(terasu-lab)

  1. sections-data.json はコミットしない。CI(deploy.yml)が core/build_core.pybuild.py
    build_lab.py を回してコミット済みの entries から作り直すので、ローカルの集約結果を混ぜると
    他セッションの未コミット entries が一覧に載ってカードだけ生きて中身が無い状態になる。
    → 格納時に add するのは entries/<id>.jsonimg/sections/<id>.pngcode-previews/<id>.html だけ
    • 他セッションが作業中だと git add -A は当然アウトだが、git pull --rebase も submodule core
      ポインタ差分で止まる
      git -c rebase.autostash=true pull --rebase で回避できる。
    • 反映確認は /sections/(末尾スラッシュ必須)/sections はゼロバイトが返る。

作業ファイルは scratchpad に置かない

  1. /private/tmp/claude-501/.../scratchpadセッションを跨ぐと消える(今回2回消えて抽出をやり直した)。
    DOM・CSS・比較スクショなど数十分かけて作る中間物は ~/dev/_<案件>_work/ に置く

確認①は「Chromeで開ける実URL」で出す

  1. /section の手順は localhost だが、大串の恒久FBは「毎回Chromeで開ける実URLで送る」
    Lab は push で本番自動デプロイなので、code-previews/<id>.html だけを先にコミット&pushして
    実URLを出し、add_section.py(一覧への登録)はOKが出てから
    にすると両立する。

id は「空きを機械確認してから」使う(2026-08-18)

  1. 0817 だけで 220件超の既存セクションがあり、report-cards-saki-0817 / news-list-saki-0817 のような
    ありがちな id は既に埋まっている。気づかず同名で書くと他メンバーの完成済みHTMLを上書きする
    (今回2件やってしまい、HEADから復元された=自分の作業が消えた)。
    → 着手前に必ず空き確認する。埋まっていたら接頭辞を足す(例 sus- = そのページ由来):
    bash for id in <id1> <id2>; do [ -e code-previews/$id.html ] || [ -e entries/$id.json ] && echo "$id 衝突" || echo "$id 空き"; done
    add_section.py は既存 id で停止する--force を付けない限り安全)が、code-previews/<id>.html
    自分で書く工程には止める仕組みが無い。危ないのはHTMLを書く方

合わせるのは「CSSの宣言」ではなく「実ページの computed 値」(2026-08-18)

  1. CARTA の .sustainability__news-title は CSS上 font-size:1.125rem(=18px)なのに、実ページの
    computed は 16px / line-height 28px / gap 20px(=768px以下ブロックの値)だった。宣言をそのまま
    移すと元より文字が大きくなり、行数が増えてパネル高が 594→627px にズレる
    → 抽出したら必ず元サイトと自分のプレビューで同じ要素を getComputedStyle + getBoundingClientRect し、
    数値で突き合わせる
    。食い違ったら実測値の方に合わせる(見た目一致が正)。
    ダミー文も元と同じ文字数にすると折り返し行数まで一致し、箱の高さがピタリと合う。

slick 等のスライダーはサムネ検品に落ちることがある(2026-08-18)

  1. shoot_section.mjs の前処理 oneSlide() は「2枚目以降を display:none」にするが、slick は clone を
    含む8枚構成+track の transform でレイアウトを組む
    ため、これに当たると絵が崩れて
    「検品不合格(輪郭0.9%)」になる。--force で撮った画像は中身が正常だった。
    (判定は1回目の測定値 rich を使うので、2回目の撮り直しで改善しても不合格のまま=ツール側の仕様)
    --force で撮り、生成画像を必ず目視確認してから採用する。SP版・詳細版も同時に生成される。

Studio(studio.design)製サイトは <style media="..."> でブレークポイントを分ける(2026-08-18)

  1. Studio は CSS を <link> ではなく 複数の <style> タグに出し、レスポンシブは
    <style media="(max-width: 1280px)"> のように style タグの media 属性で切り替える
    @media ブロックが CSS 内に無い)。
    re.findall(r'<style[^>]*>(.*?)</style>') のように属性を捨てて中身だけ抜くと、
    全ブレークポイントの宣言が「素のルール」として一列に並び、後勝ちで最小幅の値が全部に効いてしまう

    (42px見出しが28pxになる等)。必ず media 属性も取り、@media <値>{ … } に包み直してから使う
    ※ 同じ理由で、ライブページを devtools/JS で計測すると 480px 相当の computed 値が返る環境がある
    計測が明らかにおかしい時は、自分の抽出物に元テキストを流し込んだ「参照版」をローカルで描画して
    元スクショと突き合わせる
    のが確実(折り返し位置まで1行ずつ照合できる)。

単独プレビューの body 標準マージン8pxで折り返しが1文字ズレる(2026-08-18)

  1. width:1440px; max-width:100% のセクションを <body>(既定 margin 8px)直下に置くと、
    実効幅が 1424px になる。86.6% コンテナ→4カラムに割るとカラム幅が約3px縮み、
    1行18文字が17文字になって行数が増える
    (元サイトと折り返しがズレる)。
    → 他セクションと同じ <body style="margin:0">(インライン属性) を付ける。
    CSS に body{} を書くとスコープ漏れ検証(grep)に落ちるのでインライン属性で書くのが正

同じリポで別セッションが git add -A していると巻き込まれる(2026-08-18)

  1. terasu-lab を複数セッションで同時に触ると、片方の git add -A が自分の作業中ファイルまで
    commit/push してしまう
    (今回は entry/サムネを作る前の HTML が先に commit された)。
    さらにその sweep コミットが 242ファイル削除・15,346行削除(他人の作業中ファイルの消滅)を
    含んでいることがあり、そのまま push すると本番から実セクションが消える
    → push 前に必ず git diff --stat origin/main..HEAD
    git diff --diff-filter=D --name-only origin/main..HEAD | wc -l で削除件数を確認
    する。
    危険なら push せず、plumbing で origin/main の上に自分のファイルだけ載せたツリーを作って
    git push origin <commit>:main
    (作業ツリー・インデックスに触らず安全):
    GIT_INDEX_FILE=/tmp/idx git read-tree origin/maingit hash-object -w <file>
    git update-index --add --cacheinfo 100644,<sha>,<path>git write-treegit commit-tree
    ※ 今回は結果的に相手の push に自分の5ファイルが同内容で含まれていたので追加pushは不要だった。
    git diff --stat origin/main <tree> が空=もう反映済み、の判定に使える。
    ※ 集約 sections-data.jsonCI が core/build_core.pybuild.py で再生成するので
    ローカルで commit しなくてよい(他人の未完成 entry を巻き込まないためむしろ staging しない)。

このPCは node が PATH に無い / headless Chrome が固まる(2026-08-18)

  1. check_responsive.mjs / shoot_section.mjs は node が要るが、このPCは PATH に node が無い。
    export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH" で v24 が使える。
    また Google Chrome --headless=new --screenshot無応答で固まることがある
    (2回目以降・プロファイル指定を変えても再発)。ブラウザペイン(preview_start)で代替すれば止まらない。

2026-08-18 あかつき証券 4件(会社概要/沿革/代表メッセージ/経営方針)

同一リポで他セッションが並行作業中は「別ワークツリーからpush」が唯一安全

  • 症状: git pull --rebase --autostashcould not detach HEAD で失敗。原因は他セッションがstage済み・未コミットのファイル(cases-slider/footer-panel/header-nav等)が、リモートの新コミットと同じパスで衝突していたこと。git merge でも同じ理由で拒否される。
  • ここで reset --hard / checkout -f / stash を使うと他セッションの作業を破壊する。絶対にやらない。
  • git worktree add --detach <tmp> origin/main で別ワークツリーを作り、そこへ自分のコミットだけ cherry-pick して git push origin HEAD:main。メインの作業ツリーには一切触れずにpushできる(作業後 git worktree remove --force)。
git worktree add --detach /tmp/lab-push-wt origin/main
cd /tmp/lab-push-wt && git cherry-pick <自分のcommit> && git push origin HEAD:main
cd - && git worktree remove --force /tmp/lab-push-wt

git add の前に index.lock を確認する(他セッションのgitと衝突する)

  • fatal: Unable to create '.git/index.lock': File exists が実際に出た。他セッションがまさにgit操作中という意味なので、rm せず数秒待って再試行する(今回は3秒で解放)。

git commit -- <paths> のパス指定コミットで他人のstagedを巻き込まない

  • 着手時点で他セッションのファイルが既にstagedされていたgit commit すると巻き込んでしまう。
  • git commit -m "..." -- "${PATHS[@]}"自分のパスだけコミット(他人のstagedはindexに残る=妨害しない)。
  • zshでは PATHS="$PATHS a b" の文字列連結だと1つのpathspecとして渡り did not match any file(s) になる。必ず配列 PATHS+=(...)"${PATHS[@]}" で渡す。

font-feature-settings の引用符が全角なCSSはコピーしない(元サイト側のバグ)

  • 元CSSが font-feature-settings: “palt”;(全角ダブルクォート)で実際には無効だった。これをそのまま持ってくると、こちらでは有効化されてしまい字送りが元と変わる(=再現ミス)。
  • → 無効な宣言だと分かったら入れないのが「視覚一致」として正しい。

プレビューHTMLの body は CSSルールでなくインラインstyleでリセットする

  • body{margin:8px} のUA既定が残ると、元サイトとの左端位置が8pxずれる。
  • → 既存セクションの家風どおり <body style="margin:0;background:#fff;"><style> 内に body{} を書くと "貼ると崩れる" 検証(スコープ漏れgrep)に引っかかるのでインラインが正解

ページ本文カラムの余白は「ラッパーCSS」ではなく sec- の padding に寄せる

  • 元は .contentsArea_side{width:18%}.contentsArea_main{padding-right:7vw} の2カラム構造。ラッパーごと持ち込むと外来CSSになる。
  • sec-<id>{padding: 5vw 7vw 6vw 18%} としてスコープ内で同じ余白を再現(実測1440pxで左259px=元と一致)。SPは @media24px に戻す。

2026-08-18 スタディサプリ ブランドサイト(Gatsby製)フッター+ドロワー 2件

  1. ** スコープ用リセットを二重クラス(.sec-X.sec-X :is(...))にすると本体CSSを潰して全崩れする.sec-X.sec-X :is(footer) は特異性(0,2,1)で、本体の .sec-X .module(0,2,0) に勝ってしまい padding:400px 等が0になった(フッターが239pxに潰れた)。→ リセットは元サイトと同じ弱さ(.sec-X :is(...)=(0,1,1))に保つ**。2026-08-16の「二重クラスで強くする」はp/img限定の話であり、レイアウトを持つ要素へ広げてはいけない。
  2. Gatsbyのハッシュclass(styles-module--<base>--<hash>)はリネームが機械的にできる。ファイル内の全トークンを収集→base名へ一括置換(showis-showopenis-open、社名由来 recruitcorpmark だけ手当て)。base名の衝突を事前チェックしてから置換すれば安全。
  3. rem基準 html{font-size:62.5%}sec-X{font-size:10px} では直らない(remは常にdocument root)。全CSSの Nremcalc(N * var(--sec-rem)) に機械変換し --sec-rem:10px を sec 上で定義するのが正解(2026-08-17 Vivooの知見がそのまま効いた)。
  4. html,body{margin:0} をプレビュー用に足すのは禁止(スコープ漏れ検査に引っかかる)。既存97件も html/body ルールは0件。body既定マージン8pxは normalize_thumbs.py が余白トリミングするので実害なし。
  5. 原本との照合は「同幅でcomputed styleをdiff」が速い。ただし [class*=logo--] 等の緩いセレクタは原本ページのヘッダー側を拾って偽の差分を出す。セクションのルート要素の高さ(985/1076/1091px)が3幅で一致すれば実質OK。
  6. ** terasu-lab は同時セッションが多く git pull --rebase がほぼ通らない(未コミットの他人ファイル+リモート側に同名untrackedが来て could not detach HEAD)。→ git worktree add /tmp/<一意名> origin/main --detach で隔離し、そこに自分の6ファイルをコピー→build.py→commit→push が確実。/tmp/lab-push 等は他セッションが既に使っているので名前を一意にする**。
  7. worktreeでは push前に必ず build.py を撮り直して「消失0件」を機械確認する。リモートの sections-data.json は集約漏れ(entryはあるのにデータに無い/逆に実体の無いidが残る)が発生しており、rebase後に再ビルドすると自動で是正される。
  8. ローカルプレビューのサーバーは nohup で切り離して起動する& だけだとツール呼び出し終了やセッション終了で死に、大串が開こうとした時に落ちている(今回2回やり直しになった)。
  9. 同じidを過去セッションが既にコミットしている場合がある(今回 bluepanel-drawer-saki-0817.html が生プレビューだけコミット済みだった)。リネーム前に git statusD が出ていないか確認し、出たら git checkout -- で戻す。

2026-08-18 LENZ DX 4件( id衝突で他サイトに上書きされた事故)

  1. ** <主題>-<種別>-<担当>-<MMDD> は同日・同担当だと衝突するflow-steps-saki-0817 / contact-cta-saki-0817 で作った2件が、同じ日の別セッション(AI model・青木茂建築工房)に同じidで上書きされ、大串の確認時に「①と②違うHPのじゃない?」となった。担当名が入っていてもid衝突しない保証にはならない(同じ担当が1日に何十件も足す運用のため)。
    id は主題を具体的にして一意性を稼ぐflow-stepsflowsteps-arrowcontact-ctacontact-darkcard のように見た目の特徴を主題に含める**)。作る前に ls code-previews/ entries/ | grep <候補id> で衝突チェックする。
  2. 大串の確認待ちの間に上書きされる(確認①〜格納の間が最も危険)。プレビューを作ったら放置せず、OKが出たら速やかに entries 登録まで進める。entries に登録されていない生プレビューは"誰のものでもないファイル"扱いで消えやすい。
  3. 他セッションが同リポで作業中なら git add -Agit stash も絶対にしない(今回は未コミット230件が並走していた)。→ 自分のファイルだけをパス指定で git add。gitはリネームも自動検出してくれる(R code-previews/旧.html -> 新.html)。
  4. CIが build_core.pybuild.pybuild_lab.py を自前で回すので、集約JSON(sections-data.json 等)はコミットしなくてよい。むしろコミットすると他セッションの未コミット分を巻き込んだ集約結果が載って事故る。コミットするのは entries / code-previews / img/sections の自分の分だけ
  5. 自分のCI実行が cancelled になっても失敗ではない。後続pushがconcurrencyで前の実行を打ち切るだけ。→ git merge-base --is-ancestor <自分のsha> origin/main最新mainに含まれているかを見て、最新の実行の完了を待つのが正しい判定。
  6. 動画からのフレーム抽出は ffmpeg / swift / qlmanage がどれも使えない環境がある。→ <video> を貼った確認用HTMLをローカルサーバーに置き、ブラウザで再生してスクショが確実(python http.server は Range 非対応でシークが効かないので、play() して待ってから撮る)。
  7. ブラウザペインは非表示になるとSwiper等のautoplayが止まる。「動いていない」と誤判定しかけた。→ 動作確認は headless の --dump-domtranslate3d(...) が進んでいるかを機械判定するのが速くて確実。

2026-08-18 groxi株式会社(groxi.jp)円形サービスカード/パートナーロゴ/ニュース 3件

  1. ** 参考元が自社ホストしている独自フォントは CORS 未許可でまず読めない。groxi の Campton/assets/fonts/3969AE_1_0.woff2)は curl -sI -H "Origin: …"access-control-allow-origin が出ない=クロスオリジンでは適用されない。→ 着手前に curl でCORSを確認し、無ければ同系のGoogle Fonts(ジオメトリックサンセリフなら Poppins、日本語 NotoSansCJKjp は Noto Sans JP**)に置換する。フォント以外(DOM・CSS値・JS設定)は完全再現のままにできるので、置換対象を「フォントだけ」に閉じ込めるのがコツ。
  2. 巨大CSS(209KB/6294行)からの抽出は自作パーサが速い。ブレース深度を追ってトップレベルのルールを切り出し、セレクタ文字列に .s-service 等が含まれるものだけ残す。@media は中を再帰して同じ判定をかければ、PC/SP両方の指定を取りこぼさない。(落とし穴=while … else: break を書くと最初の { で外側ループを抜けて出力が空になる)
  3. .index .s-service … のようなページ限定プレフィックスは sec-<id> に置き換えるだけでスコープ化が済む。元が「ページclass配下」で書かれているサイトは、外来グローバルCSSを持ち込む必要がほぼ無く、100〜250行に収まる。
  4. ** headless Chrome の --window-size=390,700 は効かない(最小幅500でレンダリングされ、画像だけ390にクロップされる)。SPの「進捗バーが画面外に出ている」はこの見かけ上の切れで、実測すると 500px 幅での座標だった。→ SPの判定は tools/check_responsive.mjs(playwright・390で正しく測る)を信用する**。どうしても位置を知りたい時は、一時コピーに getBoundingClientRectdocument.title へ書き出すスクリプトを差し込み --dump-dom で読むのが軽い。
  5. 白基調・低密度のセクション(余白多め+12px級の細字)は shoot_section.mjs の検品で誤検知落ちする(今回 news が「色14種/輪郭3.0%」で不合格)。→ --force で撮ってから必ず自分で目視し、見出し・リスト・ボタンが読めていれば採用してよい。判定を緩めるのではなく「force+目視」で運用する。
  6. このPC(咲輝PC)は node が PATH に無いexport PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH" を付ければ check_responsive.mjs / shoot_section.mjs が動く。
  7. ** 別セッションが同一ワークツリーで並行作業していると、自分の code-previews/*.html が相手の一括コミットに巻き込まれる(今回、書いた直後に相手が commit → 自分の3ファイルが追跡済みになっていた)。慌てて git add -A や stash をしないこと。→ git worktree add した隔離環境で git checkout -B <一意名> origin/main → 自分のコミットだけ git cherry-pickgit push origin <一意名>:main**。これなら相手の未push分を巻き込まずに自分の分だけ本番へ出せる。git add が一時的に did not match any files で落ちるのも相手のgit操作との競合なので、数秒おいて再試行すればよい。
  8. Swiperの初期化値はバンドルJSから grep で取れるslidesPerView:e,spaceBetween:r,centeredSlides:i の実装を辿ると既定 slidesPerView:1、進捗は (activeIndex+1)/3.progress-valuescaleX() していた。→ CDN の swiper-bundle JSだけ読み、CSSは使う分(.swiper-container/.swiper-wrapper/.swiper-slide)を sec-<id> 配下に自前で書けば、ライブラリCSSをグローバルに撒かずに動きを完全再現できる。

2026-08-18|NewsPicks Brand Design 6セクション(実地の学び4件)

1. セクション単体で「スクロールできない」バグ=ページ級CSSの畳み込み

  • 症状: プレビューを開くとホイールが効かない(大串FB「4スクロールできない/6スクロールできない」)。見た目は完璧なので目視では気づけない。
  • 真因: 抽出時に html{overflow-y:scroll} body{overscroll-behavior:none}sec-<id> ルートへ畳み込んでいた。スコープルートがスクロールコンテナ化してホイールがページへ伝播しない
  • 対策(恒久): スコープルートに集約する宣言から overflow / overflow-x / overflow-y / overscroll-behavior を必ず除去する(opacity:0 の除去と同じ扱い)。これらはページ級の設定でセクションには不要。
  • 機械検証: 目視では絶対に落ちる。Playwrightの実ホイールイベントで検証するpage.mouse.wheel() を数回→window.scrollYが増えるか)。window.scrollTo() は効いてしまうのでテストにならない(ここが罠)。

2. body{opacity:0}(ロード演出ゲート)で真っ白になる

  • 参考元は body{opacity:0} → JSで js-loaded を付けて表示する型が多い。そのままスコープ化するとプレビューが真っ白
  • 対策: ロード後状態を正として opacity:0 を除去。同型で .js-loaded .s-top など body常時クラスは先頭から剥がして常時適用にする。

3. 他ドメイン参照はCORSで死ぬ(画像は生きるがマスク/JSONは死ぬ)

  • <img src> は他ドメインでも表示されるが、CSS mask-image のSVGLottieの path: JSON は CORS で読めない(元サーバーに Access-Control-Allow-Origin なし)。矢印アイコンが消える・イラストが出ない。
  • 対策: CSS参照アセット(svg/png/woff)は data URI 化、Lottieは animationData: で JSON を埋め込みpath: を使わない)。画像URLは従来どおり参考元のまま。
  • 併せて CSSの相対 url(../img/...) は絶対URL化が必須(CSSは /asset/css/ 配下配信なので ../ の基準が変わる)。

4. --font-device-width-unit 系のJS注入変数を拾い忘れると文字サイズが全崩れ

  • NewsPicks系は font-size: calc(... var(--font-device-width-unit) ...) を多用。この変数は startup.js が documentElement に注入しており、CSSだけコピーすると全部16pxに落ちて巨大見出しが消える
  • 対策: 変数注入をスコープ版で移植(Math.min(window.innerWidth, 1440)sec-<id>setProperty)。html{font-size:10px} と同じ「ベース設定は拾ってスコープ化」の一種。
  • mac.css の存在に注意: Mac/iOS UAのときだけJSが後から注入する調整CSS。拾わないとMacでの見た目が僅かにズレる。sec-<id>.is-macua としてスコープ化し、UA判定でクラス付与。

5. 並行セッションと作業ツリーを共有しているときのpush

  • 同じPCの別セッションが同一リポで数分おきにcommit/pushしていると、git pull --rebase が「unstaged changes」で止まり、git stash他セッションの作業を壊すので使えない。
  • 対策: git worktree add --detach <tmp> origin/main → 自分のコミットだけ cherry-pickbuild.py 再生成 → push origin HEAD:main他人の未pushコミットを巻き込まずに自分の分だけ出せる。終わったら git worktree remove
  • CIが cancelled になっても問題ない(後続runが最新mainからビルドするため)。慌てて再pushしない。確認は本番 /sections を実際に開いて掲載を見る

2026-08-18 あかつき証券トップ 5件(大串FB2回=どちらも「左に寄りすぎ」)

6. セクションの左余白を作っている「隣の列」を一緒に持ってこないと左に寄る(同じ指摘を2回もらった

  • 症状: 抜き出したセクションが元サイトより左に詰まる。大串FB「③左につまりすぎ」→直した直後に「1のタイトル左によりすぎ」=同型の見落としを連発した。
  • 真因: あかつき証券トップは <div class="contentsArea"> の中が contentsArea_side(幅13%の追従ナビ列)+ contentsArea_main の2カラム。本文の左余白は padding ではなく隣の列の存在で作られている。contentsArea_main だけ包んで持ってくると、その13%が丸ごと消えて左に寄る。
  • 対策(着手時のチェックに入れる): 本文セクションを切り出す前に、そのセクションの左右の余白が「自分のpadding」か「兄弟要素(サイド列/グリッドの空きカラム)」かを必ずDOMで確認する。兄弟要素由来なら空の <div class="サイド列"> を一緒に入れてCSSも拾う(中身は空でよい・幅指定だけ効けばよい)。
  • 見分け方: 親が display:flex / grid で、セクション側に大きな padding-left が無いのに元サイトでは寄っている → 兄弟列パターン

7. 「追従サイドナビ」等の小さいセクションはサムネ検品(shoot_section.mjs)に落ちる

  • 症状: ⛔ 中身が写らない(色7種/輪郭0.2%・2回試行) で保存されない。ナビ自体は正しく描画できている。
  • 真因: デモを「ナビ+高さ稼ぎの空div」で作るとページのほぼ全部が白になり、色数・輪郭量の検品ゲートに引っかかる。セクションが小さいほど起きる(サイドバー・パンくず・ページネーション等)。
  • 対策: --force で通さない。元ページと同じ文脈を実コードで足す=そのナビが指している本文セクション(今回は Mission と News の実抽出コード)を隣の列に入れる。捏造ではなく同一ページからの実コード流用なので品質基準を下げずに済み、追従の動き自体もデモとして成立する(スクロールでナビの現在地が移るのを見せられる)。
  • 併せて IntersectionObserver-current を移す実装を入れると、静的プレビューでも元サイトと同じ挙動になる。

8. このPCは node が素のPATHに無い(毎回ハマる)

  • node tools/check_responsive.mjscommand not foundChatGPT.app同梱nodeを使う:
    export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"(v24系)。/section の検品・撮影コマンドは全部これを前置きしてから実行する。

9. 全画面ドロワーはタブレット(768)で必ず検品に落ちる(元サイトのPC固定幅が原因)

  • .hamMenu{ left:18%; width:calc(100% - 18%); min-width:1000px } のようにPC版を横スクロール前提で組んでいるサイトが多い。元サイトのSP用メディアクエリは max-width:767px なので、768pxちょうどの帯だけPC指定が生き残ってはみ出す
  • 対策: @media (min-width:768px) and (max-width:1023px)その帯だけ min-width:0、必要なら列数を4→2に落とす+器に overflow-x:clipPC1440の見た目は1mmも変えない(撮り直したPNGがバイト単位で同一になることを確認すると確実)。

10. 他セッションが同じリポで並行作業していると push できない → worktree で回避する(2026-08-18 実地)

  • 症状: git pull --rebasecannot rebase: You have unstaged changes / could not detach HEAD で必ず失敗。git stash も exit 1。作業ツリーに他セッションの未コミット変更が200件超あり、触ると相手の作業を壊す。
  • やってはいけない: git add -A/他セッションの変更を stash・reset する(同時作業日は特に危険)。
  • 正解=現在の作業ツリーに一切触れずに push する:
    bash cd ~/dev/terasu-lab git worktree add --detach /tmp/lab-push-<日付> origin/main # クリーンな別ツリー cp <自分の成果物> /tmp/lab-push-<日付>/{code-previews,entries,img/sections}/ cd /tmp/lab-push-<日付> && python3 build.py # 集約JSONはここで再生成 git add <自分のファイルだけ> && git commit && git push origin HEAD:main git worktree remove /tmp/lab-push-<日付> --force
  • push は必ずリトライループにする(相手が数十秒おきに push してくるので1回目は non-fast-forward で弾かれる): fetch → rebase origin/main → build.py → amend → push を最大5回。
  • CI が cancelled になっても慌てない: concurrency: deploy-terasu-lab で後続 push の run に集約されるだけ。自分のコミットが origin/main の祖先かgit merge-base --is-ancestor <sha> origin/main)を見て、あとは本番URLの200と実表示で確認する。run の成否だけで判断しない。

11. 確認①用のローカルサーバーはセッションが切れると落ちる(大串が「リンク開けない」と言う真因)

  • python3 -m http.server を run_in_background で立てても、Claude Code のセッション再起動でプロセスごと消える。大串側からは「さっきのURLが開けない」に見える。
  • 対策: ①nohup python3 -m http.server 8518 > /tmp/x.log 2>&1 < /dev/null & disown でデタッチする(setsid は macOS に無い=使うとエラー)。②同時に file 直開きの手段も渡す=セクションは1ファイル自己完結なので open -a "Google Chrome" "<絶対パス>" で全機能動く。③再送を求められたらまず curl -o /dev/null -w "%{http_code}" で生死を確認してから送り直す。

10. 同じ日に複数セッションが走ると id衝突でファイルが相互上書きされる(2026-08-17 実地・重大)

  • 症状: 一度「完全再現できました」と確認まで通したプレビューが、あとで開くと CSSが1つも効いていない素のHTML(リンクが青い下線、レイアウト崩壊)になる。別の箇所は無事なので「一部だけ崩れた」ように見える。フッターに至ってはまったく別のサイト(証券会社)のフッターに置き換わっていた。
  • 真因: <内容>-<担当>-<日付> のidは、同じ担当(saki)・同じ日(0817)に別セッションが同じ内容名footer-dark news-info recruit-cta など汎用語)を使うと衝突する。さらに他セッションが git add -A でcommitするため、自分の作業中ファイルが他人のコミットに巻き込まれ、あとから別内容で上書きされる
  • 対策(恒久):
    1. 内容名を具体的にするfooter-dark ではなく footer-3col-snsnews-info ではなく newslist-thumb)。汎用語ほど衝突する。
    2. 着手前に ls code-previews/ | grep <候補id> で空きを確認してからidを決める。git ls-files でcommit済みかも見る(既にcommitされていたら他人の作品=別名にする)。
    3. 生成直後にCSSパースを機械検証する(下記11)。目視や1440pxのスクショだけでは「壊れていない」と誤判定する。
    4. push は自分のファイルだけ git add <path...> で明示し、git add -A は使わない。作業ツリーが他セッションの変更で汚れているときは git worktree add <tmp> origin/main --detachcherry-pickpush origin HEAD:main で、他セッションのローカル作業に一切触れずに出せる。

11. 「崩れていないか」は cssRules 数で機械判定する(目視では見抜けない)

  • 壊れたファイルは document.styleSheets[n].cssRules.length1(=先頭ルールだけ読まれて以降が全部無視)になる。正常なセクションは 50〜180 程度。
  • 検証コード(Playwright・生成直後とデプロイ後の両方で回す):
    js const r = await page.evaluate(() => { const sheets = [...document.styleSheets].map(s => { try { return s.cssRules.length } catch(e) { return 'X' } }); const root = document.querySelector('[class^="sec-"]'); const f = root && root.firstElementChild; return { rules: sheets.filter(x => x!=='X').reduce((a,b)=>a+b,0), display: f && getComputedStyle(f).display }; });
  • displayblock(本来 flex)なら CSS が効いていない。rules<10display:block なら即アウトとして扱う。

12. STUDIO製サイト(class="sd appear" / data-s-<uuid>)の抽出手順

  • 見分け方: DOM に .sd appear が大量、<style> 内が .sd[data-s-<uuid>]{...} 形式、__NUXT_DATA__ あり。CSSは 要素1つ=uuid1つに対応するので、セクション内の data-s- / data-r- 属性名を集めて、その属性名を含むルールだけ拾えば過不足なく抽出できる(reset.cssを丸ごと持ってくる必要がない)。
  • ルート要素自身の data-s- を拾い忘れるとセクションの padding/justify-content が丸ごと落ちて高さが変わる。for el in [node] + node.find_all(True) のようにルートを明示的に含めること(find_all は子孫のみ)。
  • 画像は lazy-load で src が空SVGのプレースホルダ。--dump-dom では実URLが取れないので Playwrightで全域スクロールしてから page.content() を取る(#service 等のアンカー付きURLで開くだけでは発火しない)。
  • 元サイトはページ最上位の .sd{overflow:clip} ではみ出しを切っている。抽出後は器に sec-<id>{ overflow-x: clip } を付けると、タブレット幅の検品に落ちずPC表示も元のまま保てる。
  • ベース設定は :where(.render-canvas) … に入っている。:where(.render-canvas)sec-<id> に置換するだけでスコープ化が完了するhtml/body/* へのページ級リセットが元から無いので安全)。

13. id衝突の事前チェック(2026-08-18 実地で事故りかけた・恒久)

  • 同じPC・同じ担当だと一般的なid(about-2col news-slider contact-band 等)は既に使われている。今回 goinc.co.jp の7件を -saki-0817 で作ったところ、6件が既存の別サイトのセクションと同名で、code-previews/*.html を上書きしてしまった(add_section.pyentries/ しか衝突判定しないので素通りする)。
  • 着手前に必ず両方チェックする:
    bash cd ~/dev/terasu-lab for id in <候補id...>; do ls code-previews/$id.html entries/$id.json 2>/dev/null && echo "⛔ $id は既存" || echo "OK $id" done
  • 上書きしてしまったら git checkout -- code-previews/<id>.html で復元し、日付部分を当日に変える-0817-0818)だけで衝突を回避できる。ファイル内の sec-<id> 参照も sed 's/-saki-0817/-saki-0818/g' で一括置換すること(クラス名にidが入っているので置換漏れると無スタイルになる)。
  • idの日付は「作業した実日付」にする。推測で前日の日付を付けると、その日の既存idと当たりやすい。

14. push は worktree 経由(他の人の未コミット作業を巻き込まない)

  • terasu-lab は複数セッションが同じローカルリポで同時に作業していることがあり、git status に他人の未コミット変更(_afloat_work/ や別セクション)が大量に出る。
  • git add -A は厳禁(既知)だが、それに加えて git pull --rebase も "unstaged changes" で失敗するgit stash は他セッションが編集中のファイルを一時的に消すので危険。
  • 安全な型=自分のコミットだけ worktree で cherry-pick して push(作業ツリーに一切触らない):
    bash git fetch origin git worktree add --detach ~/dev/.section-work/lab-push origin/main cd ~/dev/.section-work/lab-push && git cherry-pick <自分のsha> && git push origin HEAD:main cd - && git worktree remove ~/dev/.section-work/lab-push --force
  • sections-data.jsonbuild.pyentries/ 全部(他人の未コミット分も含む) から再生成するので、コミットに含めない(含めると他人の作業中セクションが混入する)。本番はCIが最新mainからビルドするので影響なし。

15. 動画(画面収録)は実Chromeでフレーム抽出して必ず中身を見る

  • 2026-08-18、動画に映っていた7つ目のセクション(WORKS)を見落として6件で完成報告してしまった(大串「7セクションなかった?」)。ffmpegが無い=動画を見ない、は不可
  • このMacには ffmpeg が無いが、Playwright の channel:'chrome'(実Chrome=H.264対応)で <video> を読み、currentTime を送って canvas に描き toDataURL で抜ける。Playwright同梱Chromiumはコーデック非対応なので channel:'chrome' が必須。
    js const b = await chromium.launch({ channel: 'chrome' }); // 実Chrome await p.evaluate(async t => { const v=document.querySelector('video'); await new Promise(r=>{v.onseeked=r; v.currentTime=t;}); /* canvasに描いてtoDataURL */ }, t);
  • 抜いたフレームは PILで4×4のコンタクトシート1枚に連結してから見る(画像を何枚も読むとコンテキストが膨らむ)。

16. 「JSが表示時に切り替えるスタイル」はCSS抽出では絶対に再現できない(GO/goinc.co.jp で全部踏んだ)

CSSルールを正しく抜いても、元サイトがGSAP等で実行時に書き換えている値は初期値のままになる。単体で開くと「見えない/色が違う」になる。
- 症状と正体(goinc WORKS の実例):
| 見え方 | 正体 |
|---|---|
| 白背景に黒文字(本当は黒背景に白文字) | .home-works{color:#fff} だが背景の黒は body 側。切り出すと白地になる → セクション自身に黒背景を持たせる |
| 見出しだけ黒いまま | .home-works-title{color:#000; transition:color 1s}スクロール時にJSが白へ変える設計 → 切替後の色を初期値として焼く |
| 大画像が出ない | data-src の遅延読み込み+GSAPが付けた インライン visibility:hidden |
| 全面が黒く覆われる | 出現アニメ前の .cover{opacity:.5}transform:translateY(-…) が残っている |
- 対処の型=「元サイトを表示状態にして computed style を全要素比較し、差分を焼き込む」。色だけでなく opacity / transform / visibility も必ず比較項目に入れる(色だけ見て「差分なし」と誤判定した)。
- インラインstyleは background-image だけ残して他は捨てるのが安全:
python keep = [d for d in style.split(';') if d.split(':')[0].strip() in ('background-image','background')]
- 比較の落とし穴: 元サイト側を scrollTo(0,0) に戻して測ると演出前の値を拾う。そのセクションが画面に入った状態で測ること。

17. ダミー文は「文字数」でなく「表示幅」で合わせる

  • 原文が英数混じり(The Breakthrough Company GO、…)だと、同じ文字数の全角主体ダミーは1.5〜2倍の幅になり、狭い幅(768)で行数が増えてレイアウトが変わる。
  • 全角=1.0 / 半角=0.5 で表示幅を計算して合わせ、PC(1440)とタブレット(768)の両方で行数一致を機械確認する:
    python w = lambda s: sum(1.0 if unicodedata.east_asian_width(c) in 'WFA' else 0.5 for c in s)
  • 行数の確認は el.getBoundingClientRect().height / parseFloat(getComputedStyle(el).lineHeight)

18. サムネ検品の誤判定(白・グレー基調のセクション)

  • shoot_section.mjs の「中身が写らない(色N種/輪郭N%)」は、CONTACT帯やニュース一覧のような白地+文字だけのセクションで誤判定する(実際は正しく描画されている)。
  • 自分でスクショを撮って中身を目視してから --force で保存する。目視せずに --force を使わない。

19. 作業ファイルはスクラッチパッドに置かない

  • /private/tmp/claude-501/.../scratchpadセッション中に消えることがある(実際に2回消え、抽出済みJSONとビルドスクリプトを失って作り直しになった)。
  • /section の中間生成物(DOM/CSS/matched.json/build.py)は ~/dev/.section-work/<サイト名>/ に置く。

20. rem基準は「最後に書かれた指定」が正(拡大バグの真因・2026-08-18 neie)

  • neie.jp の :root10px2001px:14px1601px:12px768px:10px767px以下:10px の順。
    CSSは後勝ちなので min-width:768px の 10px が拡大指定を全部打ち消していて、実効値は全幅で 10px 固定(12/14pxは死んだコード)。
  • ここで 1601/2001 の指定だけを拾うと、広いディスプレイで1.2〜1.4倍に拡大する。大串の画面(1600px超)で
    「大きく表示されすぎてる」と即バレた。狭い画面で確認していると気付けない。
  • rem基準を移植するときは :root/html の font-size 指定を出現順に全部列挙して、後勝ち後の実効値を確定させる:
    bash python3 -c "import re;css=open('site.css').read();[print(m.start(),m.group(0)[:110]) for m in re.finditer(r'@media[^{]*\{:root\{[^}]*font-size[^}]*\}\}|(?<![-\w]):root\{[^}]*font-size[^}]*\}',css)]"
  • 検証は 1440だけでなく 1700 / 1920 / 2100 でも元サイトと computed値を機械比較する(Playwrightで両方開いて差分を出す)。
    1440だけ見ていると今回のバグは通過してしまう。
  • スコープ化の書き方: --u:10pxsec-<id> に置き、CSS中の Nremcalc(N * var(--u)) に機械置換する。
    sec-<id>{font-size:10px} だけでは rem はルート要素基準なので効かない(既存セクションにこの取りこぼしがある)。
  • ついでに sec-<id>{font-size:calc(1 * var(--u))} も入れる(元は body が root の 10px を継承しているため、
    入れないと font-size 未指定の器が 16px になり継承値がズレる)。

21. push は git add -A しない(他セッションの作業を巻き込む・2026-08-18 実害)

  • /section の手順どおり git add -A && git commit && git push をやったら、同じMacで並行動作していた別セッションの
    未コミット作業(別セクション25件・ルート直下の _tmp_*.mjs 4本・node_modules のgitlink削除)まで一緒に本番へ公開
    された。
  • 幸いCIは success・全ページ200で壊れてはいないが、他人の作業を勝手に公開したのは事故。
  • push前に必ず git status --short を見る。汚れていたら git add -A を使わず、自分のidのファイルだけを列挙して add する:
    bash git add entries/<id>.json code-previews/<id>.html "img/sections/<id>"*.png sections-data.json
  • 集約JSON(sections-data.json)は全件再生成なので、他セッションの分が混ざるのは避けられない。
    同時作業の日は、集約とpushを片方に寄せるか、相手のセッションが止まってから流す。

22. shoot_section.mjs が落ちたら Playwright のブラウザを入れ直す

  • browserType.launch: Executable doesn't exist at .../chromium_headless_shell-XXXX/Playwright更新でブラウザが未取得なだけ。
    bash cd ~/dev/terasu-lab && npx playwright install chromium
  • npm i 相当は不要。入れ直せばそのまま撮影できる。

23. 日本語の改行位置がズレる=html{line-break:strict;overflow-wrap:anywhere}p{word-break:auto-phrase} の拾い漏れ(2026-08-18)

  • NOT A HOTEL ARCHITECTS の Project セクションで、CSSは全項目一致なのに本文だけ 5行→4行になった。
    原因は元サイトのベース設定 html{line-break:strict; overflow-wrap:anywhere}p{font-feature-settings:"kern" off; font-kerning:none; word-break:auto-phrase} の拾い漏れ。
    word-break:auto-phrase(文節で折り返す日本語向け指定)が無いと改行位置が変わり行数が増減する=見た目が変わる。
  • 対処=スコープ化して残す(html/body を素で書かない):
    css .sec-<id>{line-break:strict;overflow-wrap:anywhere} .sec-<id> p{font-feature-settings:"kern" off;font-kerning:none;word-break:auto-phrase}
  • font-size や line-height の比較だけでは気づけない。抽出の検証では height(=行数)も必ず比較対象に入れる。
    行数差が出たら word-break / line-break / overflow-wrap / font-feature-settings を computed 値で突き合わせる。

24. 動画(画面収録)は「どのセクションか」を必ずフレーム抽出して確かめる(2026-08-18)

  • スクショ2枚(Process・FAQ)+動画1本で依頼が来たとき、動画を見ずにスクショ2件だけ作って提示 →「動画のは?」と指摘された。
    動画は3つ目の別セクション(Project)を写していた。 素材が複数種類あるときは、全部に目を通してから箇所を確定する
  • このMacに ffmpeg は無い。Playwright(terasu-lab の node_modules)でフレームを抜くのが速い:
  • file:// の video を canvas に描くと Tainted canvases may not be exported で失敗 → ローカルHTTPで配信する(python3 -m http.server 8090 --directory /tmp)。
  • currentTime を代入して seeked を待つ方式は全フレーム同じ画になることがある。
    play() しながら requestVideoFrameCallback で連続キャプチャするのが確実(meta.mediaTime で間引く)。

25. /tmp は他セッションと共有=上書きされる(2026-08-18)

  • /tmp/dom.html に参考サイトのDOMを置いて作業していたら、並行セッションが別サイトのDOMで上書きしていて、
    終盤に og:site_name を読んだら別の会社名(StockSun)が出た。危うく誤った社名で登録するところだった。
  • 中間ファイルは id を含む名前にする(/tmp/dom_<id>.html)か、セッション専用のスクラッチパッドに置く。
    社名は core/data/reference-sites/<slug>.jsonname から取るのが最も安全(参考HP集に登録済みなら確実)。

26. caption の18字制限に掛かる社名は、既存の同サイト登録に合わせる(2026-08-18)

  • NOT A HOTEL ARCHITECTS は「分類|会社名」が26字になり add_section.py が弾く。
  • 勝手に略さず、同じサイトの既存セクションの caption を見て表記を揃える:
    bash python3 -c "import json,glob;[print(json.load(open(f))['caption']) for f in glob.glob('entries/*.json') if '<ドメイン>' in (json.load(open(f)).get('url') or '')]"
    実例=社名は NOT A HOTEL に留め、説明の側で「ARCHITECTSの〜」と補う(既存3件がこの形で統一されている)。

27. pinしてクロスフェードするセクションは「単体だと開いた瞬間が真っ白」(2026-08-18)

  • 参考元(日鉄興和不動産 sumai.nskre.co.jp/brand/.brand-v2-values-blocks)は
    tl.to(items[0],{autoAlpha:1,duration:.4},0) で1枚目をフェードインさせる作り。
    実サイトは前セクションから入ってくるので成立するが、単体プレビューはセクション先頭=progress 0 なので
    1枚目が autoAlpha 0 のまま=全面真っ白で開く。大串から「おかしいよ」。
  • 初期化に gsap.set(items[0], { autoAlpha: 1 }) を足す(参考元のヒーロー側 IIFE が使っている同じ手当てなので
    "寄せる"ではなく実コード由来)。scrub の .to は開始値1として記録されるので以降の挙動は不変。
  • 抽出したら必ず「scrollY=0 のスクショ」を撮って確認する。opacity/visibility を機械で見るのが速い:
    js Array.from(document.querySelectorAll('<スタック> > article')).map(a => getComputedStyle(a).opacity)

28. 「同じstart/endのScrollTriggerを2本立てる」書き方は単体だとstartがズレる(2026-08-18)

  • 参考元は ①pin付きtimeline ②scale制御用の ScrollTrigger.create同じ trigger / start / end で2本立てていた。
    実サイトでは両方 start=5771 で一致するが、単体ページに移すと2本目が pin 後の座標で測られて start が 0 → 4500 にズレる
    (=背景のゆっくりズームが一生発火しない)。ScrollTrigger.refresh() では直らない。
  • pin している側の scrollTrigger.onUpdate に同じ処理を載せる(1本化)。実測 scale 1.0884 対 1.0892 で実サイトと一致。
  • 見つけ方=ScrollTrigger.getAll().map(s => [Math.round(s.start), Math.round(s.end), !!s.pin]) を実サイトと単体で見比べる。

29. モリサワWebフォントは持ち出せない/白一色のリード面はセクションに含めない(2026-08-18)

  • モリサワ(morisawafonts.netMFW-*)は配信ドメイン限定+EULAで他ドメインへの持ち出し不可
    CSSの font-family は実コードのまま残し、mf.css は取り込まない(游明朝/ヒラギノ明朝にフォールバック)。
    実測の残差は「VALUE」の字幅が 112px→108px の4pxだけ。ここは詰められないので報告に明記する
  • 参考元の当該ブロックには「白背景+緑の一行」だけのリード面が含まれていたが、
    Labのセクションとしては先頭がほぼ白紙になり、大串が送ってきた動画の範囲とも違った → 外してビジュアル本体だけにした
    「セクションの範囲」は参考元のDOM区切りではなく、大串が動画/写真で指した範囲に合わせる。

30. プレビューは open <URL> で既定ブラウザに出す(2026-08-18)

  • http://localhost:8080/<id>.htmlリンクとして送っても大串は開けない(「開けない」)。
    open "http://localhost:8080/<id>.html" を実行したら即「開けた」。リンクを貼るだけで終わらせない。

31. 並行セッションの git add -A に自分の分が巻き込まれる(2026-08-18)

  • 別セッションが同じ ~/dev/terasu-lab で作業しており、向こうの git add -A で今回の entry/HTML/サムネごとコミット+pushされていた
    Add inquiry-form-saki-0817 section に混入)。こちらが commit する前に既に本番反映済みだった。
  • 格納前に git log --oneline -3 -- entries/<id>.jsongit status --porcelain -- <自分のファイル> を見る。
    空=もうコミット済み。二重コミットしにいかない。中身は git diff HEAD -- <自分のファイル> で一致確認するだけでよい。
  • 自分が commit するときは git add -A を使わず自分のファイルだけ明示 add(向こうの作業中ファイル27件を巻き込まない)。

27. 「ホバーが無い」=グローバル a:hover を剥がしたせい(2026-08-18 大串FB)

  • リビオ(sumai.nskre.co.jp)の4カラム価値カードで 「カーソル合わせた時のホバーがない」 と指摘された。
  • 真因:カードのホバーはカード固有CSSではなく、サイト全体の素の a ルールだった。
    css @media screen and (min-width:769px){ a{transition:700ms ease-in-out} } @media screen and (min-width:769px){ a:hover{opacity:.5} } a,a img{...;transition-property:opacity,background,color}
    「貼っても崩れない形で抽出する」工程で素の要素セレクタとして丸ごと削除していた。
  • リンク・ボタンを含むセクションを抽出したら、素の a{} / a:hover{} を必ず確認してスコープ版で残す
    css sec-<id> a, sec-<id> a img{ ...元のtransition... } @media screen and (min-width:769px){ sec-<id> a:hover{opacity:.5} }
    「未使用CSSは入れない」と「ページ級リセットは剥がす」を機械的にやると、ホバーだけが静かに消える
  • 検証は目視でなくcomputed値の機械比較が速い。Playwrightで実サイトとローカルの両方を
    page.mouse.move() でホバーさせ、opacity / backgroundColor / borderColor / transitionDuration を突き合わせる。
    今回これで「カード=0.5/0.7s・View more=0.9/0.2s・CTAボタン=0.5+rgba(0,0,0,.4)/0.3s」の完全一致を確認した。

28. モリサワ等のライセンス明朝は Google Fonts で寄せる(2026-08-18)

  • 参考元が MFW-A1MinchoStd-Bold(モリサワ・ドメインロックのCDN)だと、フォールバックの游明朝で明らかに太く出る。
  • Shippori Mincho(Google Fonts)に差し替えるとA1明朝にかなり近い。オリジナル化の表でも
    「固有ブランドフォント → 代替フォント」は許可されている。参考元のフォントCDNのURL(ライセンスキー入り)は貼らない。

29. 並行セッションの git add -A に自分の作業が巻き込まれる(2026-08-18)

  • 3件を作り終えて commit しようとしたら、別セッションが git add -A で私の未コミットファイルごと commit & push 済みだった
    (commit message は相手のセクション名)。成果物は本番に出たが、履歴上は他人のコミットに混ざる
  • 同時に npm install / npx playwright install が走ると ERR_MODULE_NOT_FOUND: playwright
    browserType.launch: spawn Unknown system error -88(新しいバイナリの展開途中)で一時的に落ちる。数十秒待って再実行すれば通る
  • 同時作業日は、格納前に git log --oneline -3git status を見て自分のファイルが既に取り込まれていないか確認する。
    取り込まれていたら二重コミットせず、本番URLで反映だけ確認すればよい。

32. 動画のフレーム抽出は「Range対応サーバー+ブラウザ」で確実に刻める(2026-08-18・貴瞬案件)

  • 前提: このMacに ffmpeg なし / pyobjc なし / swift はSDK不一致でコンパイル不可(AVFoundationをJXA・Swiftで叩く方式は両方とも失敗)。
  • 効いた手順(動画9秒・22MBで実証):
    1. .mov をスクラッチパッドにコピーし、<video> だけの HTML を置く
    2. python3 -m http.server は Range 要求に未対応 → currentTime を代入してもシークできず、全部同じ画になる
    206 Partial Content を返す簡易サーバー(SimpleHTTPRequestHandler を継承して Range を処理)を自作して配信する
    3. ブラウザペインで開き、currentTime 代入 → onseeked を待ってから computer{action:"screenshot"}
  • ポートは 必ず空きを確認する。8901 が別プロジェクトのサーバーに使われていて、
    自分のサーバーは起動失敗しているのに「404が返る=ファイルが無い」と誤読して時間を溶かした(curl http://localhost:<port>/ で中身を見れば一発で分かる)。

33. ブラウザペインが hidden だと rAF が止まる=GSAP/ScrollTriggerの検証はヘッドレスでやる(2026-08-18)

  • 抽出したセクションをペインで開いてスクロールさせても 画像が切り替わらないScrollTrigger.update() を手で呼ぶと動く
    → 原因はペインが hidden で requestAnimationFrame が停止していた(コードの不具合ではない)。
    さらに computer{action:"scroll"}javascript_tool が 30秒タイムアウト・スクショが真っ黒になる症状も同時に出る。
  • スクロール連動の検証は Playwright(terasu-lab の node_modules)でやる。段階的にスクロールして
    各時点の .is-active / visibility を実測すると「item1〜5 で1〜5枚目が出る」と機械的に確認できる。
    node は PATH に無い → export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"

34. 作業ツリーが他セッションの未コミットで汚れている時は worktree から push する(2026-08-18)

  • 並行セッションが code-previews/*.html を編集中&未追跡ファイル多数で、git pull --rebase
    「unstaged changes」「untracked files would be overwritten」で止まった。他人の作業を stash/削除するのは厳禁
  • 一時 worktree を切って、そこへ自分のファイルだけコピーして push する(本体の作業ツリーに一切触らない):
    bash git worktree add /tmp/<scratch>/wt origin/main cp <自分のファイル> /tmp/<scratch>/wt/<同じ場所>/ cd /tmp/<scratch>/wt && git checkout -b tmp-push && git add <自分のファイルだけ> && git commit -m "…" && git push origin tmp-push:main git worktree remove --force /tmp/<scratch>/wt
  • 集約JSON(sections-data.json 等)はコミットしない。CI が core/build_core.py && build.py を回すので、
    entries と img だけ push すれば足りる。手元で集約すると他セッションの未承認 entries まで巻き込む危険がある。

35. 「プレビュー送って」と言われたら公開URLで出す=Labは未登録ファイルだけ先に上げる(2026-08-18 大串FB)

  • ローカル(localhost:8080)とスクショで確認①を出したら「プレビューで送って」と2回言われた。
    このPCには Cloudflare の認証も cloudflared 等のトンネルも無いため、公開URLの手段は Lab の CI デプロイのみ。
  • 折衷案=code-previews/<id>.html だけを main に pushentries/ は作らない)。
    https://lab.terasu.scale-group.co.jp/code-previews/<id> で開けるが /sections の一覧には出ない=未承認のものが並ばない。
    OKが出てから entries+サムネを push して一覧に載せる。取り消しはファイル削除だけで済む。

2026-08-18・桔梗が丘整骨院(kikyougaoka.com)3セクション(咲輝PC・saki)

Google Fonts の日本語で palt が効くのは Noto Sans JP だけ(明朝は全滅・再利用価値大)

  • 元サイトが Adobe Fonts(ryo-gothic-plusn / shippori-mincho / azo-sans-web / shabby-chic)=他ドメインでは読めないので代替が要る。
  • 候補を機械計測して選ぶのが正解。同一文字列を font-feature-settings:"palt" 1normal で計測し、両者の幅が同じなら palt 非対応
    実測(13.7px・letter-spacing 1.476px・句読点/括弧の多い和文):
    | フォント | palt有 | palt無 | 判定 |
    |---|---|---|---|
    | ryo-gothic-plusn(元・Adobe) | 592.8 | 657.2 | 基準 |
    | shippori-mincho(元・Adobe) | 537.0 | 660.4 | 基準 |
    | Noto Sans JP | 590.5 | 667 | ✅ paltが効く・元と +1.0% |
    | Noto Serif JP / Shippori Mincho / Zen Old Mincho / Shippori Mincho B1 / Hina Mincho | 367.4〜368.7 | 同値 | ⛔ 全部 palt 非対応 |
  • 本文ゴシックは Noto Sans JP 一択。明朝は palt が無い前提なので、どうせ幅が同じなら元と同一書体の Shippori Mincho を選ぶ(字形が完全一致するぶん得)。
  • 副作用: 明朝は全角括弧が詰まらず 句読点だらけの見出しが約19%広くなる。今回は「体験プラン(カウンセリング込み)」が2行に折れたので その見出しだけ white-space:nowrap で元どおり1行に(はみ出しはカードのpadding内に収まる)。text-spacing-trim は効果ゼロだった。
  • フォント計測は必ず実ページでpage.setContent()<link> だと読み込まれずシステムフォント(Hiragino=paltが効く)に落ちて全候補が同じ数字=誤った合格判定になる。document.fonts.load()document.fonts.check() で読めたか毎回確認する。

流体rem(html{62.5%} + vw)は --sec-rem 変換+帯の統一で完全一致した

  • 元: html{62.5%} / ≤767px:25.641vw / ≥768px:503.2942898975% / ≥1100px:7.3206442167vw(min-width:1366) は px 抜けの無効指定なので効いていない)。
  • Nremcalc(N * var(--sec-rem)) に機械変換(data:URI はプレースホルダで保護してから置換/base64中の "3rem" 等を壊さない)。
  • 768〜1099px の帯は body{min-width:1100px} で横スクロールさせる設計。セクション単体では出せないので、その帯も 7.3206442167vw に統一した。7.3206442167vw は 1100px 時に元の 80.53px と一致するので、見た目は「1100px設計の比例縮小」=崩さずに横スクロールだけ消える。
  • 結果、PC1440pxの実測高さが お知らせ 511.89px / 料金 834.27px(元と完全一致)・フッター 809.22px(元 808.97・差0.25px)

サムネ検品は「白基調でスカスカな設計」を誤検知する

  • shoot_section.mjs の採用ゲート flatRatio>=0.92 && edgeRatio<0.03(画面の9割超が一色)に、白地+細いグレー文字の上品な設計が引っかかる(今回3件とも「中身が写らない」で不採用)。
  • 撮れた画像自体は正常。file:// でも Google Fonts も参考元画像も普通に読めることを requestfaileddocument.fonts.status で確認したうえで --force を使うのが正解。原因を潰さず force するのは禁止だが、「本当に白い設計」は force が正しい

環境(このPC=咲輝PC・2026-08-18時点)

  • node は ~/.nvm/versions/node/v24.18.0/bin/node。2026-08-17に書いた ChatGPT.app/Contents/Resources/cua_node/bin/nodeもう存在しないCodex.app 側には残っている)。nvm を見るのが確実。
  • playwright は chromium はあるのに chrome-headless-shell が未DLshoot_section.mjs だけ落ちる → npx playwright install chromium(2〜3分かかるのでバックグラウンド実行)。
  • 動画のフレーム抽出は swift スクリプト実行(コンパイルではなく swift file.swift)で通った。ffmpeg/pyobjc は無いが /usr/bin/swift があり、AVFoundation の AVAssetImageGenerator を使う30行で任意の秒数のフレームが取れる。※同日の別記録(項32)では「swiftはSDK不一致で不可」とあるので、まず swift frames.swift を試し、落ちたら項32のvideo+Rangeサーバー方式に切り替えるのが早い。今回これで「動画の箇所=料金表の体験プランカードが6秒周期で±1度ゆれる animation:rotate」を特定できた。
  • ESM スクリプトは ~/dev/terasu-lab 直下に置いてから実行する(scratchpadに置くと playwright を解決できない=ESMは cwd ではなくファイル位置から探す)。

同リポの別セッションの git add -A自分のファイルが巻き込まれて先にpushされる

  • 作業中に別セッション(arflex/spiralai)が走っており、こちらが add_section.py を回した直後の 12ファイル(entries 3・code-previews 3・img 9)が向こうのコミットに丸ごと入って本番反映済みになっていた。
  • commit する前に必ず git ls-files <自分のファイル>git status --porcelain を見る。既にコミット済みなら二重コミットせず、git log origin/main..HEAD で push 済みかだけ確認すればよい(今回は push も済んでいて、本番の実体と手元のsha256が3件とも一致)。
  • 反映確認は /sections の HTML を取って id を grep(ルート sections-data.json はSPAフォールバックのHTMLが返るので JSON パースできない=ここでも「見るな」が正しい)。

そのほか(今回効いた小ネタ)

  • グラデ文字は background-image:url(mask.png)-webkit-background-clip:textcolor:transparent背景画像はクロスオリジンでも描画されるので参考元URLのままでよい(mask-image は不可なので arrow.svg だけ data URI 化=2026-08-17の教訓どおり)。
  • @keyframes の中に @media がネストされた無効CSSがあった(SP用 ±0.5deg)。ブラウザは無視して ±1deg で動くので、実挙動どおりに書き直すのが「視覚不変」。
  • caption の会社名は参考HP集 core/data/reference-sites/<slug>.jsonname をそのまま使う(今回 桔梗が丘整骨院 が既に登録済みだった)。社名・住所・TEL・SNS ID・コピーライトはダミー化し、ナビ文言や料金プランなど設計の実体である文言は原文のまま(2026-08-17 大串FBの運用を踏襲)。

2026-08-18 株式会社貴瞬 採用ページ(福利厚生カード12枚/選考フロー矢羽根)

clamp(...calc(N * var(--mw) / var(--bw)))JSで書き換わる変数を疑う(今回最大の落とし穴)

  • CSS の :root{--mw:1920px} をそのまま持ってくると、全部の文字が本家より大きくなる(title 24px vs 本家 20.57px)。
  • 実体は main.jsdocument.documentElement.style.setProperty("--mw", Math.floor(window.innerWidth)+"px") + resize 再設定。
  • 抽出時は「CSS変数がJSで上書きされていないか」を必ずJS側でgrepする。対処は --mw:100vw をCSSに置き、同じ setter を sec-<id> 要素に対して入れる(documentElementに入れると漏れる)。

ベース設定は html/body だけでなく :where(:root) も見る

  • 今回のreset は :where(:root){line-height:1.5; overflow-wrap:anywhere; line-break:strict; text-autospace:normal; text-spacing-trim:trim-start}
  • line-height:1.5 を落とすと見出しブロックが13pxずれる(en見出しの line-height が normal になるため)。日本語の折り返し系(overflow-wrap/line-break)も行分割に効くので拾う。
  • 拾い方は sec-<id>{ … } へ寄せる。* 側の min-inline-size:0; border-style:solid; border-width:0 も忘れずスコープ化。

「完全再現」の判定は目視でなく数値でやると速い(今回これで一発)

  • Playwright で 本家と自分の両方を開き、getBoundingClientRect()テキスト行数 Range.getClientRects().length を並べて出す。
  • 今回は カード12枚の w319×h232/265/282 と タイトル/本文の行数、フロー側の secH 642.83 / leadTop 173.81 / listTop 323.98全部同値になるまで詰めた。スクショ比較より圧倒的に速く、ダミー文の文字数調整(08を短く・12を長く)も機械的に決まる。

Claude Code のブラウザペインでは GSAP が動かない(環境要因・バグではない)

  • ペイン内は document.visibilityState==='hidden'requestAnimationFrame が1フレームも発火しないgsap.globalTimeline.time() が進まず「アニメが固まっている」ように見える。
  • 動きの検証はブラウザペインでやらない。playwright(chromium)でスクロール→スクショが正解。

別セッションの未push分を巻き込まずに push する(隔離worktree)

  • 着手時にローカルが ローカル17 / リモート21 で乖離(別セッションの未push 16件が混在)。この状態で git pull --rebase は他人のWIPを巻き込むうえ衝突しやすい。
  • git worktree add --detach <tmp> origin/maingit cherry-pick <自分のcommit>git push origin HEAD:main が安全。作業ツリーに一切触れない。/private/tmp/lab-push* に他セッションの同型worktreeが残っていたのでこの型は既に共通運用とみてよい。
  • 併せて git add -A は使わず、自分のファイルをフルパスで addsections-data.jsonコミットしない(CIが build.py で再生成するため。ローカルには他人の未コミットentriesが混ざる)。

環境メモ(2026-08-18・咲輝PC)

  • この日は node が /Applications/ChatGPT.app/Contents/Resources/cua_node/bin にあった(nvm には無し)。日によって置き場が変わるので command -v node → 無ければ ChatGPT.app / nvm の順で探す
  • 参考元がフォント自ホストのとき、General Sans は Fontshare CDNhttps://api.fontshare.com/v2/css?f[]=general-sans@400,500)で同じ字形が出る。Noto Sans JP は Google Fonts で可。
  • 本家の実装バグ(SPで4番目の矢羽根が flow03-mb.png を指す)も完全再現の原則どおりそのままにした。

2026-08-18(Jコミ新卒サイト・3カラム/斜めカット2件・咲輝PC)

確認①は「localhost」でなく Lab本番の code-previews を先にpush して公開URLで出す

  • ローカル python3 -m http.serverターン跨ぎで落ちるうえ、そもそも大串の環境からは開けない(「開けないからもう一回送って」)。
  • entries を登録する前に、code-previews/<id>.html だけを commit→push(CIで約1分)。https://lab.terasu.scale-group.co.jp/code-previews/<id> が200になったらそのURLで確認①。一覧(/sections)には出ないので登録前に見せるというフローは崩れない。
  • 併せて、確認①の前にオリジナル化(クラス名の汎用化)まで済ませてから公開した。見た目は変わらない工程なので確認の意味は落ちず、参考元クラス名を本番に一瞬でも出さずに済む。

macOSの画面収録(.mov/HEVC)は Chromium でシークできない

  • video.currentTime= を代入しても 0 のまま・seekable[0,0]play() も進まない(1フレーム目だけは描画される)。ffmpeg も pyobjc も無い環境だった。
  • qlmanage -t -s 1400 <mov> -o <dir> が中盤あたりのポスターフレームを1枚出すので、「1フレーム目(ブラウザ)」+「ポスター(qlmanage)」の2枚で当たりを付け、あとは実サイトのDOMで該当セクションを特定した方が速い。
  • 学び: 動画は「どのセクションか」の特定に使えれば十分。動きの検証は実サイトを開いて computed style を見る方が確実。

画像と動画で別々のセクションを指していることがある

  • 今回、添付画像=.sec-faces__list(01/02/03の3カラム)、動画=.sec-future(斜めカット2ブロック)と別物だった。
  • → Step0で両方を特定して「A: 両方 / B: ①だけ / C: ②だけ」の3択で1問だけ聞く。勝手に片方だけ作らない・勝手に両方作らない。

別セッションが同じリポで作業中のときの安全な push

  • 共有ツリーが他セッションの未コミット変更だらけだと git pull --rebase"cannot pull with rebase: You have unstaged changes" で必ず失敗する。--autostash は相手の編集中ファイルを触るので避ける。
  • 正解は一時worktree:
    bash git worktree add -q --detach /tmp/lab-push origin/main cd /tmp/lab-push && git cherry-pick <自分のsha> && git push origin HEAD:main cd - && git worktree remove /tmp/lab-push --force
    相手の作業ツリーに一切触らず、最新 origin/main の上に自分の1コミットだけ載る。
  • CI は core/build_core.pybuild.py自分で回すので、sections-data.json はコミットしなくてよい(他セッションの未コミットentryを巻き込まないためにも、コミット対象は entries/<id>.jsonimg/sections/<id>*.png だけに絞る)。

node_modules~/dev/mikke/node_modules へのシンボリックリンク(terasu-lab)

  • gitに mode 120000 で入っているリンク先はCodex.app のままgit checkout -- node_modules すると壊れたリンクに戻り、playwright が全滅する(ERR_MODULE_NOT_FOUND → 一見「別セッションがnpm操作中」に見える)。
  • 復旧は rm -f node_modules && ln -s /Users/oogushisaki/dev/mikke/node_modules node_modulesChatGPT.app 同梱の playwright(1.57) はブラウザ(1200)が未DLで使えないので、mikke側(1.62.1=chromium-1234あり)を指すのが正。この差分はコミットしない。

抽出まわりで効いた型

  • rem基準が無く px 主体のサイトでも、reset由来の *{box-sizing}font:inherit をスコープ版で再現しないと余白が崩れる。sec-<id> :where(div,span,h1..h6,p,ul,ol,li,img){margin:0;padding:0;border:0;font:inherit;vertical-align:baseline} が効いた。
  • 単独プレビューで上下が黒帯(=白背景が効かない)のは、中身の margin が root の外へ抜ける margin collapsing。root に display:flow-root を足すと、余白の値は元のまま白背景で覆える(overflow-x:clip ではBFCにならないので直らない)。
  • jQuery heightLine の高さ揃えは、jQueryを持ち込まず 15行の素のJSinnerWidth>768 のとき最大高さを揃える)で挙動まで同じにできる。
  • 参考元フォントが自サーバーの .ttf(CORS不可の恐れ)でも、Hanken Grotesk / Noto Sans JP は Google Fonts にあるので fonts.googleapis.com で代替すれば見た目は一致する。
  • ダミー文は原文の文字数を python で測って ±0 に合わせる(今回12箇所すべて一致)。文字数が同じでも改行位置は1行ずれることがあるが、これは preview の body 既定マージン16px分で、既存セクションも同条件。

2026-08-17 neie(お知らせ一覧+ページネーション)

同じidを別セッションが使うとプレビューが別サイトの中身にすり替わる(2026-08-17 実地・大串が気づいた)

  • neie のページネーションを pagination-arrow-saki-0817 で作って http://localhost:8080/... を渡したら、大串の画面には白地・丸ボタン・赤矢印の別サイトのページネーションが出た。「このサイトのページネーションはこれじゃないの?」と2往復した。
  • 原因は2つ重なっていた:
    1. 別セッションが同じid pagination-arrow-saki-0817code-previews/ を上書きしていた(-saki-0817 は今日70件超あり、<主題>-<種別>-<author>-<MMDD> だけでは衝突する)。
    2. ポート8080も別セッションが先に専有しており、こちらの python3 -m http.server 8080Address already in use起動失敗していた& でバックグラウンド起動+ログを捨てていたので気づかない。curl は相手のサーバーが 200 を返すので疎通確認も素通りする)。
  • 恒久対策(着手時に必ずやる)
  • id は着手前に ls code-previews/ | grep <主題> で空きを確認してから決める。衝突しそうなら主題を具体化する(pagination-arrowpagination-dotsarrow)。
  • ローカルサーバーは空きポートを確保してから起動する。lsof -nP -iTCP:<port> -sTCP:LISTEN で確認し、起動後にログを必ず読むcat /tmp/xxx.log)。8080 は共用前提で使わない。
  • --bind 127.0.0.1 にすると Browser pane から見えないことがある(localhost が ::1 に解決される)。--bind は付けない。新しいポートは preview_start {url} で開くと承認が通る(navigate だけだと denied になる)。
  • 渡したリンクが疑われたら、同じ画角・倍率で自分のプレビューを撮って画像で並べると一発で切り分けできる(--force-device-scale-factor=2sips -csips --resampleWidth)。

薄い線画セクション(ページネーション等)はサムネ検品に落ちる

  • shoot_section.mjs の採用ゲートは 色<=10種 && 輪郭<2% で落とす。白地に細いグレー線+小さな数字のページネーションは実際に 色2種/輪郭0.3% で不合格になる(描画は正しい)。
  • 既存の合格例 pagination-dots-saki-0817現在ページが黒ベタなのでゲートを通っていた=中身が薄いセクションは構造的に落ちる
  • → 描画が正しいことを実測(computed値・スクショ)で確認済みなら --force で保存してよい。保存後に必ず PNG を目視すること。

id が既存とかぶると既存セクションのプレビューHTMLを上書きして本番を壊す(2026-08-18・実際に起こした)

  • neie のフッターに footer-sitemap-saki-0817 を使ったが、同じ日に千代田工販で同じidが既に登録済みだった。code-previews/<id>.html を書いた時点で既存ファイルを上書きし、そこへ別セッションの git add -A が走って本番の千代田工販フッターが neie のフッターに差し替わった状態でpushされた。
  • add_section.pyentries/<id>.json の重複だけは止めてくれるが、その前に書く code-previews/<id>.html は素通りする。つまり「add_section.py で気づく」では遅い。
  • HTMLを書く前に必ず空きを確認する。id は <主題>-<種別>-<author>-<MMDD> だが、footer-sitemap works-grid のような汎用的な主題名は同じ日に衝突しやすいので、主題側を具体的にする(今回は footer-darksitemap に変更)。
    bash cd ~/dev/terasu-lab test -e code-previews/<id>.html -o -e entries/<id>.json && echo "⛔ 衝突" || echo "✅ 空き"
  • 壊してしまったら git log --oneline -- code-previews/<id>.html で元のコミットを特定 → git show <sha>:code-previews/<id>.html > code-previews/<id>.html で復元できる。復元後は grep -c <参考元ドメイン> で中身が戻ったことを機械確認する。

rem基準(:root{font-size:10px})は calc(N * var(--nr)) に変換する

  • neie は :root{font-size:10px} を前提に全寸法が rem。コマンドの手順どおり sec-<id>{font-size:10px} に寄せても、rem は常に root を見るので効かない(ホストHPの root=16px で 1.6倍に膨らむ)。
  • → セクションに --nr: 10px を定義し、抽出CSSの Nrem を機械置換で calc(N * var(--nr)) にする。clamp(1.6rem,1.25vw,2.4rem)clamp(calc(1.6 * var(--nr)),1.25vw,calc(2.4 * var(--nr))) で成立する。
  • あわせて sec-<id>{ font-size: calc(1 * var(--nr)) } を入れる(入れないと font-size を持たない要素の継承値が実サイト10px/抽出16pxでズレ、em 指定が狂う)。
  • 検証は実サイトと抽出側の computed 値を機械比較するのが速い(今回フッターは 1440/768/390 の3幅で471項目すべて一致まで詰められた)。

モリサワのWebフォント(MFW-* / morisawafonts.net)は持ってこない

  • mf.css の冒頭に複製・改変の禁止が明記されており、かつドメイン制限で他サイトからは読めない。Noto Sans JP / Noto Serif JP に置換する(neie 自身も併用しているので違和感が出にくい)。
  • 置換時は font-weight を元の値のまま残すfont-weight:300 の明朝を 400 で代用すると computed 比較で差が出る)。Google Fonts の URL 側で wght@300 を要求しておくこと。

サムネの「残像」は表示時の縮小によるもののことがある

  • 生成したサムネを見て別カラムの文字が薄く重なって見えたが、PNGを等倍で切り出すと何も無かった(ピクセル走査で背景より明るい画素ゼロ)。撮り直す前に PIL で該当座標を等倍クロップ+走査して確かめる。

2026-08-18 SpiralAI(go-spiral.ai)=STUDIO製サイトからの抽出

STUDIO製サイト(data-s-<uuid> 属性がCSSセレクタ)の抽出手順

  • STUDIO は class ではなく [data-s-<uuid>] 属性セレクタでスタイルを当てる。抽出後は uuid→意味のある汎用名に機械置換する(data-s-30b9ce21-…data-s-cards)。
  • 置換時の落とし穴が2つ。どちらもURLと変数をマスクしてから置換すれば防げる:
    1. 画像URL(storage.googleapis.com/.../s-480x480_<uuid>.webp)に uuid が入っており、素直に置換するとアセットが404になる。
    2. data-r-… パスや --gap-h-<uuid>親のuuidを含む。マップに無い=抽出漏れではないので、ctx1 等の汎用トークンへ自動割当てにする。
  • CSSは :where(.render-canvas) … で始まるものが素のリセット相当。.render-canvassec-<id> に置換すればそのままスコープ化できる(html/body/* を持ち込まずに済む)。

「スマホ用に別DOMを丸ごと用意している」サイトがある

  • SpiralAI は 540px以下でPC版DOMを display:none にし、別に用意したSP専用DOMを出す(CONTACT/RECRUITカード)。PC版だけ抜くとスマホで何も出ないセクションになる。
  • 見つけ方: 390px幅で対象テキスト(CONTACT 等)を全部拾い、offsetWidth||offsetHeight で可視な方を探す。可視ノードが別uuidならSP専用DOMがある。
  • 対処: 両方を同梱し、切替のブレークポイント(元サイトの @media を読む=今回は540px)ごと再現する。

器(親の余白・中央揃え)は"セクション本体の外"にあるので抜けやすい

  • 抽出対象を sec-<id> で包むと、元サイトで親が持っていた「中央揃え」「左右余白」「上下余白」が消える(今回①が左寄り・②が上に詰まる、と大串FB)。
  • → 親チェーンの computed(margin / max-width / align-items)を確認し、sec-<id> 側に再現する。今回は①に display:flex;align-items:center、②に元footerの margin:76px 48px(990px以下57px/24px、540px以下16px)を移した。

全画面の"極薄テクスチャ背景"はサムネ検品に落ちる(--forceが妥当なケース)

  • 紙テクスチャ(ほぼ白+微細な粒のGIF)は shoot_section.mjs の「中身が写らない(色2種/輪郭0.0%)」で弾かれる。描画は正常で、被写体が本当に薄いだけ。
  • 撮影幅を狭めても粒は大きくならないbackground-size:cover なので常に画面いっぱいに伸縮する=粒の相対サイズは不変)。ズームでの回避は不可能。
  • → 実物PNGを目視確認したうえで --force で保存する。ただし一覧カードではほぼ白に見えることは、登録時に大串へ明示する。

同時セッション中の rm _tmp_*git add -A は事故る

  • _tmp_*.mjs別セッションの作業中スクリプトで、しかもリポにコミット済みのことがある。まとめて rm すると他人の作業を消す(今回発生・git checkout -- <files> で全復元できた)。
  • 逆に他セッションの git add -A がこちらのWIPを巻き込んでコミットすることも起きる(今回、私の3セクションは相手のコミットに同梱されて push 済みだった)。
  • → 片付けは自分が作ったファイル名を明示して消す。コミットも git add -A ではなく自分の対象ファイルだけ明示ステージする。sections-data.jsonCIが再生成するので、他セッションのWIPが混ざる状況ではコミットしない。
  • CIは連続pushで前のrunがcancelledになるX表示=失敗ではない)。success が出た run の sha に自分のコミットが含まれるかは git merge-base --is-ancestor <自分のsha> <runのsha> で機械確認する。

node_modules が壊れて Playwright 系ツールが全部落ちることがある

  • 症状: Cannot find package 'playwright'。原因は node_modules が別Mac用の絶対パス(/Users/<別ユーザー>/…)へのシンボリックリンクに差し替わっていたこと。
  • 復旧: rm node_modules && npm install → 続けて npx playwright install chromium(パッケージが上がるとブラウザのビルド番号も変わるため。1200→1228 で必要になった)。

2026-08-18 StockSun(stock-sun.com/about/)3セクション(saki)

元サイトがwebfontを読み込んでいないなら、こちらも足さない(今回の最重要)

  • StockSunは font-family:"Noto Sans JP", sans-serif指定するだけで @font-face もGoogle Fontsリンクも無い(=閲覧環境のOSフォントで出るのが"元サイトの見え方")。
  • 良かれと思って Google Fonts の Noto Sans JP を足したら、70名 の「名」が h30→44px・x-13.8px ズレて不一致になった。webfontとOSフォントで ascent/descent が違うため。
  • 元のfont-family指定をそのまま写すのが最も忠実。webfontを足すのは「元サイトが実際に読み込んでいる時」だけ(2026-08-16の@import併記ルールはその場合の話)。

ホスト埋め込み対策リセットは :where() を付ける(2026-08-16の型の修正)

  • .sec-X.sec-X p{margin:0} は特異性(0,2,1)で、セクション本体の .sec-X .c-client__ttl{margin-bottom:2rem}(0,2,0) を潰す(見出し下の余白32px→0で詰まった)。
  • .sec-X.sec-X :where(p, img){margin:0;padding:0}(0,2,0)にすると、ホストの p{margin-bottom:1.6em}(0,0,1)には勝ちつつ、本体CSSには先に書いてある分だけ負ける=狙いどおり。

「マッチするCSSルールだけ抜く」抽出スクリプトが速くて確実

  • playwrightで実ページを開き、document.styleSheets を順番どおりに列挙 → 各ルールのセレクタを対象サブツリーの全要素に matches() で当てて、ヒットしたものだけ @media 付きで回収する方式。minifyされた448KBのCSSでも 36〜50ルールまで絞れた(=121〜268行の自己完結HTML)。
  • CORSで cssRules が読めないシートは、Node側で fetch → ページ内で new CSSStyleSheet().replaceSync(text) して再パースする(ページ内fetchはCORSで落ちる)。
  • 素の要素セレクタ(div,img,li,p,span,ul)・全称・::-webkit-scrollbar のルールは捨てて、スコープ版リセットに手で書き直すとそのまま「貼っても崩れない形」になる。

検証は「元サイトと抽出版のcomputedを全要素diff」(今回も有効)

  • 結果: ロゴ一覧=差分ゼロ、数字カード=サブピクセル(278→277.984px)のみ、理由リスト=アイコンだけ差分
  • 遅延ロード画像は元サイト側が未ロード(h=24等)のまま計測されるので、data-srcsrc を入れたこちらの方が正しい。この差分は「不一致」ではないと判断してよい。

同じMacで複数セッションが同時に走っている時の事故と回避

  • 作った3ファイルが、別セッションの git add -A に巻き込まれて先にコミットされていた(中身は同一だったので実害なし)。着手前に git ls-files | grep <id>同名idの既存を確認すると早い。
  • 自分の分だけ確実に入れるには git commit -m "..." -- <明示パス...>(indexに他人のstagedがあっても巻き込まない)。git add -A は厳禁のまま。
  • CIは後続pushで次々 cancelled になる(concurrency group)。runの成否を追うより、本番URLの200と /sections 一覧で確認する方が速い
  • build.py 前に他人の未コミット entries/*.json/tmp へ退避 → build → 自分の分だけ add → commit → 戻す、は今回も有効。

playwrightが「Executable doesn't exist … chromium_headless_shell-1228」で落ちたら

  • 誰かが node_modules を更新してplaywright本体のバージョンが上がると、旧ブラウザ(1200)しか無くて全ツールが落ちる。→ cd ~/dev/terasu-lab && npx playwright install chromium で復旧(既存1200は残るので他は壊れない)。

2026-08-17 エイトデザイン /about(Next.js+CSS Modules+Lenis)から2件

Next.js(CSS Modules)サイトの抽出の型

  • マークアップは SSR HTML(curl)から取る。レンダリング後DOM(headless --dump-dom)は文字分割スパンが1文字=4要素に膨らんでいて読めない。SSRなら分割前の素の構造が出る。
  • CSSはチャンクを総当たりで落としてハッシュで特定する: <link ... /_next/static/chunks/*.css> を全部 curlgrep -l "<モジュールハッシュ>__" *.css。1モジュール=1ファイルとは限らないので、コンポーネント(ボタン/見出し切替/目玉)ごとに探す。
  • CSSの切り出しはブレース走査のパーサで。ざっくり正規表現だと @media (hover:hover) 内のホバー規則を丸ごと取りこぼす(実際に落として、ボタンのホバー反転が消えた)。@media 文脈ごと拾う。

液体レイアウト(html{font-size:calc(100vw/1440)})は rem→calc(var(--u)*N) に機械変換する

  • この手のサイトは 96rem のような値が全編に出る。sec-<id>{font-size:...} に寄せても rem は root 基準のままなので効かない(スコープ化の定石が唯一通用しないケース)。
  • 正解は sec-<id>{--u:calc(100vw/1440)} を置いて Nremcalc(var(--u)*N) に一括変換。SP側は @media(max-width:960px)--u:calc(100vw/375) に差し替え(参考元の --liquid-htmlroot と同じ設計)。
  • 落とし穴=負のrem(?<![\w.#-])(\d...)rem のような否定先読みだと right:-90rem が変換されず、root既定16px換算で 要素が1440px右へ吹き飛ぶ(目玉が画面外に消えた)。(?<![\w.#])(-?\d*\.?\d+)rem\b で符号ごと拾う。calc(100% - 128rem) は空白があるので誤爆しない。

動きは「推測せず Playwright で数値を実測」してから実装する

  • Lenis を使うサイトは window.scrollTo が効かないbody.scrollHeight が画面高と同じ・実体は wrapper の transform)。page.mouse.wheel() で動かし、2秒以上待って落ち着かせてから読む。0.6秒待ちだと慣性の途中を読んでしまい、進捗が非線形に見えて解析を誤る。
  • 今回取れた値をそのまま実装に使った: インデックス切替=round(-main.top / vh)/見出しドラムの回転=90deg + 270deg × index(-90ではなく+270。前回りに回る)/写真パララックス=リード上端が画面上端+170pxから約1050pxの間を power2.out、y −130/−100/−60vh → 100/200/300vh・scale .2 → .8/.7/.6・x 15%→0。
  • 登場アニメも同じ手で計測(100msごとにinline styleを読む)。文字ロールは stagger 0.022s / duration 1.1s / power3.out、行は y:1em→0+opacity、和文は0.7s、目玉は0.95s遅れて back.out(1.7)
  • 合否は目視でなく座標で判定する。参考元と自作を同じviewportで開いて getBoundingClientRect を突き合わせ、画像枠 [270,200,900,500]/見出し [64,170,1312,90]/ボタン [1088,578,288,72] まで一致させた。ズレたら原因が必ずある(今回は分割spanに入れた line-height:1 が行高91.2→96pxにしていた。参考元の line-height:1.不正値で無効=入れないのが正解)。

文字分割は「単語単位でくくる」

  • 1文字ずつ inline-flex に入れると、SPで単語の途中で改行する(SERVIC / ES)。単語ごとに display:inline-block;white-space:nowrap の箱でくくると語中改行が消え、参考元のSP(3行)と同じ見え方になる。
  • 参考元がSPでDOMごと行を出し分けている場合、単一DOMでは行幅が画面幅いっぱいになり装飾(目玉)の位置がズレる。@media(max-width:960px){ .row{width:min-content} } で「一番長い単語の幅」に縮めると同じ配置になる(fit-content利用可能幅を返すので効かない)。

スクロール連動セクションのサムネは自前で撮る

  • tools/shoot_section.mjsセクション先頭から4:3で切り出すので、100vh×5ブロックのスクロール連動ものは導入部だけが写る(真っ暗+小さい写真=何のセクションか分からない)。
  • viewport 1440×1080・deviceScaleFactor:2見せ場までスクロールしてから撮り、img/sections/<id>.png へ直接保存すれば規格(2880×2160)は同じ。normalize_thumbs.py も不要。

terasu-lab が他セッションで汚れている時は 一時worktreeでpushする

  • 実際に遭遇した状態: 未pushのローカルcommitが10件以上+作業ツリーに36ファイルの未コミット変更(全部他セッションの進行中作業)。この状態で git pull --rebase すると autostash が衝突し、git add したファイルが他セッションのcommitに吸われる
  • 安全な型(他セッションも同じ方式で運用していた):
    git worktree add --detach /private/tmp/lab-push-<自分の名前> origin/main → 自分のファイルだけ cp → git add <明示パス> → commit → git rebase origin/maingit push origin HEAD:maingit worktree remove共有の作業ツリーに一切触らない
  • 未追跡ファイルがリモートと衝突して pull が止まる時は、消さずに _local_backup_<日付>/ へ退避してから pull(他人の未コミット作業を消さない)。
  • CIはpushのたびに走り、後続pushで前のrunがcancelされる。自分のrunがcancelでも中身は後続runに含まれるので、runの成否ではなく本番URLで反映を確認する(/section/<id>/ が200になるまでポーリング)。

2026-08-18 株式会社プレックス(plex.co.jp)から2件(ニュース/記事カルーセル)

巨大CSS(467KB)からの抽出は「ブラウザで拾う」より「ローカルで機械抽出」が速くて確実

  • bs4 + soupsieve + tinycss2 はこのMacに入っている。DOMダンプ(Chrome headless --dump-dom)とCSSをローカルに落として、セレクタが実際にセクション内の要素にマッチするルールだけ抜くスクリプトを書くのが最短(news 136ルール / articles 221ルールまで自動で絞れた・全部で数秒)。
  • マッチ判定は2段構え。① soupsieve で実DOMにマッチするか、② セレクタ中のクラスが全部そのセクションで使われているか(is-active など実行時に付くクラスを拾うために必須。これが無いと出現アニメのCSSが丸ごと落ちて「アニメだけ消えた」状態になる)。
  • html{}/body{}/:root{} は自動でスキップし、body由来の font-family / color / font-feature-settings は sec-<id>{} に手で寄せる。plexは html{font-size:1rem}(=16px・ブラウザ既定と同じ)なので rem はそのままで一致した。

Splide の splide.length は 0 を返す(進捗バーが壊れる真犯人)

  • 進捗ドットは left: calc(var(--current) / var(--total) * 100%)。元サイトは this.total = ee.length で入れているが、実サイトでも --total: -1 のまま=本番の進捗ドットが動いていない(元コード側のバグ)。
  • 正しくは splide.Components.Controller.getEnd()(今回11)。Lab版はこちらで動くようにした(大串OK済み)。
  • 併せて:Splideは深くネストした .splide__arrows もちゃんと拾う(自前で click→go() を張ると二重送りになるので張らない)。

ブラウザペインが非表示だと innerWidth = 0max-width ブレークポイントが誤発火する

  • breakpoints:{767:{destroy:true}} のカルーセルが「mountされない・矢印が効かない」ように見えるが、コードは正しい。ペインが隠れた瞬間に幅0=SP判定でdestroyされていただけ。
  • 動作検証はブラウザペインでやらない。リポの playwright(node_modules は mikke 側を指すリンク)で実ビューポート1440で開いてクリックまで機械検証するのが正解。
    bash /Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node <リポ直下に置いた検証.mjs> # /tmp に置くと playwright を解決できない
  • ペインは「見た目の最終確認」だけに使う。数値・動作は playwright の evaluate で取る。

今回の実測一致(完全再現の確認に使える型)

  • 元サイトと抽出版を同じ幅で開いて getBoundingClientRect().height / padding / font-size を突き合わせると一発で判定できる(news: 高さ894px・上下160px・タイトル16px・日付12pxが完全一致)。
  • 単独プレビューはスクロールバー16px分だけ container が細くなるが、これは実HPに貼れば消える差なので不一致扱いしない。

Alpine の小さな挙動は素のJSで再実装できる

  • x-intersect.full.onceIntersectionObserver({threshold:1})is-active を付与。
  • x-data="hover" のロール(rollEnter/rollLeave)は、is-hover を enterTime だけ付け、離れたら no-hover を leaveTime 付けて外すという10行程度の再実装で同じ動きになる(元実装を main.js から読んで写すのが速い)。

15. cssRules=1 のもう一つの原因=@font-face の base64 が多重化して url("data:…; が欠落(2026-08-18 実地)

  • 既知の 11「cssRules 数で機械判定」と同じ症状(cssRules.length === 1・CSS全滅)だが、原因が id衝突ではないケース。フォントをbase64インライン化する処理が重複適用され、1つの src がこうなっていた:
    css src:url("data:font/woff2;base64,XXX") format("woff2");base64,XXX") format("woff2");base64,XXX") format("woff2");… /* ^^^^^^^ url("data:font/woff2; が欠けた不正トークン */
  • CSSトークナイザはここで復帰できず、1つ目の @font-face だけ読んで残り全部(50〜180ルール)を捨てる。目視では「フォントが違うだけ」に見えるので気づけない。
  • 検出(base64の妥当性まで見る):
    bash # 1行の @font-face に base64 が2個以上入っていたらアウト grep -o 'base64,' code-previews/<id>.html | wc -l # @font-face の数と一致するのが正
  • 修復(重複を落として最初の1つだけ残す):
    python pat = re.compile(r'(src:url\("data:font/woff2;base64,[A-Za-z0-9+/=]+"\) format\("woff2"\);)' r'(?:base64,[A-Za-z0-9+/=]+"\) format\("woff2"\);)+') s2, n = pat.subn(r'\1', open(f).read())
  • 切り分けの型: sed '<@font-face行>d' した版で cssRules を数え、急に 1→54 に戻れば犯人は @font-faceと確定できる。

16. rem基準が vw連動html{font-size:1.14vw})のサイトは sec-<id>{font-size:…} では移植できない

  • j-security.co.jp は @media(min-width:768px){html{font-size:max(9px,1.1428571429vw)}} / SP は 4.2666666667vwrem は常にルート基準なので、sec-<id> に font-size を書いても中の rem は 16px のまま=コマンド定義の「sec-<id>{font-size:10px} へ寄せる」が効かない唯一のパターン。
  • --rt を置いて Nrem → calc(N * var(--rt)) に機械変換する(完全等価・レスポンシブも元のまま):
    css .sec-<id>{ --rt: max(9px,1.1428571429vw); font-size: var(--rt); } @media (max-width:767px){ .sec-<id>{ --rt: max(9px,4.2666666667vw) } } /* .title{font-size:clamp(12px,1rem,1.5rem)} → clamp(12px,calc(1 * var(--rt)),calc(1.5 * var(--rt))) */
    変換は re.sub(r'(-?\d*\.?\d+)rem', r'calc(\1 * var(--rt))', css) の1行で足りる。
  • sec-<id> 自身にも font-size:var(--rt) を付けるのを忘れない。付け忘れると font-size 未指定の要素だけ 16px を継承して、実サイトと 0.46px ずれる(行送りが変わり、テキストの折り返し行数が変わって高さが29px単位でずれる)。

17. 「完全再現できたか」は実サイトとの computed値の機械比較で確定させる(目視では詰め切れない)

  • 実サイトと自作プレビューを Playwright で同時に開き、対応セレクタの computed を突き合わせる。今回これで3つの取りこぼしを検出した:
    | 検出された差 | 真因 |
    |---|---|
    | fontSize: 16.4571 / 16 | sec-<id> に html 相当の font-size 未設定(上記16) |
    | lineHeight: 24.68 / 28 | body{line-height:1.75}後に reset の body{line-height:1.5} があり後勝ちで1.5。1行に畳むとき先に書いてある方を採ると間違える |
    | width: 1440 / 1424 | プレビュー単体では body の既定 margin 8px が残る → <body style="margin:0">(既存作法・<style>内ではないのでスコープ検証に引っかからない) |
  • 比較する props: fontSize/fontWeight/lineHeight/letterSpacing/color/margin*/padding*/width/gap/borderTop*/backgroundColor/display/position/topgetBoundingClientRect() の w/h。
  • 残ってよい差: ダミー化した文字列・数値の字幅差だけ(例 70,00060,000 で 5px)。それ以外が残っているうちは Step3 に進まない。
  • 実サイト側は body:not(:has(.loader)) 等でローディング中だけ overflow:hidden になっていることがある。スクロールバーの有無で 16px ずれるので、差がその値のときは document.documentElement.clientWidth を両方で測って確認する。

2026-08-18 お問い合わせフォーム(j-security.co.jp)で得た知見

成果: contactform-split-saki-0818。PC1440でセクション高さ 2059.046875px が実サイトとピクセル一致
リネーム前後は 146要素×6287項目が3幅すべて完全一致。フォーム系は動きの検証項目が多いので、下記の型が効いた。

:LiFlaskConical Typekit配信フォントは他ドメインでは使えない(412)→ 実フォントは推測せずCDPで確定する

  • use.typekit.net/<kitId>.css412 precondition failed(ドメイン制限)。参考元がAdobe Fontsを使っていたら
    その欧文は再現できないと最初に確定させ、確認①で正直に伝える。
  • ただし諦める前に 「実際に何が描画に使われたか」をCDPで測る。今回それで
    日本語はTypekitではなく元サイト自前の woff2(Kinto Sans)が当たっている」と分かり、
    そのwoff2を絶対URLで読むだけで日本語は完全一致した(=差は欧文だけに限定できた)。
    js const client = await page.context().newCDPSession(page); await client.send('DOM.enable'); await client.send('CSS.enable'); const {root} = await client.send('DOM.getDocument', {depth:-1}); const {nodeId} = await client.send('DOM.querySelector', {nodeId: root.nodeId, selector: '<sel>'}); const {fonts} = await client.send('CSS.getPlatformFontsForNode', {nodeId}); // familyName + glyphCount
  • 参考元の woff2 / mask用svg は CORSが access-control-allow-origin: * なら絶対URLでそのまま使える(先に curl -I で確認)。
  • 残差は「欧文グリフ幅0.3px」「SPでラベル行が1pxずつ低い」程度に収まる。これは避けようがないので確認①で明示する

:LiRuler rem基準が vw のサイト(html{font-size:max(9px,1.14286vw)})のスコープ化

rem は常に html を見るので sec-<id>{font-size:…} では効かない。--rem 変数+機械変換が正解(既存の
accesscontact-info-saki-0817 と同型)。Nremcalc(var(--rem)*N) を正規表現で一括変換し、clamp() の中も同時に置換する。

re.sub(r"(-?\d*\.?\d+)rem\b", lambda m: "calc(var(--rem)*%s)" % m.group(1), body)

ブレークポイントごとに --rem を差し替える(SP max(9px,4.26667vw) / PC max(9px,1.14286vw))。変換後に生remが0件であることを機械検証する。

:LiTriangleAlert 「中身が空のdiv」を消すと margin collapse で高さがズレる

元HTMLにあった Turnstile用の空 .contact__row(コメントだけ)を「意味がないから」と削ったら、
margin-bottom:43.2px と直前の 37.03px の相殺が変わり ちょうど6.17px低くなった
DOM上で空に見える要素も「余白の一部」。抽出時に消さない。高さが数px合わない時は真っ先にここを疑う。

:LiShieldCheck オリジナル化(リネーム)の崩れゼロを機械証明する型

class名を一括置換した後、git HEAD版を code-previews/_tmp_before.html として横に置き、
DOM順のindex対応で全要素×全computed値を突き合わせる
。クラス名が変わっても構造は同じなので完全比較できる。

git show HEAD:code-previews/<id>.html > code-previews/_tmp_before.html

今回 1440/768/390 の3幅すべてで 6287/6287 一致(差分0)。目視より速くて確実。確認②はこれで代替できる
リネームは page-contact__maincontact__ を含むように部分一致で壊れる順序があるので、長い名前から先に適用する。

:LiUsers 複数セッション同時作業の地雷(今回3つ踏んだ)

  1. 他セッションの git add -A に作業中ファイルを巻き込まれてコミットされた
    → 自分は git add <自分のファイルだけ> にする。Labは1件1ファイルなので全く問題ない。
  2. node_modules を作り直され、pixelmatch/pngjs が消えた(package.jsonにはplaywrightしかない)。
    → ピクセル比較に依存せず computed値比較を主にする(そもそもこちらの方が原因が特定できる)。
  3. リポ内に置いた _tmp_*.mjs が消される
    → 一時スクリプトはスクラッチパッドに置き、絶対パスでplaywrightを読む
    import { chromium } from '/Users/MUGI/dev/terasu-lab/node_modules/playwright/index.mjs'

:LiGitBranch CIの cancelled は失敗ではない

並行pushが続くと concurrency で前のrunが次々キャンセルされる(今回5連続)。
git merge-base --is-ancestor <自分のsha> origin/main自分のコミットがmainに載っていることを確認し、
その後の成功したrunでデプロイされたかを見る。反映確認は /sectionsPlaywrightでレンダリングしてカードの有無で判定する
(curl+grepでは出ない=一覧はJS描画)。

:LiUserCheck 追加者の恒久ルールを id に反映し忘れない

このPCからの追加は --author saki --contributor saki/id も -saki-(2026-08-13 大串指示)。
今回いったん -ogushi-0817 で作ってしまい、id・ファイル名・スコープクラス sec-<id>・JS内のセレクタを一括で揃え直した
先に id を確定させてから Step1 に入ると手戻りがない。

フォーム系セクションで再現すべき「動き」チェックリスト

実サイトのJSから参考元CMSへの送信だけ除去し、以下は残すと実例として価値が高い(今回すべて動作確認済み)。
- 同意チェック → 送信ボタン解禁(disabled/opacity/cursor
- 空送信 → 各項目に .…__error 生成+フォーム全体メッセージ(success/error
- 入力/変更でエラーとメッセージをクリア
- チェックボックスの :has(input:checked) による塗り+mask SVGのチェックマーク
- 郵便番号7桁 → zipcloud(公開API) で都道府県・住所を自動補完(外部APIなのでそのまま動く)
- 送信ボタンhover → 背景色+矢印とラベルの入れ替えスライド

2026-08-18 ポジティブドリームパーソンズ(3件同時)で得た知見

  • CSSの mask-image はクロスオリジンだと問答無用でブロックされる(=マスク全面が黒=中身が全部消える)。
    元サイトでは同一オリジンなので動いていたものが、Labに持ってくると「斜線が出ない」形で壊れる。
    マスク用SVGだけ data URI に埋め込む(数百バイトなので軽い・形状は元データと1バイト同じ)。
    background-image は CORS 不要なのでそのまま参考元URLでよい=画像は元URL維持のルールと両立できる
    切り分け方: 「背景だけの箱/単一マスクの箱/2枚マスクの箱」を並べたテストHTMLを1枚作って撮ると一発で分かる。
  • rem基準(html{font-size:1.1111111111vw})は sec-<id>{font-size:...} に移しても効かない(remは常にhtml基準)。
    コマンド本文の「rem基準はスコープ内に寄せる」を素直にやると崩れる。
    --r 変数を sec-<id> に持たせ、1.25remcalc(1.25 * var(--r)) に機械置換する。
    ブレークポイントごとに --r:16px / --r:1.1111111111vw を切り替えれば元サイトと完全一致。
  • ローカルmainが他セッションの未pushコミットを抱えている時ahead 21 / behind 31 等)は、
    git pull --rebase が他人の未コミット変更で失敗し、素直にpushすると他人の未完了分まで公開してしまう。
    git worktree add --detach <tmp> origin/main → 自分のコミットだけ cherry-pickpush origin HEAD:main
    他セッションのツリー・コミットに一切触らずに自分の分だけ本番へ出せる。終わったら worktree remove
  • build.py が作る sections-data.json は他セッションの未コミットentriesまで巻き込む
    CIが本番ビルド時に再生成するので、集約JSONはコミットしないgit checkout -- sections-data.json)のが安全。
  • 動画から箇所を特定するとき、このPCには ffmpeg が無い
    ~/Library/Python/3.9/lib/python/site-packages/imageio_ffmpeg/binaries/ffmpeg-macos-aarch64-v7.1 が使える。
    swift はCLTのモジュール重複エラーで動かない=AVFoundationスクリプトは諦めてよい)
  • 同名idのcode-previewsが既にあることがある(前日の未登録プレビューが git log に居る)。
    add_section.py は entries/ しか見ないのでHTMLの上書きは検知されない。着手前に
    ls code-previews/<id>.htmlgit log -1 -- code-previews/<id>.html を確認する。

2026-08-18 野村不動産PMO(setupoffice)FV / FAQ 追加で得た学び

素材が複数あるとき「全部が同じセクションの説明」と決めつけない(最重要)

静止画1枚+動画2本+URLをもらい、動画2本も静止画と同じセクション(Three Answers)の動きを写したものと決めつけて着手した。実際は動画①=FV、動画②=FAQ の別セクションで、素材は3箇所を指していた。
着手前に、素材1つずつが「どのセクションか」を先に特定して一覧提示する(Step0の一覧提示を素材単位でやる)。

ffmpeg が無い環境での動画フレーム抽出(実地で確立)

ffmpeg も pyobjc(AVFoundation) も無い。Chrome に <video> で読ませて canvas に描くのが唯一動いた方法。
- ローカルHTTPサーバに .mov を置き、<video>canvas.drawImage でフレーム化
- seeked 発火直後に描くと前のフレームが描かれ、全フレーム差分0=「動きなし」と誤判定するsetTimeout(120ms) を挟むこと。これで最初「動画に動きなし」と誤報告した
- --headless + --virtual-time-budget では動画デコードが進まない(フィルムストリップが全部同じ絵になる)。動画がらみはブラウザペインで実行する

ブラウザペインのバックグラウンドタブは「見えない」

  • computer screenshot が真っ白/IntersectionObserver のコールバックが配信されない、という現象が出る
  • DOM計測(getBoundingClientRect / getComputedStyle)は正常に効く
    数値検証はブラウザペイン、見た目とIO/アニメの発火確認は実Chrome(Google Chrome.app --headless=new --screenshotと使い分ける

CSS抽出は「クラス名 grep」だけでは必ず取りこぼす

.faq__* を拾っただけでは、親側の汎用ルール .setupoffice-caption ul{margin-top;display:flex;gap:4px}... ul li{font-size:12px;color:#00031a99} を取りこぼしていた(設問の間隔44pxと文字の不透明度60%が変わる)。
抽出後に必ず「実サイト vs 自分の再現」で算出スタイル+幾何を機械比較する。要素×プロパティのJSONを両方で吐いて突き合わせると、目視では絶対に気づけない差が出る。今回はこれで2件検出。
(一致確認の例:総高さ1322.82px・設問高さ77px・回答パネル164.8px までピタリ一致すれば完全再現と言い切れる)

terasu-lab は他セッションと並行作業になる前提で push する

着手時 git pull 済みでも、作業中に ahead 21 / behind 31 まで乖離し、git pull --rebase は「他セッションの未コミット200ファイル」で失敗、push も連続で non-fast-forward / cannot lock ref。
隔離ワークツリーで自分の分だけ積むのが安全(ローカルの他人作業に一切触れない):

git worktree add --detach /tmp/lab-push origin/main
# 自分のファイルだけ cp → git add <明示パス> → commit
cd /tmp/lab-push && for i in 1 2 3 4 5; do
  git fetch -q origin && git rebase -q origin/main >/dev/null 2>&1
  git push -q origin HEAD:main 2>/dev/null && break
done
git worktree remove --force /tmp/lab-push
  • git add -A は厳禁(既知ルール)。CI が build_core.pybuild.pybuild_lab.py を回すので、生成物(sections-data.json)はコミット不要=他セッションの未コミットentryを巻き込まずに済む
  • CI run が cancelled になっても失敗ではない(後続pushに追い越されただけ)。run を追うより本番URLをポーリングして反映確認する方が速く確実

id 衝突は起こる(同じサイトを別セッションが同時にやる)

faq-accordion-saki-0817 が既に別セッション登録済みだった(同じPMOだが /faq/ ページ・回答内にSTEP縦線フロー図の別物)。
add_section.py が「既に存在します」で止めてくれる設計に助けられた。--force で潰さず、id を変えて共存させる(今回 faq-accordion-setup-saki-0817)。

淡色ミニマルなセクションはサムネの採用ゲートに誤って落ちる

FAQ(地色#ddddd9+細い罫線と文字だけ)が「中身が写らない(色24種/輪郭1.8%)」で不合格。実物は正しく描画されていた。
--force で保存し、必ず画像を目視してから採用する。リポの過去ログにも「白基調ミニマルは機械だけでは正常/異常を区別できない」と同じ記録あり。


2026-08-17 エイトデザイン(eightdesign.co.jp・Next.js + three.js + GSAP + Lenis)

WebGL描画かどうかは「文字がDOMにあるか」で機械判定できる

FVスライダー(湾曲画像・番号・カテゴリのピル)は一見HTMLだが document.body.innerText に「BINO」「エイトトレーラー」が1つも出てこない=WebGL描画。逆に巨大見出し「EIGHT JOURNAL」やフッターの「KEEP IN TOUCH ON...」は1文字ずつspanに割られているため生HTMLのgrepでは0件でも innerText には出る。
--dump-dom の生HTMLをgrepして「無い」と判断しない。ブラウザで innerText を見る。加えて document.querySelectorAll('canvas') の実寸を見れば全面canvas(1440x900)が一発で分かる。
→ 画像が湾曲している=シェーダー。CSSでは作れないので抽出不可の合図。

rem基準が html{font-size:calc(100vw / 1440)} の"リキッド設計"はスコープ化で必ず壊れる

remは常にhtml基準なので、sec-<id>{font-size:…} に寄せても効かない(ボタン文字が0.89pxになった)。
--sec-root を定義し、抽出CSS中の Nrem を機械的に calc(N * var(--sec-root)) へ置換する--sec-root: var(--liquid-htmlroot) にすると、元サイトのメディアクエリ(1440基準→375基準)もそのまま効く。
→ 併せて 抽出CSSに残る html{font-size:var(--liquid-htmlroot)} は落とす(二重適用になる)。

next/font の --font-xxx は別ルールで定義されている=拾い漏れると別フォントで描画される

font-family:var(--font-unbounded) だけコピーしても変数定義が来ないので Zen Kaku Gothic New にフォールバックしていた。
セクションのCSSに --font-unbounded:"Unbounded",sans-serif; 等を明示的に足す(Google Fontsから読む)。数値検証で getComputedStyle(el).fontFamily を必ず確認する。

サムネは「ページ先頭・スクロール無し・透明要素は全部表示」で撮られる

shoot_section.mjs は先頭を撮り、reveal() が opacity<0.15 の要素を強制表示する。そのため
- スクロール用の先頭余白(100vh等)を入れるとサムネが空白になる → 余白は入れず、スクロール余地が無い時は演出を一度だけ再生する分岐にする。
- ホバー時だけ出るレイヤ(目・手など)が全部出る → 装飾要素に aria-hidden="true" を付ける(reveal は aria-hidden 配下を触らない仕様)。

terasu-lab は同時セッションが多く「共有インデックス」で他人のファイルを巻き込む

git add から git commit までの間に別セッションが stage したファイル(他案件のentry/preview)が自分のコミットに混入した。さらに他セッションの未コミット変更があるため git pull --rebase が実行できない。
別worktree(git worktree add /tmp/... origin/main)を作り、自分のパスだけ git checkout <commit> -- <path> で持ち込んでコミット→push。作業ツリーに一切触らないので他セッションを壊さない。終わったら git worktree remove

WebGLの画像帯を近似するときの素材の取り方

フッターの写真帯は fv_videos.mp4(3×3タイル・35MB)を貼ったWebGL。近似のためにハイライト画像を探すより、元の動画から ffmpeg で3×3を切り出して9枚の静止画にするのが最も元に近い(crop=1280:720:x:y)。ffmpegが無い環境は npm i ffmpeg-static で即用意できる。

クラス名の一括リネームで sec-<id> スコープ名を壊す(2026-08-18・The Day Osaka)

c-news → newsslide の単純置換が sec-news-swiper-fullbleed-saki-0817 の中の c-news("se|c-news|-swiper…")まで置換し、
スコープ名が senewsslide-swiper-… に化けた。CSSもHTMLも同じ文字列なので見た目は崩れず、気づかないまま登録されるのが怖い。
→ リネーム後は必ず grep -o 'class="sec-[a-z0-9-]*"'ルートが sec-<id> のままかを機械確認する。
→ そもそも id に news header footer 等の一般語が入ると旧クラス名と衝突しやすい。置換は長い名前から順に、かつ置換対象から sec- 始まりを除外するのが安全。

ヘッダー単体はサムネ検品に必ず落ちる → 「メガメニューを開いた状態」で撮る

白いヘッダー帯だけのセクションは 中身が写らない(色11種/輪郭0.7%) で撮影が2回とも不合格になる(正しい判定=カードが真っ白になる)。
--force で通しても上に帯・下8割が白の使えないカードになる。
tools/shoot_section.mjs を一時コピーし、prepare(page) の直後に ①閉じているドロワーを display:none ②メガメニューの is-megaOpen/is-show を付与 する処理を挟んで撮ると自然に合格する(1107KB・中身558px)。
→ このとき メガメニュー下の暗幕(megaBg02 = rgba(51,51,51,.3))も外す。元サイトでは下のFVを暗くする正しい演出だが、
セクション単体では「下半分が灰色一色」の絵になり、原因を暗幕と気づくまで2回撮り直した。撮り終えたら一時コピーは削除する(リポに残さない)。

動画で「どの箇所か」を渡された時、このPC(咲輝機)ではフレーム抽出が両方死んでいる

ffmpeg 未インストール、かつ /usr/bin/swift も CommandLineTools の SwiftBridging 再定義エラーでビルド不可、
システムpython3にPyObjC(AVFoundation)も無い。→ フレーム抽出に時間を使わない。参考URLがあるなら実サイトを直接触って動きを確認するのが最短。
どうしても必要なら npm i ffmpeg-static(上の項目と同じ手)。

2026-08-17 kishun.co.jp(株式会社貴瞬)から7セクション追加で得た知見

① 参考元のフォントは他ドメインから読めない(CORS)=全セクションが代替フォントで描画される
kishun.co.jp のフォント(woff2)は access-control-allow-origin を返さないため、@font-face をそのまま持ってきても読み込み失敗して黙って代替フォントになる。見出しが1行に収まらない等の微妙なズレの正体がこれ。
参考元ドメインの @font-face は入れず、同じ書体を配布元CDNから読む(Noto Sans JP=Google Fonts / General Sans=Fontshare https://api.fontshare.com/v2/css?f[]=general-sans@300,400,500,600,700)。
→ 検証は document.fonts.check() だけだと false でも実際は効いていることがある。canvasで同じ文字列の描画幅を実サイトと突き合わせるのが確実(今回 "Statement" 123px・本文226px で一致を確認)。

② スケール基準の CSS変数を JS で入れているサイトがある
このサイトは clamp(14px, calc(20 * var(--mw) / var(--bw)), 24px)--mwJSで window.innerWidth を毎回代入していた(--mw の初期値 1920px のままだと文字が最大値に張り付いて別物になる)。
→ 抽出時は --mw: 100vw に置換すればCSSだけで同じ挙動になる。「clampなのに元とサイズが違う」ときは変数の出所をJSまで追う

:where(:root) のベース指定を拾い忘れると全体の行送りがズレる
:where(:root){line-height:1.5; text-spacing-trim; text-autospace; line-break; overflow-wrap} が効いていた。line-heightnormal になると見出しの高さが 149px→119px のように変わる。
→ scoped base に line-height:1.5 +日本語まわりの既定値まで移植する(html{font-size} だけ見ていると漏れる)。

④ レスポンシブ検品の⛔が「プレビューHTMLの body 既定マージン8px」由来のことがある
position:fixedleft 未指定の要素(メガメニュー)は静的位置=8px から始まり width:100%(=viewport) で右に8px はみ出す。カルーセルも同様に4pxオーバーになる。
→ 直し方は2つとも1行: left:0 を明示セクションの器に overflow-x: clip(参考元が body{overflow-x:clip} でやっていることのスコープ版)。見た目は変わらない。

⑤ 同じ日に他メンバーが登録していてIDが衝突する(今回2件)
footer-sitemap-saki-0817 header-globalnav-saki-0817 が既存だった。add_section.py は entries の重複は止めてくれるが、code-previews/<id>.html は先に書くと黙って上書きされる(=他人の作業を消す)。
書き込む前に ls code-previews/<id>.html entries/<id>.json で空き確認。衝突したら git checkout -- <file> で戻してからID変更(今回 footer-sitemapsns / header-frostmenu に変更)。

⑥ terasu-lab が他セッションの作業中で分岐しているときは worktree で回避
git pull --ff-only が ahead 15/behind 20 で失敗し、作業ツリーには他人の未コミット削除が残っていた。この状態で git add -A すると他人の作業を消す。
git worktree add <別ディレクトリ> -b <作業ブランチ> origin/maingit submodule update --init でクリーンな場所を作り、そこで登録→git push origin HEAD:main。本体リポには一切触らない。node_modules は本体からシンボリックリンクで共有できる。
→ push は他メンバーと競合するので fetch→pull --rebase→build.py 再実行→push を数回まわす。自分のrunが cancelled になっても後続runが最新mainを配信するので、本番URLと /sections の掲載で確認する(今回 908カード中に7件掲載を確認)。

⑦ 動画だけ渡されたときのフレーム確認
このMacに ffmpeg は無く、swiftc も SDK不整合で使えない。Playwright(Chrome) で <video> を seek → canvas に並べてコンタクトシート化が実用的(file:// はcanvasが汚染されるのでローカルHTTPで同一オリジンにする/player HTMLはキャッシュされるので id ごとに別ファイル名にする)。

2026-08-18 kishun.co.jp /contact/ のフォームを追加して得た知見(contact-form-split-saki-0817)

Contact Form 7 の「見えないスピナー」が送信ボタンの中心を36pxずらす

CF7 の送信ボタンには <span class="wpcf7-spinner">visibility:hidden / width:24px / margin:0 24px)が必ず並んでいる。
justify-content:center の中で幅72pxの不可視要素が場所を取るため、実サイトのボタンは中心より 36px 左に寄る。
→ これを省くとボタンだけ右にズレる(今回それで37px差が出た)。不可視スピナーごと再現するのが正解。

sec-<id> ルートの背景が上に抜けるのはマージン相殺

中の .pContact{margin-top:25.7vw} がルート外へ抜けて、背景色がその分だけ塗られない(上が白く残る)。
→ ルートに display:flow-root を1行足すだけで元と同じ塗りになる(レイアウトは1pxも動かない)。
あわせて <body style="margin:0;background:<セクション背景色>"> にする(body既定8pxで全体が8px下・末尾の空白行ボックスで下に白帯が出るのも同時に消える)。

リセットに textarea{vertical-align:top} を勝手に足すと5pxズレる

元サイトの reset には無い指定を「良かれと思って」足すと、インラインブロックの行ボックスが変わり以降が5px上がる
→ ベースは元サイトにある物だけをスコープ化して移植する。迷ったら足さない。
同じ理由で、元DOMのラッパー(CF7の span.wpcf7-form-control-wrap 等)も外すと行ボックスが変わるので残す(クラス名だけ改名)。

共有インデックス経由で「他セッションのステージ済みファイル」を巻き込む(実害を確認)

git add <自分のファイル>git commit としても、別セッションが先に git add していたファイルが同じコミットに入る
(今回 servicecards-dark/light の entries+画像 6件が混入。共有リポなのでインデックスは1つ)。
→ 対策は2段: ① コミット後に git show --stat で中身を必ず確認する。
② push は一時 worktree(git worktree add --detach <tmp> origin/maingit checkout <自分のcommit> -- <自分のファイルだけ> → commit → git push origin HEAD:main)。
本体の作業ツリー(他人の未コミット変更)に一切触らずに自分の分だけ本番へ出せる。

デプロイrunが cancelled でも慌てない

セクション追加が同時多発している日は、後続pushが前のrunをキャンセルする(concurrency)。
自分のrunがキャンセルでも後続runが最新mainを配信するので、判定は run ではなく 本番URLで行う
/section/<id>/ が200+/sections にidが出る+プレビュー実描画)。

このMacで動画のフレームを見る最終手段

ffmpeg 無し・PyObjC 無し・swiftc はSDK不整合・headless Chrome は .mov を canvas に描けない、で全滅した時は
qlmanage -t -s 1400 -o <dir> <file.mov> でポスターフレーム1枚だけ取れる(今回はこれで箇所を特定できた)。
複数フレームが要るときは既出の Playwright+ローカルHTTP方式を使う。


2026-08-18 野村不動産PMO スペック(左テキスト+フェード自動スライダー)で得た学び

「全部崩れてる」= <style> の中で文字列が閉じていないを最初に疑う

症状: 背景色とフォントサイズだけ効いて、レイアウト指定(flex/幅/高さ)が丸ごと無視され画像が縦積みになる。
真因: @font-facesrc:url("data:...base64,XXXX") の base64 が重複して書き込まれ、引用符が閉じないまま
以降のCSSをパーサが全部捨てていた(18,855字 → 正しくは 2,307字)。
- 見つけ方: {/} の数は合うので括弧チェックでは出ない。@font-face を丸ごとCDNの正しい物と差し替えるのが最短。
- 予防: 生成後に「(a) 先頭の .sec-<id>{} が効くか (b) 末尾側のセレクタも効くか」を両端で1つずつ実測する
getComputedStyle(該当要素).display を見るだけ。片方だけ効いていたら途中で死んでいる)。

mask-image は別オリジンだと CORS で読めない=アイコンが消える

一時停止ボタン(-webkit-mask-image:url(参考元/pause.svg))が無言で消える。画像<img>は別オリジンでも出るので気づきにくい。
mask/背景に使うSVGだけは data URI で埋める(元ファイルの中身そのままをbase64化=再現度は落ちない)。写真はURLのまま。

ベースに img{max-width:100%} を「良かれと思って」足すと画像高さが変わる

元サイトは #pxBase img{max-width:100%;width:100%;height:auto} のように祖先ID配下のグローバルで画像を決めていた。
これを拾わず自作リセットを当てると、height:100%!important と喧嘩して画像が 500px → 494px になる(=下に隙間)。
→ 抽出時は #<主要コンテナID> 〜 のルールも必ず拾って sec-<id> 根に寄せる。逆に元に無い指定は足さない。

shoot_section.mjs の1枚化前処理が「重なり型スライダー」を消す

oneSlide は器の子のうち slide|item に当たる先頭要素だけ残して他をdisplay:noneにする。
Swiper fade + 先頭に swiper-slide-behind のような非スライド要素がある構造だと、残るのが実体のない要素になり
右カラムごと消える(サムネが見出しだけ・中身605px)。
→ その場合は自前で撮る: 1440幅・4:3・deviceScaleFactor 2 で file:// を開き、DOMは触らず自動送りだけ止めて
img/sections/<id>.png に保存 → python3 tools/normalize_thumbs.py <id> で正規化(採用ゲートは通る)。

ブラウザペインが非表示だと <video> のフレームが進まない

currentTime を動かしても requestVideoFrameCallback が発火せず、何枚撮っても1枚目と同じ絵になる(md5が全部同一で気づける)。
→ 動画の箇所特定は Playwright(node_modules/playwright をそのまま import)+ローカルHTTP配信で撮るか、qlmanage -t

作業途中の code-previews/<id>.html は他セッションの git add -A巻き込まれて本番へ出る

今回、完成前(壊れた状態)のHTMLが別セッションのコミットに入って origin/main に載っていた。
→ 気づき方: publish用の一時worktreeに自分のファイルをcopyした時、新規のはずが M(modified) で出たら「既に上流にある」。
→ 直し方はそのまま完成版で上書きコミット(退化ではなく修復)。完成までは code-previews/ に置かない運用が本来は安全。

2026-08-18 The Day Osaka(3箇所まとめて追加)で踏んだ落とし穴

同じサイトの同じ箇所を別セッションが並行で作っていた(二重作業)

theday.osaka の News / リンク帯 / フッターを3件作り切った直後に、別セッション(同じ「さき」名義)が
news-swiper-fullbleed linkband-dotted footer-sitemap-shape として同じ3箇所を完成させていたことが判明。
着手前に必ず既存・作業中を確認する(entries は未コミットでもディスクにある):

grep -l 'theday.osaka' ~/dev/terasu-lab/entries/*.json

→ 2026-08-18 大串判断は「両方とも載せる」。消し合わない。

id衝突は add_section.py が止めるが、code-previews/<id>.html は先に上書きされている

footer-sitemap-saki-0817 が既存(千代田工販)と衝突。add_section.py は ⛔ 既に存在します で止めてくれるが、
その前のビルドで他人のプレビューHTMLを上書き済みだった(実害は出る前に気づけたが危険)。
HTMLを書く前に存在チェックする: ls entries/<id>.json code-previews/<id>.html
→ idは <主題>-<種別>-<担当>-<MMDD> でも衝突する。主題を具体的にする(footer-sitemapfooter-hotelinfo)。

一時ディレクトリ(/private/tmp のscratchpad)はセッション中に消える

DOM・CSS・抽出スクリプトが作業途中で全部消えた。
中間成果物が消えた前提で1コマンド再生成できるスクリプトにしておく(curl→抽出→ビルドを1本にまとめる)。

IntersectionObserver はブラウザペインで発火しないことがある

スクロールフェード(is-animeActive)が付かず、セクションが opacity:0 のまま真っ白に見えた。
元 script.js と同じ scroll ハンドラ方式にすると確実に動く(しかも元挙動に忠実):

function scrollTrigger(){ trg.forEach(el=>{ const top=el.getBoundingClientRect().top+scrollY;
  if (scrollY+innerHeight > top) el.classList.add('is-animeActive'); }); }
scrollTrigger(); addEventListener('scroll', scrollTrigger, {passive:true});

100vwフルブリードのスライダーは「外側の器」が切っていた

元サイトは .l-outer{overflow-x:hidden} がセクションの外ではみ出しを切っており、抜き出すとその器が無くなって
check_responsive.mjs が PC/タブレットで横はみ出しを検出(3735>1440)。
sec-<id>{ overflow-x: clip; } を足す(見た目は不変・stickyも壊さない)。抜き出し前に
「どの祖先が overflow を持っているか」を実測しておくと早い:

let el=target; while(el){ console.log(el.className, getComputedStyle(el).overflowX); el=el.parentElement; }

shoot_section.mjs の検品は「細い白帯セクション」で誤検知する

guide-shapelink(上余白240px+帯37px)が 中身が写らない(色12種/輪郭2.4%) で不合格。
実際は Facility/Q&A/Pamphlet も点線区切りも緑シェイプもちゃんと写っていた=描画失敗ではなく元デザインが淡いだけ
→ 絵を目視して中身が写っていれば --force で採用してよい(単色・真っ白なら本物の失敗なので直す)。

ローカルとリモートが分岐したまま作業が進むことがある

着手時は Already up to date だったのに、push直前には ローカル25 / リモート40 コミットの分岐(うち21件は同内容の重複)。
→ 解消手順(2026-08-18 大串「今すぐ分岐を解消してpush」で実施・無事完了):

git branch backup-local-<日付> HEAD            # ① 退避(必須)
tar czf <backup>.tar.gz entries code-previews  # ② 作業ツリーも退避
git add <自分のファイル>; git commit -- <自分のファイル>   # ③ 自分の分だけ部分コミット(他人のstaged分を巻き込まない)
git stash push -u -m "other-session-wip"       # ④ 他セッションの作業を退避
git rebase origin/main                          # ⑤ 重複21件は "already upstream" で自動スキップされる
#    競合が `sections-data.json` だけなら --ours で潰してよい(CIが build.py で再生成する)
#    プレビューHTMLの add/add 競合は「idとファイル名が一致し、html/body 指定が無い方」が正
git push origin main && git stash pop           # ⑥ 戻す(競合したら HEAD 版に寄せる。stashは消えないので復旧可)

CIが core/build_core.pybuild.py を回すので、生成データ(sections-data.json / data/*.json)はコミット不要
これを知っていると「他人の未完了分を巻き込まないと整合が取れない」というジレンマが消える。


2026-08-18 野村不動産PMO(pmo-web.com)4件 — 事故と対策4つ

参考: https://www.pmo-web.com/ → fv-slidein-kv-saki-0817 / photowall-autoslide-saki-0817 / value-sticky-split-saki-0817 / lineup-cardslider-saki-0817

1. id衝突を「ファイルを書く前」に確認する(今回いちばんの事故)

fv-fadeslider-saki-0817既存のLIVIOのFVで使用済みだった。先に code-previews/<id>.html を Write してしまい既存を上書き(=退化)。大串の「マルイチ開くとLIVIOのFVが出る」で発覚した。
- add_section.py には --force ガードがあるが、HTMLを先に書く手順だとガードより前に壊れる
- 着手時に必ずgit ls-files --error-unmatch code-previews/<id>.html entries/<id>.json(ヒットしたら別idにする)。
- 既存が壊れていないかは git diff --quiet HEAD -- <path> で確認できる(今回は無傷だった)。

2. base64(フォント等)は絶対に手で書かない

swiperの矢印グリフを埋める際、base64を手打ち/記憶から書くと壊れる。さらに壊れたdata URIは
引用符が閉じず、@font-face以降のCSSが丸ごと無効になる(=素のHTMLが縦に伸びる「崩れ」)。1440幅では偶然気付けず1280幅で発覚。
- 手順:curl で実CSSを落とす → 正規表現で @font-face{...} を機械抽出 → python で置換 → <style>内の "' が偶数か検査
- 崩れの機械判定:ページを開いて document.styleSheetscssRules 総数が0でないかscrollHeight が想定内か。

3. サムネ撮影ツール(shot_common.oneSlide)の前処理に当たらない作りにする

oneSlideクラス名に slide|item を含む子を1枚だけ残して他を display:none にする。スライダー系はここで真っ白/1枚だけになる。
| 症状 | 対策(実表示は不変) |
|---|---|
| FVが真っ白(残った1枚が translate(-100%) のまま) | .swiper-slide:first-child .item{transform:translate(0)} を足す |
| グリッドのセルが空で写る(wrapperのtransformが残る) | 各セルに1枚目と同じ写真を background: url() center/cover で敷く |
| カード横並びが1枚しか写らない | Swiperの slideClass/slideActiveClass/...slideitemも含まない独自名にする(lc-card等)。lc-slide は "slide" を含むので当たる |

4. 同じリポを別セッションと同時に触らない

別セッションが git add -A を使っていたため、こちらの作りかけHTMLが相手のコミットに巻き込まれ、逆にこちらの未追跡ファイル(entries/サムネ/node_modules)が消えた。マージ競合が未解決のまま放置される時間帯もあった。
- 着手前に git log --since="30 minutes ago" --oneline | wc -l他セッションの活動量を見る。多ければ待つ。
- 自分は必ずパス指定コミットgit commit -- <自分のファイル>)。git add -A は使わない。
- 相手のステージを外す必要がある時は git restore --staged内容は消えない)。git checkout/clean は使わない。

その他(今回の抽出で効いたもの)

  • 別オリジンの mask-imageCORSで落ちる → 同じ図形を data URI 化する(pause/play アイコン)。
  • 元サイトが body{overflow:clip} で切っていた分は、器側に overflow:clip を付ける(overflow:clip はスクロールコンテナを作らないので position:sticky を壊さない)。
  • html{font-size:62.5%}(1rem=10px)の site は、rem を px に換算して持ってくるとホストHPのルート指定に依存しない。

2026-08-20 LUMA(luma-pet.com)5件 — 抽出パイプラインで踏んだ地雷4つ

参考元: https://luma-pet.com/ ・ https://luma-pet.com/about/ / 追加者=さき

① 疑似要素セレクタを querySelector で判定すると必ず落ちる(影響が一番大きい

セクションが使うCSSを「そのセレクタが配下にマッチするか」で選別する方式だと、
.about_greeting_img::after のような疑似要素つきセレクタは querySelector が例外を投げて false になり、丸ごと捨てられる
実害=写真の上に重なる線画アイコンが消えた(見た目でしか気づけない)。
→ 判定前に ::before/::after:hover/:focus/:active/:visited 等を外した文字列で照合する

const norm = s => s.replace(/::[\w-]+(\([^)]*\))?/g,'')
                   .replace(/:(hover|focus|active|visited|target|checked)\b/g,'').trim() || '*';

② CSSOM の rule.style.cssTextvar() 入りショートハンドを壊す

padding: var(--inner-padding)padding-right: ; padding-left: ;値が空)になって出力され、
セクション左右の余白が丸ごと消えた(元 x=128px → 再現 x=0)。rule.cssText でも同じ。
CSSOMは「どのルールを採用するか」の判定にだけ使い、本文は生CSSを自前パースしてそのまま使う
@import を再帰解決 → url() を絶対化 → ブレース走査でルール分解 → ブラウザ側は採否判定だけ返す)

③ 親コンテナの制約が抜けて幅が合わない

#about_greeting{ padding: var(--inner-padding) } のようにルート自身、あるいは #contents など祖先
幅・余白を決めている。セクション単体を切り出すと当然消える。
ルートの祖先チェーン(body直下〜親)をそのままラッパーとして再現し、祖先にマッチするCSSも採用対象に含める。

④ 背景が position:fixed のセクションはそのまま持ってくると貼り先HPを覆う

LUMAのTOP ABOUTは背景を持たず、FV(#idx_main .img-back)の position:fixed レイヤーが透けているだけだった
elementsFromPoint で確認できる)。これを気づかず抜くと背景なしの灰色になる。
かといって fixed のまま持ってくると、貼り先HPでビューポート全面を覆って事故る。
→ 背景レイヤーも一緒に抜き、セクション内に閉じた absolute(全高) + 中身を sticky(100vh) に置き換える
見え方は元と同じまま、外へ漏れない。

数値照合は「出現アニメ後」に取らないと全部ズレて見える

.inv-item 系は初期状態で translateY(20px)。元サイトを読み込んだだけで測ると全要素がy+20になり、
再現側が間違っているように見える。→ 比較スクリプトは両方 scrollIntoView して2.5秒待ってから測る。
<source> <br> など幅0の非描画要素は比較から除外する(永遠に一致しないが見た目に無関係)。

同じPCで別セッションが同じリポを触っている前提で動く(今回いちばん危なかった)

~/dev/terasu-lab作業ツリーを共有する。今回、別セッションが並行して同じLUMAを作っており、
luma-top-fv-saki-0820 / luma-top-about-saki-0820完全に同じidになって、
cp で相手の未コミットファイルを上書きしてしまった(相手のビルドスクリプト _make_luma.mjs を再実行して復旧)。
- 着手直前だけでなく、ファイルを置く直前にもう一度 ls code-previews/<id>.html で衝突確認する
(今回は着手時0件→作業中に相手が作成、という時間差だった)
- リポ直下の _*.mjs など他セッションの作業ファイルからidを拾って照合する
grep -ohE "luma-[a-z0-9-]+-saki-0820" _*.mjs | sort -u
- 未コミットの他人ファイルは git で戻せない。上書きしたら相手のビルドスクリプトで再生成できないか探す
- git pull が submodule core のダーティで Aborting すると、HEADだけ進んで作業ツリーにファイルが無い状態が残る
ctsap-* の2件が消えていた)。git statusD が出たら git checkout -- <path> で戻す


2026-08-20 ANA新千歳空港(ctsap.ana-g.com /about)から2件(下層FV/追従目次2カラム)

mask-image はクロスオリジンだと無音で消える(CORS)

  • 参考元の -webkit-mask:url(.../icon-plane-tail.svg)絶対URLのまま持ってくると、404も出さずにアイコンだけ消える(サブタイトル横の飛行機マークが丸ごと消えていた)。background-image はクロスオリジンで出るので「画像は出るのにマスクだけ出ない」という形で現れる。
  • CSS内の .svg は全部 data URI 化する(既存の ctsap-pagehead-saki-0820 も base64 で入っていた=これが正解の型)。検出は目視でしかできないので、マスクを使うルールがあったら data URI 化を機械的にやるのが速い。

自ホストのwebfontは Access-Control-Allow-Origin が無くLabからは読めない

  • fonts.css の @font-face を絶対URLで持ってきても、フォントはCORS必須なので document.fonts.load() が NetworkError(=フォールバック描画)。curl は200なのでURL確認だけでは気づけない
  • 症状は数値照合にだけ出る: No.1 の幅が 147px(元は139px)。レイアウト箱は一致するので目視では絶対に分からない。
  • 同一書体を Google Fonts の @import に置き換える"Inter Variable""Inter")。fontsource=Google Fonts なので書体は同一・メトリクスも一致(置換後に 139px で完全一致)。Lab既存515件中400件が Google Fonts 方式。
  • 検証は document.fonts.check('bold 68px "Inter"') を見る。false なら読めていない。

body.xxx .foo{} をスコープ化すると常時適用になって高さがズレる

  • body.is-section-upper-mg .l-two-column{padding-top:8.75rem}.sec-x .two-column{...} に変換したせいで +20px
  • 前置きが body.クラス / html.クラス のルールは捨てる(そのページ限定の状態指定なので、単体セクションでは成立しない)。素の body{} は寄せる、body.xxx{} は捨てる、と分けるのが正解。

素タグリセットの写し漏れは「高さの丸い差」で出る

  • pmargin-block:1em(ブラウザ既定)のまま出したら +96px(32px×3段落)。元サイトは p{margin:0} 相当だった。
  • リセットは推測で書かない。元サイトで getComputedStyle(p).marginTop を実測してから書く(今回 0px を確認して修正)。

/tmp/section_<site>/担当名まで入れないと別セッションと衝突する

  • 同じPCの別セッション(さき)が同じ ctsap を同時に処理していて、/tmp/section_ctsap/ を共有していた(相手の pagehead.html service.html _ph.css 等が同居)。DOMダンプを取り直されていたら別サイトを見て作る事故になっていた。
  • /tmp/section_<site>_<担当>/ にする。コマンド本体の「サイト別」だけでは足りない。
  • 併せて 同じサイトを別担当が同時に触っている可能性を前提に、id は <site>-<箇所>-<担当>-<MMDD> の担当名で必ず分ける(今回 ctsap-pagehead-saki-0820ctsap-about-fv-ogushi-0820 が別物として共存)。

Labは未デプロイでも200を返す(フォールバックHTML)

  • curl -o /dev/null -w %{http_code} だけ見て「反映OK」と誤判定した。実体は mikke のindex(1.35MB)が返っていた。
  • 反映確認は curl | grep "sec-<id>"(中身で見る)。ステータスコードは反映の証拠にならない。

このMacには ffmpeg が無い/swift もSDK不整合で動かない → 動画のフレーム抽出は Playwright+実Chrome

  • ffprobe/ffmpeg 無し、swift は「SDK is not supported by the compiler」で落ちる、python3 に pyobjc 無し。
  • 動画をローカルHTTPで配信 → chromium.launch({channel:'chrome'})<video> に読ませて currentTime を送りながら要素スクショplaywright同梱のchromiumはH.264非対応なので channel:'chrome'(実Chrome)必須。file://setContent のページから読めないのでHTTP配信にする。

抽出スクリプトの型(今回も数分で完了・再利用推奨)

  • tinycss2 で全ルールを走査 → soupsieve でセクションのサブツリーに実際にマッチするものだけ回収(@media/@container 文脈ごと)。style.css 195KB → FV 35ルール / 2カラム 118ルールまで自動で絞れた(131行 / 219行の自己完結HTML)。
  • 実行時クラス(is-active 等)はDOMダンプに無いので、RUNTIME 集合を作って「セレクタ内のクラスが(実在クラス ∪ RUNTIME)に収まり、かつ1つは実在」なら拾う。これが無いと目次のアクティブ色が丸ごと落ちる
  • スクロールスパイは元JSを読んでから移植する(IntersectionObserver / rootMargin:'-50% 0px' / is-active)。推測で書かない。

2026-08-20 a-thily お問い合わせフォーム(動画1本だけで来たケース)

動画からフレームを抜く現実解(このMacに ffmpeg が無い)

「動画で箇所を指定」された時、このPCには ffmpeg も pyobjc も無く、swiftc は2分でもコンパイルが終わらない(AVFoundation import が重い)。JXA の ObjC.import('AVFoundation')$.AVURLAsset が undefined で不可。
通ったのは headless Chrome(Playwright)で <video> を読ませて canvas で抜く方法。動画をローカルサーバーで配信し、video.currentTime を送って onseekedtoDataURL する。5秒の収録から12枚を数秒で取得できた。
- node は PATH に無い → /Applications/ChatGPT.app/Contents/Resources/cua_node/bin を PATH に足す
- playwright は ~/dev/terasu-lab/node_modules にしか無い=スクリプトはリポ配下に置かないと ERR_MODULE_NOT_FOUND(scratchpad に置くと解決できない)
- ポートは別セッションが既に使っている(8080/8099/8123 は埋まっていた)。lsof -nP -iTCP:$p -sTCP:LISTEN で空きを探してから使う。埋まったポートに繋ぐと別セッションの成果物が返ってきて混線する(FB⑦の実例そのもの)

数値照合は「主要要素8個」より「全要素総当たり」が速くて確実

getBoundingClientRect() をセクション配下の全要素(今回67個)で取り、w|h|x|y|font-size|color|background|font-family を1行に連結して行単位でdiffする。合否が一目で出る。
合格ラインは「差分が display:none の要素だけになること」display:none は rect が 0,0 で返るため、元ページ(セクションが y=870 にある)と再現物(y=0)で相対座標がズレて必ず差分として出る=これは偽陽性なので無視してよい。今回は 67要素中この5件だけ残して一致。

今回踏んだ3つの罠(次回は先に潰す)

  1. body の初期 margin 8px で幅が16px縮む(1440→1424)。<style>body{margin:0} を書くとスコープ漏れgrepに落ちるので、<body style="margin:0;background:#fff"> の属性で吸収する。属性ならセクションdivをホストHPに貼っても付いてこない=ゲートも思想も両立。
  2. body,html{font-size:62.5%} の site は、セクションの継承font-sizeが 10px ではなく 6.25px(html で62.5%→10px、body で更に62.5%→6.25px の二重適用)。ルートに font-size:10px を入れると継承値が全部ズレる。「rem基準=10px」と決め打ちせず、必ず元サイトの実測(対象要素の computed font-size)に合わせる(トップ10の#2の実例)。rem 自体は Xrem → X*10px に機械変換してホスト非依存にした。
  3. :placeholder-shown を潰す正規表現バグ。擬似要素を剥がす時に /::?(before|after|placeholder|…)\b/ と書くと :placeholder-shown の「placeholder」だけ消えて input[type="text"]-shown という不正セレクタになり、そのルールが丸ごと落ちる(入力欄の color が #ddd → 黒になった)。placeholder(?!-) にする。

効いた手順(このサイト系=WordPress + 単一 style.css)

実ページを Playwright で開き、document.styleSheets を走査してセクション内の要素に実際にマッチするセレクタだけを集める → そのセレクタ集合で元CSSの原文から該当ルールを切り出す(Chromeの cssTextcalc(calc(770/1400)*100vw)calc(55vw) に潰す・border-top-color: ; のような壊れた直列化もするので、原文から取り直すのが正解)。185行で完全一致。
Google Fonts などクロスオリジンのstyleSheetは cssRules アクセスで例外を投げる。try/catch をループの外に置くとそこで全部止まって :root 変数を1つも拾えない(トップ10の#1に直結)。変数は元CSSから正規表現で別途拾うのが安全。

2026-08-20 動画だけで来た素材の扱い(a-thily 創業ヒストリー)

ffmpeg が無いMacで動画からフレームを抜く(このPCは ffmpeg 未インストール)

qlmanage は1枚しか取れず、JXA の ObjC.import('AVFoundation')ブリッジが効かず $.AVAsset が undefined(BridgeSupport はあるのに使えない)。pyobjc も未導入。
正解=Playwright(Chrome) で動画を再生して requestVideoFrameCallback で canvas に描き出す
- chromium.launch() の既定ビルドは H.264/AAC を再生できない{channel:'chrome'} で実機Chromeを使う(実測: 既定ビルドは loadedmetadata すら来ない)
- file:// + setContent は動画を読めないpython3 -m http.server で配信して http://127.0.0.1:<port> から読む
- currentTime を飛ばして drawImage すると全フレーム同一画像になる(seek後の再描画が来ない)。再生しながら requestVideoFrameCallback で拾うと確実
- 素材の判定用なので toDataURL('image/jpeg',0.7) + 1/2.6 縮小で十分(読む前に sips -Z 1100 も必ず通す=画像2000px制限)

「セクション末尾の要素だけ発火しない」=スクロール余地が無いのが真因

元サイトの発火条件は scrollTop > 要素top - 画面高/1.6セクション単独ページは要素の下にコンテンツが無く最下部までスクロールしても条件を満たせないため、末尾の要素(今回は .fire_txt「燃えるような毎日は…」)だけ opacity:0 のまま残る。
元の条件はそのまま残し、「最下部に到達 かつ 要素が画面内」なら発火を1行足すのが正解(全部を無条件に表示する保険にすると、スクロールで見るはずのアニメが最初から消える)。

同時実行では作業ディレクトリ名まで衝突する

別セッションが同じ /tmp/section_<site>/ を使っていた(同じサイトを別担当が並行処理)。$WORK をサイト別にするだけでは足りず、セッション別に接尾辞を付ける(例 /tmp/section_athily_msg/)。
python3 -m http.serverポートも取り合う(8080/8099/8123 が既に使用中だった)。lsof -nP -iTCP:<port> -sTCP:LISTEN で空きを探してから起動する。
git も同時に走るので .git/index.lock で commit が落ちる → 数秒待ってリトライするループで通す(git add -A は当然禁止・自分のidのファイルだけ add)。

追記(同日)— FV+直後のセクションを1本に繋げる時のsticky背景の作り方

大串「④と⑤は一緒。繋げて」で FV(#idx_main) と ABOUT(#idx_about) を1セクションに統合。
背景(元は position:fixed)をセクション内に閉じ込めて追従させるまでに、3段階ハマったので手順を固定する。

  1. position:fixed の中に sticky を置いても効かない(スコープに transform を当てて fixed を閉じ込める手は、
    子の sticky が死ぬので不採用)。→ 背景レイヤーを FV の外へ出しSCOPE 直下に
    <div class="bg-layer"> として置く(CSSの #idx_main .img-back.bg-layer .img-back に機械置換)。
  2. overflow: hidden は sticky を殺す(スクロールコンテナ化して、そのコンテナが動かないので固定されない)。
    背景ラッパーは overflow: clip にする。
  3. 真犯人は body{overflow-x:hidden}。スコープ化すると SCOPE{overflow-x:hidden} になり、
    セクション全体がスクロールコンテナ化して sticky が完全に無効になる。overflow-x: clip; overflow-y: visible; に置換する。
    ただし元サイトが float レイアウト(main/#idx_main/#idx_containarfloat:left)だと、
    BFCが消えてセクション高さが0に潰れるdisplay: flow-root を併記して包含し直す。

最終形(これをテンプレとして使う):

SCOPE { position: relative; display: flow-root; overflow-x: clip; overflow-y: visible; }
SCOPE .bg-layer { position: absolute; inset: 0; height: 100%; overflow: clip; z-index: 0; }
SCOPE .bg-layer .img-back { position: sticky; top: 0; width: 100%; height: 100vh; }

検証は必ずスクロール途中で測るwindow.scrollTo(0,1500) して getBoundingClientRect().top が 0 なら効いている)。
全画面キャプチャだと sticky は自然位置で写るので「効いていない」と誤判定する。

追記(同日)— 共有worktreeでは cp の直前が勝負/自分が置いたはずのファイルが別物になる

sed ... > 新id.htmlリポの中でやった数分の間に、別セッションが元ファイルを自分の版で上書きしており、
相手の中身が自分のid名で commit・push されたluma-top-fv-slider / luma-top-about-parallax が相手の
luma-top-fv / luma-top-about と中身完全一致になっていた)。<title> で照合して初めて気づいた。
- リネームや加工はリポの外(作業ディレクトリ)で完結させ、完成品だけを最後に1回コピーする
- push後に <title> や自分だけのマーカーで「本当に自分の中身か」を照合する(200確認だけでは足りない)
- 他セッションがインデックスにファイルをstageしていることがある。git commit -m "..." -- <自分のパス…>
パス限定コミットなら、相手のstageを巻き込まず自分の分だけ入れられる(git add -A はもちろん論外)
- git pull --rebase がインデックス汚れで止まる時は --autostash本番反映の確認は「バイト数+title」で
(未デプロイだと Lab は一覧ページ(約1.3MB)を返すので、200だけ見ると反映済みと誤判定する)

2026-08-20 ANA新千歳空港(ctsap)お知らせ一覧+ページネーション

  • 自社ホストのwoff2はCORS未対応が多い=そのまま持ってくると別ドメイン(Lab)でフォントが読めないcurl -sI <woff2> | grep -i access-control が空なら、同一書体をGoogle Fonts(Noto Sans JP / Inter / Material Icons Outlined)に置換する。フォント名が "Inter Variable" のようなセルフホスト用の別名になっている時は、CSS内の名前も "Inter" に置換しないと当たらない。再現度の支配要因なので着手直後にCORSを確認する
  • CSS mask: url(...) はクロスオリジンだと黙って消える(background-imageと違いCORSが要る)。丸ボタンの中のアイコンだけ出ない=これ。SVGをdata URI化して埋め込む(画像URLは原則そのままだが、maskは例外的に埋め込まないと再現しない)。
  • html:not([lang=en]) body{...} のようなbody前置ルールは、素朴なセレクタ一致では落ちてフォント指定ごと消える。抽出器は「先頭の html/body トークンを剥がしてから一致判定し、.sec-<id> 残り として出す」正規化を必ず入れる。:not(a,b) のカンマでセレクタ分割が壊れるので、カンマ分割は括弧の深さを見て行う。
  • 単独プレビューの body 既定マージン8pxで幅が16px狭くなり、数値照合が全部ズレる。Labの既存プレビューと同じく <body style="margin:0">(インライン)で解消。CSSルールにしないのでホストHPに貼っても漏れない。
  • 中身が小さいセクション(ページネーション単体など)は shoot_section.mjs の採用ゲートに落ちる(輪郭1.7%等)。--force で撮ると全画面の中に点のように写って一覧で見えない。→ 要素だけを狭めのビューポート(960幅・dSF3)でelement screenshotし、normalize_thumbs.py で4:3化すると見やすい。ただし幅を詰めすぎると端のボタンが切れる(640幅で「次へ」矢印が消えた)=必ず生成物を目視する。
  • 同じサイトを別セッションが並行作業していると /tmp/section_<site>/ を共有して踏み合う(DOMを上書きされ、別ページのDOMを見て作りかけた)。サイト別に分けるだけでなくセッション別にも分ける/tmp/section_<site>2/ 等)。
  • リポの作業ツリーが他セッションの未コミット変更で汚れていると git pull --rebase ができず push もできないgit commit -- <自分のパス> で他人のステージを巻き込まずコミットし、一時ワークツリー(git worktree add /tmp/... origin/main → cherry-pick → push)で自分の分だけ流す。作業後は git worktree remove で片付ける。

2026-08-20 LUMA /dog/ 6件 — rem はスコープ化では効かない(一発格納率に直結・恒久)

起きたこと: 6件とも「フォントも余白も全部1.6倍に膨らむ」状態で出来上がった。数値照合で font-size:25.6px(元は16px)を検出して真因が確定した。

真因: コマンド本体の「html{font-size:10px} 等の rem基準は sec-<id>{ font-size:10px } へ寄せる」は rem では成立しない
rem は常に <html> 基準で解決されるので、sec-<id> に font-size を移しても rem の基準は変わらない(プレビュー単体なら 16px のまま)。
結果 body{font-size:1.6rem} が 16px ではなく 25.6px になり、そこから継承で全体が1.6倍に膨らむ。1行の宣言で全滅するタイプの崩れ。

正しい直し方(機械変換): 元サイトの html の font-size を実測し(今回 html{62.5%}=10px)、抽出CSS内の Nrem を全部 N×10 px に置換する。

body = body.replace(/(-?[\d.]+)rem\b/g, (m,n)=>(parseFloat(n)*10)+'px');

変換後は html{62.5%} 由来の sec-<id>{font-size:62.5%}不要になるので落とす(残すと % と em の基準を壊す)。
副次効果としてホストHPの html font-size に左右されなくなる=貼っても崩れない

同時に見つかった2つ(どちらも数値照合で出た)
1. sec-<id> *{box-sizing:border-box} を"おまじない"で足さない。 元サイトの reset に全称 box-sizing が無ければ、足した瞬間に元と違うレイアウトになる(今回 reset.css に全称指定なし=content-box が正)。元にある物だけ持ってくる
2. プレビュー単体の <body> 既定 margin 8px が container を左右16px縮め、padding:8.9% 系の内寸が 1184→1171px にズレる。
<body style="margin:0">style属性で持たせる。<style>body{} を書かないのでスコープ漏れ grep も通る。

数値照合は「セクション単位に絞って」やる

元サイトは .section_headline 等が全セクションに存在するため、ページ全体から拾うと要素の並びがズレて差分が読めない(今回1回無駄にした)。
元サイト側も #concept #price 等でスコープしてから比較する。要素数が一致して初めて差分が意味を持つ。

白背景+薄いグレーの表はサムネ採用ゲートの誤検知で落ちる

shoot_section.mjs の「色数/輪郭量」判定は、白地に細罫線+薄グレー文字の料金表を「中身が写らない」と誤判定する(実例 luma-price)。
必ず生成物を目視してから --force を使う。v2.5 の「ヘッダー単体が真っ白でゲートに落ちる」と同系統。

macOS画面収録(.mov)からフレームを抜く最速手段

このPCには ffmpeg が無い。Playwright/Chrome に <video> で読ませる方法はデコードでハングする(2分待っても1枚も出ない・2回試して両方ダメ)。
qlmanage -t -s 1400 -o <出力先> <file> で先頭フレームPNGが即出る。 尺は mdls -name kMDItemDurationSeconds
実務上は先頭フレーム1枚で「どのセクションか」は確定できるので、これで十分だった。

同じサイトを別セッションが並行で触っていることがある

今回 ~/dev/terasu-lab に別セッションの一時スクリプト(_fetch_luma.mjs)があり、LUMAトップページ側6件を並行制作していた。
同じリポの作業ツリーを共有しているので、git add -A は絶対に禁止(相手の削除・変更を巻き込む)。
git pull --rebase も相手の未staged変更で失敗することがあるが、明示addした自分のファイルだけの commit なら push は通る
id接頭辞をサイト+ページで分けていれば衝突しない(今回 luma-*(/dog/)と luma-top-*(トップ)で棲み分け)。


2026-08-20 J-CAT(Studio.design製)ヘッダー+役員一覧で得た知見

参考元: https://j-cat.co.jp/team / 追加: jcat-header-saki-0820 jcat-members-saki-0820(追加者=さき)

Studio.design 製サイトの抽出作法(再現度100%で通った型)

  • スタイルは .sd[data-s-<uuid>] の属性セレクタで当たる。class名は sd / text / link / appear / _boolean1 の汎用名だけなので、
    オリジナル化=data属性のリネームが本体(data-s-*data-e01… / data-r-*data-t01… / --s-color-*--c01… / --s-font-*--f01…)。
    HTMLとCSSに同じ写像を同時適用すれば見た目は完全に不変。data属性を「CMS痕跡だから削除」すると全スタイルが死ぬので消さずに改名する。
  • :root{--s-color-*/--s-font-*} は独立した <style> に入っている(sheet末尾)。取りこぼすと色が全部フォールバックになる(崩れる原因#1そのもの)。
  • リセットは :where(.render-canvas) a,div,header,img,nav,span{…}.render-canvas / .StudioCanvas実DOMに実在する祖先なので、
    抽出時は el.matches(セレクタ全文)完全一致だけで判定できる。→ スコープ化時に :where(.render-canvas).sec-<id> へ読み替えると見た目が保たれる。
    「セレクタ末尾だけで採用」する緩いフォールバックを入れると .StudioCanvas > .sd{min-height:100dvh} を誤って拾う。使わないこと。
  • .appear は「外すと表示」型.sd[data-s-…].appear{opacity:0})。抽出時にクローンから appear / appear-active を除去すれば表示済み状態になる。
  • ヘッダーは二重の非表示ゲート#header._boolean1{visibility:hidden}(DOM内の <style> に埋まっている)+ .sd._boolean1[data-s-…]{opacity:0}
    抽出クローンから <style>/<script> を必ず除去し、この2ルールは採用対象から落とし、単独用に position:relative / opacity:1 / visibility:visible で打ち消す。
  • html の font-size は 16px(62.5%トリックなし)。rem基準の付け足しは不要(崩れる原因#2の誤対応をしないこと)。

数値照合ゲートの落とし穴(2つとも今回実際に踏んだ)

  1. body の既定マージン8pxで全要素が最大16pxズレるbody,html{margin:0}sec-<id> に読み替えると実body側のマージンが残るため、
    ルート幅が 1440→1424 になり x も総崩れに見える。→ <body style="margin:0"> とインライン指定で解決(ページ級CSSを足さずに済むのでスコープ漏れgrepも0のまま通る)。
  2. display:none の要素は getBoundingClientRect() が全部0になるため、元では「ルート基準 y=-254」、再現では「y=0」と出て偽の不一致になる。
    Studioは同じ内容をPC用/SP用で二重に持つのでこれが必ず出る。→ 比較器で w===0 && h===0 の要素は対象外にする(display 値での判定は不可=非表示の親の下でも子のcomputed displayは flex 等のまま)。

縦長セクションの遅延画像

scrollIntoView だけでは画面外の loading="lazy"data:image/svg のまま抜ける
→ セクション上端-200px から下端+400px まで 400px刻みでスイープしてから抽出する(役員5枚とも実URL化できた)。

格納時:他セッションと同居しているときの安全策

  • build.pyentries/ を全部集約するので、他人の未コミット entries が sections-data.json に混入する(今回ANA新千歳空港の7件が同居)。
  • CI(.github/workflows)が build_core.py → build.py → build_lab.py を毎回回すので、
    sections-data.json はコミットしなくてよい。ローカルでは検証のためだけに build.py を回し、git checkout -- sections-data.json で戻してから
    自分の entries とサムネだけを add する=退避/復元の小細工なしで巻き込み事故をゼロにできる(今回この方法で通した)。
  • ヘッダー単体セクションのサムネは上下に大きな余白が入るが、既存の ctsap-header-saki-0820 と同じ体裁なのでそれが正(撮り直し不要)。

コマンドが /section で起動しないとき

~/.claude/commands/section.md は Vault への symlink。アプリ側の登録が古いと Unknown command: /section になるが、
symlink 先の実体(22_AI運用ルール/コマンド集/section/section.md)を直接読めばそのまま同じ手順で実行できる(再起動を待つ必要はない)。


2026-08-20 べすとほうむEP(besthome-ep.com)5件 — WordPress自社テーマ系の落とし穴

#top のような「ページ級のid前置」でヘッダーの配色指定が丸ごと落ちる(トップ10 #5 の実例)

  • 元サイトは header のCSSを common.css に、トップページだけの白抜き指定を top.css#top header …分けていた
    <body class="home …" id="top">)。^header で抽出すると白抜きが全部落ちて、黒文字のヘッダーが写真に埋もれる
  • 対処=抽出前にCSSを #topbody に置換してから拾い、スコープルートに読み替える
    python TOP_ID = TOP.replace('#top ', 'body ') # body.〜 も同様 pred = lambda s: re.match(r'^body\s+header\b', s.strip())
  • 前置は body.home .archive だけでなく #top のような id もある。抽出前に必ず
    grep -oE '<body[^>]*>' dom.html で body の class/id を確認し、その両方で前置ルールを探す。

rem基準は「sec-<id>{font-size:10px} に寄せる」では直らない → rem→px の機械換算が正

  • html{font-size:62.5%} の rem は常に html を見るので、sec-<id> に font-size を置いても 1rem=16px のまま=全部1.6倍に崩れる
  • 抽出CSSに (\d*\.?\d+)rem → ×10 px を一括適用(10px基準なら 1.6rem→16px)。
    これで html{} を1つも書かずに寸法が完全一致した(実測 24項目すべて一致)。既存の良いプレビュー(athily 等)も rem は 0 件=これが慣習。

数値照合で「幅だけ一律16px小さい」=プレビューの <body> の既定マージン

  • getBoundingClientRect() 比較で高さ・font-size は全一致なのに幅だけ常に -16px なら犯人はこれ(8px×2)。
  • body{margin:0} は書けない(貼ると崩れる)ので <body style="margin:0"> のインライン属性で消す。
    属性なのでセクションのマークアップには何も付いてこない=ホスト汚染ゼロ。スコープ漏れ grep も通る。

.visually-hidden を拾い忘れるとロゴの代替テキストが画面に出る

  • ロゴが <svg>+<span class="visually-hidden">社名</span> の構成だと、社名が縦組みで露出する。
  • ヘッダー系を抽出するときは ^header に加えて .visually-hidden / .sr-only / .screen-reader-text を必ず同梱する。

低いセクション(ヘッダーの帯だけ等)はページの地色も合わせないとサムネに白帯が写る

  • セクションが128pxしかないと、撮影範囲にページ下部の白が入り、4:3正規化で黒地の中に白い横線が残る。
  • 黒地セクションは <body style="margin:0;background:#000"> にしてから撮る。
    (上下に大きな余白が入るのは既存のヘッダー単体セクションと同じ体裁=それが正)

同一リポで別セッションが並走しているときの事故と復旧

  • git add <自分のファイル> だけでも、インデックスに残っていた他セッションのリネームが自分のコミットに紛れ込むことがあった
    jcat-contact-ogushi→saki のリネームが同梱された)。commit 直後に git show --stat HEAD で中身を必ず確認する
  • rebase が「untracked working tree files would be overwritten」で止まったら、消す前に origin と md5 照合する。
    今回は11件すべて origin と完全同一(別マシンから既にpush済みの同じ成果物)だったので、退避→rebase→復元で無傷で通せた。
    bash a=$(md5 -q "$f"); b=$(git show origin/main:"$f" | md5 -q); [ "$a" = "$b" ] && echo same
  • zsh は for f in $VAR で改行分割しない(1回しか回らない)。ファイル一覧のループは
    while IFS= read -r f; do … done < list.txt で回すこと(同一性チェックが空振りして「差分0」に見える事故)。

動画素材の中身確認(ffmpegが無いMac)

  • mdfind -name ffmpeg既存の node_modules 内 ffmpeg-static が見つかることが多い(今回 ~/dev/_section_work_0817/node_modules/ffmpeg-static/ffmpeg)。
    swiftc でAVFoundationのフレーム抽出ツールを作るのはビルドに2分以上かかって詰まるので先に mdfind する。
  • 抽出は -vf "fps=2,scale=1400:-1"2000px以下にしてからReadする=画像サイズ制限に引っかからない)。

補足(既存知見の確認)

  • sections-data.json はコミットしなくてよい(CIが再生成)」は今回コミットしてしまったが実害なし(CIが上書き)。次回は git checkout -- sections-data.json してから add する。

2026-08-20 ANA新千歳空港トップ 7セクション(さき・実コード抽出をCSSOM機械抽出に切替)

抽出を目視のコピペではなく ブラウザのCSSOMから機械抽出する方式に変えたら、
getBoundingClientRect() の全要素照合が 1440px / 390px とも差分0 で7件そろった(初回で一発)。
以下は同じ方式を再利用するときに必ず踏む地雷。全部この日に実害を出して直したもの。

CSSOM抽出で必ずやること(順番に効く)

  1. 宣言は rule.style.cssText で取る。プロパティ列挙(style[i])は禁止。
    border:1px solid var(--x) のように var() を含むショートハンドは、列挙すると全ロングハンドが空文字になって消える。
    実害=カードの丸い枠線と -webkit-mask の飛行機アイコンが丸ごと消えた。
  2. シートの media 属性を尊重する。 <link media="print">(print.css)のルールは sheet.cssRules からは
    メディア無しで出てくるので、そのまま入れると .c-plane-icon{display:none} のような印刷用指定が画面に効く
    sheet.media.mediaText@media で包む。
  3. .is-* / .js-* の状態クラスは、素の一致で落ちる。
    スナップショットに .is-open は無いので .js-dropdown.is-open .l-header__submenu{...} が捨てられ、
    メガメニューが永久に開かなくなる。一致しなかったら状態クラスを外して再判定し、採用するセレクタは元のままにする。
  4. 自前の要素リセットを足さない。 参考サイトの reset(img{max-width:100%;height:auto;vertical-align:top} 等)は
    「素タグのルール」として自動で拾えている。そこに :where(img){display:block} を足すと
    インラインの行ボックスが消えてロゴのaタグが35px→31pxになる。足すのは box-sizing の保険くらいでよい。
  5. html/body の基本指定は「算出値」でなく「指定値」で移す。
    line-height:2.2(無単位)を算出値 35.2px で焼くと、子の15px文字が2.2倍で再計算されず高さがズレる。
  6. メディアクエリ内の html/body 指定も拾う。 SPで html{font-size:0.8125rem} に落とすサイトだと、
    拾わないと 390px幅で全文字が16px基準のまま(実測:フッターの各項目が13px→16px、高さ169→197px)。
  7. <body> の既定マージン8px。プレビューHTMLの器が持っているだけで 全要素が横16px細くなる(1440→1424)。
    ただし body{margin:0}<style>に書くと貼った先を壊すので、<body style="margin:0;padding:0">属性で打つ。

クロスオリジンで死ぬもの2つ(Labは別ドメインなのでどちらも踏む)

  • mask-image は同一オリジン or CORS必須。参考元のSVGを絶対URLで参照してもマスクだけ効かない(円は出るが中の飛行機が出ない)。
    SVGだけ data URI に埋める(画像は原URLのままというルールと矛盾しない・アイコンは数KB)。
  • @font-face も CORS必須。ctsapは Access-Control-Allow-Origin 無しで配信していてフォントが1つも読めない
    → 同じ書体を Google Fonts から読むInter VariableInter のように名前も置換)。base64で埋めると1書体1MBで現実的でない。

動きは「実測」でなく「元の計算式」を読む

js-infinite-slider の速度を transform の差分から測ったら 測定ごとに 20〜39px/s とブレて当てにならなかった
(画面外だと止まる・周回のwrapを跨ぐ)。app.js を読んだら式が書いてあった:

px/秒 = (先頭アイテム幅 + gap) × 1000 ÷ data-infinite-speed / 方向は data-infinite-direction(reverse=右へ)
クローン数 = ceil((wrapper幅 + track幅) / track幅)、ループ距離 = track幅 + gap

これで ESG=32px/s、採用=21.2px/s と一意に決まる。バンドルJSは丸ごと積まずに、式とパラメータだけ読んでスコープ内に再実装するのが正解
(実際 app.js をCDNから読み込めば動くが、253KBのサイト全体JSを貼るとホストHPを乗っ取る)。

  • data-slider-inited="true" は消さない。 CSSに .js-infinite-slider[data-slider-inited=true]{opacity:1} があり、
    消すと スライダーが opacity:0 のまま真っ白になる(ESGの写真帯が丸ごと消えた)。

サムネ撮影ツールの前処理が壊すもの(素撮りに切り替える判断基準)

shot_common.mjs の前処理は opacity<0.15 を 1 に戻し、さらに filter を全部 none にする。つまり:

症状 原因 対処
意図的に薄い装飾が濃く出る opacity:0.06 の背景地図が1.0にされる(採用情報の北海道が真っ青に) 素撮り(前処理なし)で3枚撮り直す。filter:opacity() に逃がしてもfilterごと消されるので無駄
ヘッダー単体が白紙同然 96pxのバーが4:3に収まるだけ。しかも前処理が閉じたメガメニューを勝手に開いて画角外に出す 1つだけ hover で開いた状態を素撮り(mouseenter を dispatch)。v2.5の指摘どおり

素撮りで用意するのは3枚(この3枚セットを必ず作る・カードだけ差し替えると詳細ページが古いまま):
<id>.png(1440幅・2倍・全高 → normalize_thumbs.py で4:3化)/ <id>__detail.png(1440×1000・2倍)/ <id>__sp.png(390×760・2倍・モバイルUA)。

別セッションが同じリポで作業中のときのpush(今回実際に衝突しかけた)

git status他人の staged ファイル + 未pushコミット2件 + submodule変更 で埋まっていた。
git pull --rebase は相手の作業ツリーを巻き込むのでやらない。作業ツリーに一切触らず自分のファイルだけ push する:

git fetch origin
export GIT_INDEX_FILE=/tmp/mypush_idx && rm -f "$GIT_INDEX_FILE"
git read-tree origin/main
for f in <自分のファイル>; do h=$(git hash-object -w "$f"); git update-index --add --cacheinfo 100644,$h,"$f"; done
TREE=$(git write-tree); C=$(git commit-tree "$TREE" -p origin/main -m "...")
git push origin "$C:main"
  • zsh は for id in $IDS で単語分割しない(bash と違う)。変数展開に頼ると1語扱いになり、
    今回は staged 0件の空コミットを1つ push してしまった(実害なし)。リストはループにベタ書きする
  • CIが core/build_core.pybuild.pybuild_lab.py を回すので、コミットするのは
    entries/*.jsonimg/sections/*code-previews/*.html だけでよい
    sections-data.json は触らない=相手の生成物を汚さない)。

このMac(咲輝機)の環境メモ

  • node は ChatGPT.app 同梱のみexport PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"
  • swift/swiftc は壊れているredefinition of module 'SwiftBridging')。AVFoundationでの動画フレーム抽出は諦める。
    ffmpeg も無いので、動画は Playwright に <video> で読ませて currentTime を送りながら撮るのが確実(HTTPで配信すれば数秒で済む)。
  • python3 -m http.server は cwd が Google Drive 配下だと PermissionErroros.getcwd() で落ちる)。
    cd してから起動するか、そもそも検証は file:// で足りる(絶対URLで自己完結しているため)。

2026-08-20 J-CAT(STUDIO製サイト)5件 — 抽出を「機械で完全一致」させる型が確立した

成果: 5件すべて 数値照合MATCH(全要素3px以内)+レスポンシブ✅ で一発格納。
partner 164要素 / footer 96 / philosophy 97 / news 105 / services 418。

汎用抽出スクリプト(これが本体・今後はこれを使う)

セレクタ手作業でCSSを拾うのをやめ、ブラウザに「この要素にマッチするルール」を判定させる
document.styleSheets を再帰で歩き、els.some(e => e.matches(セレクタ)) で残す/捨てるを決める。
これで STUDIO のような .sd[data-s-<uuid>] だらけのサイトでも取りこぼしゼロになる。

この方式でハマる4点(全部実害が出た・毎回チェックする)
| # | 症状 | 真因 | 対処 |
|---|---|---|---|
| A | borderだけ消える・バッジが1px小さい | style[i] の列挙は var() を含むショートハンドを落とすborder-bottom: 1px solid var(--x) が消える) | 宣言は r.style.cssText をそのまま使う(個別プロパティを組み立てない) |
| B | 全部 display:block で縦積み・レイアウト総崩れ | 基盤CSSが :where(.render-canvas) .sd{…} のように祖先クラス前提で書かれ、単独プレビューに祖先が無い | 祖先チェーンのclassを全部集めてスコープdivの内側にラッパーを1枚作る(.sec-id > div.render-canvas...) |
| C | タブレットだけ横スクロール・SP指定が効かない | @media グループをメディア無しより先に出力していて、PC値が後勝ちしていた | メディア無し → @media を max-width 降順で出力する(順序が全て) |
| D | 画像が灰色/サムネが空白 | loading="lazy" のまま+一気に最下部へ飛ぶ抽出で遅延読込が発火しない | loading 属性を除去+600pxずつ段階スクロールしてから対象を scrollIntoView して待つ |

CMSがJSで差し替える画像は抽出できない(J-CAT News)

STUDIO CMS の記事サムネは 自動化ブラウザ(headless / headful / --dump-dom すべて)でプレースホルダのまま(180バイトのwebp)。
実ブラウザでは出るので、プレビューが灰色ボックスになったらこれを疑う
各記事ページの og:image から実画像URLを取ってカード順に差し込むのが最短・確実(=参考元の画像そのまま を満たす)。

curl -sL "<記事URL>" | grep -oE '<meta[^>]*og:image[^>]*>' | grep -oE 'https://[^"]+' | head -1

数値照合はオフセット2を固定する

再現HTMLは .sec-<id> の中に ラッパーdiv + ルート要素 の2つが余分に入る。
元は root.querySelectorAll('*')、再現は同じ配列の index+2 が対応する。
ここを自動検出しようとすると誤検出して「全要素ズレている」ように見え、無いバグを追いかける(今回30分溶かした)。

他セッションと同じリポで同時作業していた時のpush

別セッションが同じ ~/dev/terasu-lab で作業していて、git pull --rebase
「untracked working tree files would be overwritten」で止まる(相手がpush済み・ローカルに未追跡で同名ファイルが残っている状態)。
- 消さないgit hash-objectgit rev-parse origin/main:<path> を突き合わせ、内容一致を確認してから退避(mv)する。
- 一致していれば rebase 後に git が復元するので実害ゼロ(今回28件・相手の作業は無傷)。
- submodule core の差分は --autostash で逃がす。

このMacの動画確認

ffmpeg / ffprobe / PyObjC いずれも無い。qlmanage -t で先頭フレーム、複数フレームは
Chrome headless に <video> を読ませ currentTime を送って canvas に描く--allow-file-access-from-files で file:// のままいける)。


2026-08-20 HERP Trust(STUDIO製)で踏んだ3つの地雷+検品の穴

大串FB「⑤開けない」=FAQのプレビューが真っ白だった。数値照合(getBoundingClientRect)は
全項目一致していたのに真っ白。原因は下記①。数値だけで合格にしてはいけない

① スコープ用ラッパーの背景が z-index:-1 のセクションを隠す(真っ白の真犯人)

STUDIO は各セクション(.sd)に position:relative を敷き、一部セクションに z-index:-1 を付ける。
元サイトでは白背景が body 側なので問題なし。ところが抽出時に body{background:#fff}
.sec-<id>{...} へ寄せると、ラッパーの背景がペイント順で負のz-index子より後に描かれ、中身を丸ごと覆う
- 症状: 計算値(色・opacity・visibility・座標)は全部正常なのに何も描画されない/elementFromPointも拾わない
- 対処: ラッパーに isolation:isolate(独立スタッキング文脈にすると、自身の背景→負z-index子の順で描かれる)
- 検出: CSSルールを前から順に足しながらスクショのバイト数で二分探索すると一発で犯人ルールが出る

.sec-<id> を素で前置すると詳細度が+1され、外部CSSに不当に勝つ(アイコン豆腐)

.fa{font-family:FontAwesome}(外部CDN・詳細度0-1-0)に対し、こちらの .sec-x .hpt-sd{font-family:…}
0-2-0 になって勝ってしまい、Facebookアイコンが□になった。
- 対処: スコープは :where(.sec-<id>) で付ける:where() は詳細度0)。元サイトの優先順位がそのまま残る。
- 併せて reset も :where() ベースにし、reset → 元サイトCSS の順に並べて元のカスケードを再現する。

③ 入れ子 <li>outerHTML を再パースすると平坦化される

<li><div><li>… という DOM(JSで組まれたSTUDIOサイトに実在)を outerHTML で書き出して読み直すと、
HTMLパーサの規則で外側の li が閉じられ、構造が壊れる(3カラムが崩れて縦積みになった)。
- 対処: 抽出時(ブラウザ内)に li lidiv へ置換してから直列化する。li を対象にしたCSSが無いことを
grepdocument.styleSheets を走査)で確認してから実施すれば見た目は完全に不変。

検品ゲートに「描画量」を追加する(数値照合だけでは穴がある)

getBoundingClientRect の一致はレイアウトの一致であって描画の一致ではない
各件つき最低1枚はフルページのスクショを撮り、png bytes が極端に小さくないかを見る(真っ白は数KB)。
今回 issue/price だけ目視して faq を数値のみで通した結果、真っ白のまま大串に送ってしまった

STUDIO製サイトの抽出メモ

  • CSSは [data-s-<uuid>] 属性キー+:where(.render-canvas) .sd 系。.render-canvas は祖先なので
    プレビューは <div class="sec-x"><div class="hpt-canvas">…二重ラッパーにしないとルールが当たらない。
  • .appearCSS定義が無いことがある(=アニメ発火ゲートではない。外す必要なし)。
  • 画像は初期DOMだと data:image/svg+xml のプレースホルダ。全ページをスクロールしてから currentSrc を拾う
  • @font-face はクロスオリジンCSS(fonts.studio.design等)からは読めないので <link> をそのまま同梱
    ただし Font Awesome 6 の @font-face は同一オリジンの entry.css 側にあるので、
    こちらは抽出して出力しないとブランドアイコンが豆腐になる(FA4のCSSリンクだけでは足りない)。

このMacの動画確認(2026-08-20 追記・上の記述を訂正)

ffmpeg/ffprobe/PyObjC 無し。Swift も CommandLineTools のSDK不一致でビルド不可
JXA の AVFoundation ブリッジも不可Chrome(headless・channel:chrome)は macOS の .mov をデコードできず
loadedmetadata が来ない(file:// でも http:// でも同じ)。
→ 現状で確実なのは qlmanage -t -s 1400 -o <dir> <file.mov> の先頭フレーム1枚だけ
複数フレームが要るときは大串に「動画のどこか」を1行で聞くほうが速い。

動画から複数フレームを抜く方法が確立(2026-08-20・上の「qlmanageの1枚だけ」を訂正)

上記は「Chromeが.movをデコードできない」で止まっていたが、3点セットで完全に解決した(Re Loop contact で実証)。
1. .mov.mp4 に変換(macOS標準・追加インストール不要)
avconvert --source in.mov --output out.mp4 --preset PresetHighestQuality
--quiet は廃止済みオプション(付けるとエラー)。元が H.264 でも コンテナが .mov だとChromeが読まないのが真因。
2. localhost で配信し、プレイヤーHTMLも同じオリジンに置くpython3 -m http.server
setContent() の about:blank から http://localhost の動画を読むと canvas が tainted になり toDataURL が SecurityError
必ず page.goto('http://localhost:PORT/player.html') にして同一オリジンにする。
3. playwright は chromium.launch({channel:'chrome'})(バンドルChromiumはH.264非対応)。
あとは v.currentTime を進めて canvas.drawImagetoDataURL で任意枚数抜ける。
動画1本でも「どこの箇所か」を大串に聞かずに特定できる=FB②(着手前の確認禁止)を守れる。

node のパスが変わっている(2026-08-20)

memory codex-bundled-node の旧パス(.../app.asar.unpacked/node_modules/cua_node/bin)はもう存在しない
現行= /Applications/ChatGPT.app/Contents/Resources/cua_node/bin(node v24)。export PATH=...:$PATH で使う。

rem基準が「vw」のサイトは rem→calc(N*var(--u)) に機械変換する(2026-08-20・重要)

崩れる原因トップ10 #2 の発展形。Re Loop は html{font-size:0.6613756614vw}(=1512pxで10px)/SPは 2.5445292621vw(=393pxで10px)。
sec-<id>{font-size:10px} に寄せる従来の手当てでは直らない——rem は常に html を見るので、スコープ化した瞬間に全サイズが16px基準に化ける。
CSS内の全 Nremcalc(N * var(--u)) に一括置換し、sec-<id>{--u:0.6613756614vw} / media内で --u:2.5445292621vw を切り替える。
calc(100vh - 30rem - 6rem) のような入れ子も calc(100vh - calc(30 * var(--u)) - ...) で有効。vw可変の再現までそのまま効く

数値照合で必ず出る「あと2つ」(2026-08-20・毎回チェックすると一発で合う)

Re Loop で getBoundingClientRect() 照合が 18項目中18件NG → 下の2つを直しただけで 0ズレ(完全一致)になった。
1. <body> の既定 margin 8px が全体を右に押す(x が 150→158、幅が 848→837)。
→ 単独プレビューの <body style="margin:0"> で消す(CSSに body{} を書くとスコープ漏れになるのでインライン属性で)。
2. リセットとタイポグラフィの記述順。元CSSは input{line-height:1.15}(reset) → h1..input,textarea{line-height:1.8}(base) の順で
後者が勝つ。抽出時に順序を入れ替えると input の高さが 58px→50px に縮む(フォーム全体で130px以上ズレる)。
元ファイルの読み込み順・記述順をそのまま保つ
3. クラスの無い divfont-size は元サイトだと html から 10px を継承している。
→ ルートに sec-<id>{font-size:1rem}(=calc(1*var(--u)))を置くと継承チェーンまで一致する。

白基調のフォーム系はサムネ検品が誤検知する(2026-08-20・Re Loop contact で実測)

shoot_section.mjs⛔ 中身が写らない(色41種/輪郭2.0%) で落ちた。画像を見ると中身はしっかり写っている
真因は判定式の3本目 isPoorShot = … || (flatRatio >= 0.92 && edgeRatio < 0.03)
実測 flatRatio 0.95 / edge 0.026 =「白地に淡いグレー(#f2f2f2)の入力欄」が量子化で白に吸収され、画面の95%が一色と数えられていた。
- 推測で直さないcore/tools/shot_common.mjsmeasureShot をリポ内から呼べば実測値が出るtools/ に一時 .mjs を置く//tmp からだと playwright が解決できない)。
- 対処は①上部の余白を詰める→②実画像を目視→③--force。上部を詰めるだけでは 2.0%→2.5% で閾値を越えられない
(フォーム系は本質的に淡いため)。目視で中身が写っていることを確認したうえで --force が正解。
--force でも 3枚(カード/__detail/__sp)と 4:3 正規化はすべて通常どおり走る。

セクション上部の「ヘッダーが居た余白」は単独プレビューでは詰める(2026-08-20)

下層ページは main{padding-top:30rem} のようにヘッダー分を空けている。セクションだけ抜くと上に約290pxの白が残る
(サムネの過半が白 = 上の検品落ちの主因にもなる)。
→ トップ10 #8「単独プレビューで前提が無い指定は打ち消す」の正当な適用。上余白と sticky の top を同じ値で縮める
(例 30rem→8rem)。フォーム本体の寸法は1mmも変わらないので、getBoundingClientRect() 照合(w/h/x)は0ズレのまま


2026-08-20 harimanics 沿革タイムライン(さき)で得た知見

一括リネームで「画像URL」まで壊れる(404)— 新パターン

class="history_2023_02" のように クラス名と画像ファイル名が同じ文字列だと、
リネームの一括置換が images/about/history_2023_02.jpgURL側まで書き換えて404になる。
単独プレビューでは「画像だけ出ない」ので気づきにくい(レイアウトは崩れない)。
リネーム直後に必ず画像404を機械チェックする(登録前ゲートに追加推奨):

await p.evaluate(()=>[...document.images].filter(i=>!i.naturalWidth).map(i=>i.src))

rem基準は sec-<id>{font-size:10px} では再現できない(本体ルールの補足)

rem常にroot(html)基準なので、sec-<id> に font-size を置いても効かない。
html への指定は禁止(貼ると崩れる)なので、該当箇所の rem を px に手変換するのが唯一の正解
html{62.5%}=10px基準なら 2.4rem24px)。今回は6箇所だけだったので全部px化して完全一致した。
着手時に元サイトの html の font-size を必ず実測62.5%=10px / 未指定=16px)。

ffmpeg が無いMacで動画のコマ送りを取る方法(実証)

  • swift スクリプト(AVFoundation)はコンパイルが重く2分でも終わらない=実用外
  • Playwright の同梱Chromiumは HEVC(画面収録) をデコードできない(readyStateが上がらない)
  • video 要素を直接 screenshot() すると真っ黒(ハードウェアデコード面が取れない)
  • 正解=chromium.launch({channel:'chrome'}) で実Chromeを使い、canvas.drawImage(video)toDataURL()
    --allow-file-access-from-files と、file:// ディレクトリに goto してから setContent で相対srcを解決する
  • 大まかな1枚だけでよければ qlmanage -t -s 1200 -o . <mov> が最速(箇所特定はこれで足りることが多い)

作業ディレクトリ名は「サイト名」だけだと衝突する

/tmp/section_<site>/ は本体ルール通りに分けたが、同じサイトを別セッションが同時に処理していて衝突した
(先に別セッションの動画フレームが入っていた)。→ /tmp/section_<site>_<担当>/ まで含めるのが安全。
同様に、同じリポで別セッションが同サイトの他セクションを作業していることがあるので
git add は自分のidのファイルだけ-A 禁止)を厳守する。git pull --rebase は他セッションの
未staged変更で失敗するので、失敗しても自分のcommitのpush成否を git status -sb で確認する

うまくいった型

スクロール連動の横スクロール年表は GSAPではなく素のJSwindow.scrollY から translateX を算出)だった。
元サイトのインラインJSから setupTimeline を丸ごと移植し、セレクタを sec-<id> 配下に限定するだけで完全再現できた。
単独プレビューでは固定ヘッダー前提の top: calc(100px + 48px)top:48px に、headerH も合わせて調整すると自然に収まる。
数値照合の結果=スクロール器 8140x500・全10itemの寸法とx座標が元サイトと完全一致


2026-08-20 株式会社エースリー(a-thily.co.jp)トップ6件 — 崩れの真因4つ

rem基準は「html の指定」だけ見ると間違える(body,html{font-size:62.5%} の二重適用)

エースリーは body,html{font-size:62.5%}両方に書かれており、body が html(10px) の 62.5% = 実効 6.25px
html だけ見て「10px基準」と判断すると全フォントが1.6倍になる(実測 h2 が 126px→201.6px)。
基準は grep で決めず、元サイトで getComputedStyle(該当セクション).fontSize を実測する

rem は「ホストの html」を見る=スコープ化だけでは閉じない

sec-<id>{font-size:6.25px} を敷いても rem はルート要素(html)基準なので効かない(ホストが16pxなら1.6倍のまま)。
生成CSSの Nrem(N×10)px に機械変換するのが正解。スコープ内で完全に閉じ、貼り先のhtml指定に一切依存しない。
re.sub(r'(-?\d*\.?\d+)rem', lambda m: f'{float(m.group(1))*10:g}px', css)

素タグ a{color:…} の抽出漏れ=リンクが「既定の青」になる

抽出条件を「クラスを含むセレクタ」にすると a{color:#212121} が落ち、rgb(0,0,238) の青が出る(大串FB「テキストの色が入ってない」)。
→ ベースCSSに sec-<id> a{color:<実測値>} を必ず補完する。トップ10 #9(素タグのルート指定)の亜種。

単独プレビューは「スクロール不能」でアニメが永久に発火しない

元サイトの発火条件 rect.top < 画面高/1.6 をそのまま移植すると、セクション高さ < ビューポートのページでは
一生 on が付かず中身が消える(member が実例)。逆に最初から on を付けると動きが無いと指摘される(大串FB)。
→ 発火判定に ①スクロール不能なら全on ②最下部到達なら残り全on の2条件を足す。これで動きも残り消えもしない。

サムネは「アニメ発火済み」「メニュー開いた状態」で撮る

  • スクロール発火型は fullPage 撮影でビューポート外が未発火=本文が消えたサムネになる。
    → 撮影直前だけ els.forEach(on) を注入したコピーで撮り、撮影後にプレビューを元に戻す
  • ヘッダー単体は閉じた状態だと採用ゲートに落ちる(中身が写らない)。
    .child{opacity:1;visibility:visible;transform:none} を撮影用に注入しメガメニューを開いて撮る
    → 器の min-height は「ヘッダー高+メニュー高」に合わせる(560pxだと白余白が目立つ→380pxに)。

環境の落とし穴(このPC)

  • --dump-dom の Chrome が遅延して dom.html を上書きし、セクションが消えたDOMになることがある。
    WordPress系は curl で十分(今回は curl で全セクション取得できた)。1行HTMLへの grep -oE
    catastrophic backtracking で固まるのでPythonで処理する
  • ローカルサーバは他セッションが 8090/8091/8097-8099/8123 等を使用中。空きポートを lsof で確認してから起動する。

固定サイドナビぶんの左余白は、単体セクションでは左右を揃える(2026-08-20 大串FB・恒久)

指摘: ReLoop(re-loop.jp)のフッターで「②左右の余白が違う」。

真因: 元サイトは左端に幅9u(87px)の固定サイドナビがあり、フッターは padding: 3u 3u 3u 12u
(=左だけ 9u ぶん多い)でナビを避けている。数値照合は元サイトと1pxも違わず一致していた=再現は正しい。
単体で切り出すとナビが無いので、左だけ広く見える(左87px / 右29px)。

直し方: 元サイトのPC表示ではなく「抜き出した後の収まり」を直す(Step3.5と同じ思想)。
padding: 3u 3u 3u 12upadding: 3u(左右対称に)。中身の設計は変えない

横展開のチェック(着手前に毎回): 固定サイドナビ/固定ヘッダーを持つサイトから抜くときは、
padding-leftpadding-right の差がナビ幅とほぼ一致していないか見る。一致していたらナビ避けの余白=打ち消す。
ただしナビ幅を超える左右差は元サイトの意図した非対称レイアウト(例 ReLoopのFV/Projectsは左15u・右3u〜6u)なので、
勝手に揃えず大串に確認する

教訓: 数値照合PASSでも「単体で見たときの収まり」は別問題。登録前に必ず単体プレビューを一度目で見る


2026-08-20 NIPPON WONDER FACTORY(CTAボタン単体)で得た4つの学び

① 「アイコン(SVG)だけ消える」= リセットの svg{display:block} 拾い漏れ

wonder-factory.jp のボタンを抽出したところ、矢印アイコンだけが表示されなかった
真因は元サイトのリセットにある audio,canvas,embed,iframe,img,object,svg,video { display:block; vertical-align:middle; max-width:100% }
これを落とすと svg が inline になり、ベースライン分(実測15px)下にずれる → 親 .__inner{height:11px;overflow:hidden} に切られて完全に消える
- 症状: 数値照合(ボタン寸法・色・transform)は全部一致するのにアイコンだけ無い
- 検出: getComputedStyle(svg).display を元サイトと比較(block vs inline)/svg の rect.y が親と一致するか
- 対処: sec-<id> img, sec-<id> svg, sec-<id> video, sec-<id> canvas { display:block; vertical-align:middle; max-width:100%; height:auto } をスコープ版で補完
- 崩れる原因トップ10 #9(素タグのルート指定が抽出条件から漏れる)の亜種。アイコン・画像が消えたらまず素タグの img/svg リセットを疑う

② ボタン等「小さい部品1個」のセクションはサムネ採用ゲートに必ず落ちる

shoot_section.mjs は撮影幅 W=1440 固定。300×70 のボタン単体だと ⛔ 中身が写らない(輪郭1.7%) で保存されない。
- 対処(ヘッダー/メガメニューを「開いた状態で撮る」のと同じ考え方):
① 画角を寄せる(幅780でPCレイアウトを維持。767px以下にすると SP のメディアクエリが発火して別デザインになるので下げすぎない
② 動きが本体のセクションは"途中の状態"で固定して撮る(今回は ::beforetranslateY(50%) に固定=黒とピンクの波が両方写る)
- 3枚セットで作る<id>.png 2880×2160 / <id>__detail.png 2880×2000 / <id>__sp.png 780×1520)。カード用だけ差し替えると詳細ページが古いまま(v2.6の教訓)
- 非整数倍率になる時は大きめに撮って sips -z <h> <w> で正規化する

③ 動画素材のフレーム抽出(このMacは ffmpeg 無し)

ffmpeg が入っていないので、動画の中身確認はこの手順が最短:
1. qlmanage -t -s 1600 -o <dir> <file.mov> … 1枚だけ即取れる(中身の当たりを付ける用)
2. 複数コマ欲しい時は /usr/bin/avconvert --source rec.mov --output rec.mp4 --preset PresetHighestQuality で H.264 に変換
chromium は macOS 画面収録の HEVC を再生できない(video.error.code=4)ので変換が必須
3. python3 -m http.server <空きポート> で配信し、playwright で <video>currentTime シークしてコマ撮り
file:// 直読みは再生できないポート8080・8099は他セッションが使っていることが多いので空きポートを使う
- swift + AVFoundation でのフレーム抽出は swift実行が2分でタイムアウト・swiftcもこの環境ではビルドに失敗した(使わない)

④ CIが cancelled になっても慌てて再pushしない

同時作業日は他セッションのpushが連続し、concurrency: deploy-terasu-lab の待ち行列で自分のrunが cancelled 表示になる
だが後続runは main の HEAD(=自分のコミットを含む) をビルドするので、内容はそのまま反映される
- 確認は run の成否ではなく本番URLのHTTP 200と /sections に id が出ているかで見る(gh run watch の X 表示だけで失敗と判断しない)
- build.py をローカルで走らせると他セッションの code-previews/*.html まで書き換わる(CIが同じ処理をするので放置でよい)。絶対に git add -A しない

⑤ 条件付き html[lang=xx] ルールがスコープルートに化けて全体を壊す(2026-08-20 BRUTUS)

抽出時に「先頭の body クラス文脈(.home .x)を除去する」処理を素朴に書くと、
html[lang=ko],html[lang=ko] *{font-family:system-ui,sans-serif!important} のようなそのページに当てはまらない条件付きルールまで
.sec-<id>,.sec-<id> *{...!important} に化け、セクション全体のフォントを乗っ取る(実際に全要素が system-ui になり、幅が 499→685px にズレた)。
- 対処=先頭が html/body でも、[属性]#id・body文脈以外のクラスが付いていたら「不採用」にする(除去して残さない)
- 症状の見分け方: computed の font-family が指定と違う → 生成物を grep -o 'font-family:[^;}]*' して !important の混入を探す

⑥ 遅延描画サイトは Playwright 内蔵Chromiumだと中身が出ない → channel:'chrome'

brutus.jp のフッターは内蔵Chromiumだと .c-page-foot が 596px の空箱のまま(子要素0件)で、数値照合が全部❌になる。
実Chrome(chromium.launch({channel:'chrome'}))に変えた瞬間に全項目一致した。
- 元サイトのDOM取得を実Chrome headlessでやったなら、照合も実Chromeで揃える(ブラウザを混ぜない)
- あわせて「最下部まで段階スクロール→待機」を入れる(遅延読み込みの発火)

⑦ 数値照合は「セクション限定セレクタ」で測る

.c-base-headline__heading__en のような共通クラスは元ページ内の他セクションにも大量にあるため、
そのまま測ると元だけ4件・再現1件で必ず❌になる。.c-home-regulars .c-base-headline__heading__en のようにセクションのルートを前置して測る。
- 残差が 1px 以内の時は Range.getBoundingClientRect()テキストの実描画幅まで見る。
padding/letter-spacing/font-feature(palt)/weight が全同値で 0.6px 差ならサブピクセル丸め=元CSSを書き換えてまで合わせない(それは「寄せる」になる)

セクションHTMLだけ切り出すと <symbol> がページの別の場所にあるためロゴが消える
DOM から該当 <symbol> を抜き、body先頭に <svg style="position:absolute;width:0;height:0;overflow:hidden">…</svg> として同梱する。
- 汎用化: 断片内の href="#xxx" を集める → dom.html から同id の symbol をタグバランスで切り出す

⑨ 動画からのフレーム抽出(このMacに ffmpeg が無い場合の正解)

ffmpeg 無し・swiftスクリプトは初回コンパイルで2分超えて実用外。Playwright + <video> + currentTime シークが最速。
1. 動画を python3 -m http.server <port> --directory <dir> で配信(--directory 必須。サブシェルの cd は効かない)
2. <video id="v" src="v.mov" muted preload="auto"> のページを開き、v.currentTime=tlocator('#v').screenshot() をループ
- node は PATH に無いexport PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"
- スクリプトは ~/dev/terasu-lab/ 内に置く(playwright は そこの node_modules にしか無い。scratchpad から実行すると ERR_MODULE_NOT_FOUND)

⑩ 同時作業日は git commit もパス指定でやる

git add はしていないのに git status に他セッションのファイルが staged で出る(インデックスは1つ=共有)。
そのまま git commit -m すると他セッションの作業を自分のコミットに巻き込む
- 対処=git commit -m "..." -- <自分のパスだけ列挙>(pathspec commit)。インデックスの他のファイルは触らない
- なお相手セッションが先にcommitして自分の分を巻き込むことも起きる(自分のcommitは "no changes added" になる)。
その場合内容は正しく入っているので履歴を直そうとrebaseしない(進行中の他セッションを壊す)。git log -- <自分のファイル> で入ったコミットを確認して先へ進む

⑪ ミニマルなセクションはサムネ検品に落ちる(崩れではない)

白背景・黒細線・要素が小さいセクション(ページネーション、シンプルなボタン帯など)は
shoot_section.mjs の採用ゲートに 中身が写らない(色3種/輪郭1.4%) で落ちる。
撮影は W=1440固定・中身が低いと contentH が下限200pxに丸められるため、
1440×200 の中に幅200px程度の要素がポツンと写る=輪郭比率が低いのは仕様どおりの結果
- 判定手順=まず数値照合(getBoundingClientRect())が元サイトと一致しているか見る。一致していれば崩れではない。
次に同じ kinds の既存セクションのサムネを実際に開いて見比べる(例 ctsap-pagination-saki-0820.png は同じ余白量で採用済み)。
同水準なら --force で保存してよい(shoot_section.mjs v2.6 以降は末尾で normalize_thumbs が自動実行され4:3・2880×2160になる)。
- --force を使ったら報告に理由を明記する(「検品に落ちたが中身は正常・既存同種と同水準」)。黙って force しない。
- 2026-08-20 実例: brutus-pagination-saki-0820(BRUTUS「01 / 45」+円形矢印)。3幅すべて寸法完全一致 → force保存で採用。

⑫ 数値照合で font-size だけ全体的にズレる=rem基準をJSが可変にしている(2026-08-21 marumi)

getBoundingClientRect() 照合で 幅は合うのに font-size と余白だけ一律 1.1倍ズレるときは、
CSS の :root{--fz:10px}JSが上書きして vw連動にしている(CSSだけ読んでも絶対に分からない)。
- 実例(shizai.marumi.com / common/js/basic.js):
js if (display_mode == 'n') setProperty('--fz', (vw/375)*10 + 'px'); // SP else { vw = clamp(vw,1300,1920); setProperty('--fz', (vw/1300)*10+'px'); } // PC
→ スコープ版は --sec-rem: clamp(10px, calc(100vw / 130), 14.7692px) / SPは calc(100vw / 37.5) で完全一致(8/8)。
- 手順=照合でfsが合わない → grep -n "fz\|setProperty\|documentElement.style" <site>/common/js/*.js を先に見る。
「10px固定」と決め打ちして進めない(トップ10の#2の実例。CSSの記述だけを信じるのが罠)。
- grep -oE '.{100}fz.{140}' のような両側ワイルドカードのgrepは巨大JSでハングする(120秒タイムアウトの実害)。
python3 -c "import re; [print(s[m.start()-160:m.start()+180]) for m in re.finditer('fz', s)]" で見る。

⑬ 動画素材からのセクション特定:ffmpegが無いMacでのフレーム抽出(2026-08-21)

このPCには ffmpeg も pyobjc も無く、swift はCLTのmodulemap衝突で動かないredefinition of module 'SwiftBridging')。
- 実際に動いた方法=Playwright(Chrome) で動画を再生しながら canvas に描いて PNG 化
chromium.launch({channel:'chrome'})=H.264/.mov が再生できる。playwright同梱chromiumは不可の可能性)。
1. 動画を作業ディレクトリへASCII名でコピー(日本語ファイル名のまま扱わない)
2. python3 -m http.server で配信し <video> に読ませる(file:// はcanvasが汚染される)
3. currentTime でのシークは効かない(python の http.server が Range 非対応 → 全フレーム同一画像になる。md5で気づける)
v.play() して while (v.currentTime < t) await sleep(30)再生しながら間引き取得する
4. sips -Z 900 に落としてから読む(画像1枚=数千文字。コンテキスト肥大の主犯)
- 同じ画像が出てきたら md5 で必ず疑う(見た目が同じ=抽出できていない、を静かに見逃す)


2026-08-21 rem基準は sec-<id>{font-size:10px} では 効かない(崩れ原因#2の真因・恒久)

サイト: OND°(https://www.ond-crc.jp/ )/ニュース一覧グリッド・お問い合わせ帯

何が起きたか: コマンド本体(設計思想3・崩れ原因#2)の指示どおり html{font-size:10px}
.sec-<id>{font-size:10px} に寄せたが、全要素が1.6倍で描画された(数値照合で発覚)。

項目 寄せただけ calc変換後
.p-home-section__body 1240x500 1424x520 1240x500 ✅
.entry 250x250 260x260 250x250 ✅
.m-contact 1340x242 1424x386 1340x242 ✅

真因: remroot em = <html> の font-size 固定。祖先要素に font-size を置いても rem は一切変わらない。
.sec-<id>{font-size:10px} は「子の em」を変えるだけで、rem は既定の16pxのまま
12rem(=120px想定) が 192px になり、余白・最大幅・文字サイズが全部ズレる。
単独プレビューでは"それっぽく"見えてしまうので目視では絶対に気づけない(数値照合でのみ検出できる)。

正しい直し方=rem を CSS変数 calc に機械変換するhtml を触らないのでホストHPに貼っても崩れない):

# 1) 基準値を変数で持つ(元サイトの html の切り替えをそのまま写す)
.sec-<id>{--r:10px}
@media only screen and (min-width:1600px){.sec-<id>{--r:12.5px}}
@media only screen and (max-width:1023px){.sec-<id>{--r:2.57vw}}

# 2) スコープ化済みCSSの全remを一括変換(生成後の文字列に対して1回かけるだけ)
REM = re.compile(r'(?<![\w.#-])(\d*\.?\d+)rem\b')
css = REM.sub(lambda m: f'calc({m.group(1)} * var(--r))', css)
  • top:calc(50% - .8rem)calc(50% - calc(0.8 * var(--r))) とネストしても正しく効く
  • body側の font-size:1.6rem も同じ変換で .sec-inner に載るので、html/body 2段構造が完全再現できる

教訓: rem を使っているサイトは「寄せる」だけでは必ずズレる。 getBoundingClientRect() の数値照合を
やっていなければ、そのまま格納して「サイズが全部おかしい」案件になっていた。照合ゲートは絶対に省略しない。

同時に効いた小ネタ

  • html{font-size:…}3段階(既定10px / min-1600px:12.5px / max-1023px:2.57vw)ある設計。1つ見つけて満足しないgrep -oE 'html\s*\{[^}]*\}' で全部出す)
  • a{color:#000;text-decoration:underline}h1..h6{font-weight:inherit}拾わないと即バレる(リンクが青くなる/見出しが太くならない)。スコープ版で必ず入れる
  • mask-image の url(../imgs/…svg)data URI 化すると、絶対URL化とCMSパス除去を同時に満たせる(215バイトの矢印SVGなら軽い)
  • networkidle が来ないサイトがある(OND°)→ domcontentloaded + スクロール + 待機に切り替える
  • このPCは node が PATH に無い → export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"
    ただし playwright は ~/dev/terasu-lab/node_modules にしか無いので、比較スクリプトは tools/ に置いて terasu-lab から実行する(終わったら消す)

STUDIO製サイトは「属性セレクタ丸ごと」で抜ける(2026-08-21・東急新卒採用サイト)

https://www.tokyu-recruit.jp/(Nuxt + STUDIO)で3件抽出。STUDIO製と分かったら以下の型に乗せるだけで数値完全一致まで行ける(実測:全277要素の getBoundingClientRect() が1pxも違わず一致)。

見分け方

DOMのclassが sd appear text sd appear image sd appear のような汎用名だけで、各要素に data-s-<uuid> が付いていたらSTUDIO。
CSSは <style> が50個以上に分かれ、.sd[data-s-<uuid>]{…} の形で入っている。

抽出の型(4点)

  1. ベースCSSは全部 :where(.render-canvas) … にスコープ済みrender-canvassec-<id> に置換するだけでスコープ化が完了する
    :where() のまま置換すること(.sec-x に変えると詳細度が上がり、.sd[data-s-…] とのカスケードが逆転して崩れる)。
    data-s ルール側も :where(.sec-<id>) を前置=詳細度を1ミリも動かさないのが要点。
  2. CSS変数は最後の <style>:root{--s-color-… --s-font-…} として全部ある(東急は685バイト)。これを .sec-<id>{…} にして先頭へ。
  3. 画像はDOMに無い<div class="image" data-r-…><style></style></div> の空styleへJSが実行時に注入する
    .sd[data-r-…]:before{background-image:url(https://storage.googleapis.com/studio-design-asset-files/…)})。
    --dump-dom では画像ゼロになる。Playwrightで実行後DOM+document.styleSheets の cssText を取るのが正解。
  4. @font-face は entry.css 側にある。使う font-family 名で絞って同梱(Material Icons を落とすと矢印が keyboard_arrow_right の文字になる=トップ10の⑩と同じ症状)。

このサイトで踏んだ崩れ2つ(STUDIO共通なので次も出る)

症状 真因 直し方
セクションの高さが必ず画面高になる .render-canvas{min-height:100dvh}.StudioCanvas>.sd{min-height:100dvh} がラッパ/セクションに誤爆(元はページ全体の器の指定) 末尾に .sec-<id>{min-height:0}.sec-<id> > .sd{min-height:0;overflow:visible} を足して打ち消す
PCでは出るのにSPで見出しが消える .appear は出現アニメの状態クラスで、JSがスクロール時に外す。PC幅でDOMを取ると「PCでdisplay:noneだったSP用要素」は発火せず appear が残り opacity:0 のまま PC/SP/TBの3幅でDOMを取り、どれかで appear が外れている要素は外す(文書順で突き合わせてマージ)。東急では9件が該当

数値照合スクリプトの注意

元DOMには注入用の <style> が要素として混ざるため、sel, sel * で拾うとインデックスが1つずつズレて全部DIFFに見える。
filter(e => !['STYLE','SCRIPT'].includes(e.tagName)) を必ず入れる。textContentも比較に入れると同じ理由で偽陽性になるので、寸法+opacity+displayだけで比較する。

素材(動画)が消えるケース

受領直後は ls できたのに、作業中にデスクトップから実体が消えて cpqlmanage も失敗(TCCではなく本当に無かった)。
動画・画像を受け取ったら、真っ先に作業ディレクトリへコピーしてから着手する。ffmpeg がこのPCに無いので、
フレーム抽出は Playwright か qlmanage -t(遅い・2分でタイムアウトした)。swiftc は SDK 不整合でビルド不可。


2026-08-21 松下組 お問い合わせ導線(matsushitagumi-contact-saki-0821)

参考元 https://matsushitagumi.com/contact/ の .t-contact-means(電話/メールの2ブロック)。
4幅(1440/1024/768/390)で寸法・実文字幅とも完全一致・修正ゼロで格納できた回。効いた型を残す。

sec-<id>{font-size:10px} では rem は直らない(コマンド本体の記述より正確な事実)

rem常にルート要素(html)基準なので、html{font-size:62.5%}sec-<id>{font-size:10px} に移し替えても
scoped側では効かない(ホストHPに貼ると 1.4rem が 22.4px になる)。
rem値を px に機械変換して書くのが唯一確実な方法(10px基準なら 2.5rem25pxcalc(1rem + 2.65vw)calc(10px + 2.65vw))。
視覚は完全に等価で、単独でもホストHPでも同じ寸法になる。これを標準にする。

幅が 1024px以下でだけ 14px ズレる = body の既定マージン8px

元サイトは巨大リセットで body{margin:0} が効いている。抽出側は sec-<id> 配下しかリセットしないので
body の 8px マージンが残り、width:90% の器が両側で 7px ずつ内側に寄る(1440では max-width:1100 で頭打ちになるため気づけない)。
<style>body{} を書くのは禁止なので、<body style="margin:0"> のインライン属性で消す
セクション本体(div)を貼るときは付いてこないのでホストHPを汚さない。単独プレビューだけ正しくなる。

Adobe Fonts(Typekit)の扱い

  • kit CSS(例 https://use.typekit.net/tll5pmd.css)は 他ドメインからでも読めるので、<link> 1本でそのまま同梱してよい(画像を原URLで残すのと同じ思想)。kit id は Playwrightの page.on('request') でネットワークを見て取る(DOM/JSをgrepしても出てこない=JSが動的に挿す)。
  • document.fonts.check('40px azo-sans-web') は当てにならない元サイトでも false を返した(weight指定違いで落ちる)。
    → フォントが効いているかは Range.getBoundingClientRect().width(実文字幅)を元サイトと突き合わせるのが確実。今回は 275/207/207/211 が4幅とも一致。

数値照合は「元と再現でセレクタ名が違う」ので対応表で回す

リネーム後は .p-contact__title.contact-row__title のように名前が変わる。
{orig:{sel:{…}}, repro:{sel:{…}}} のキー対応表を作って同じキー同士を比較すると、そのまま4幅ループに乗る。
差が出たら 幅x高さ@x の3値のどれがズレたかで真因が割れる(全要素が一律 -14px/+7px なら器の幅=body/スクロールバー系)。

実務メモ(このPC)

  • node は /Applications/ChatGPT.app/Contents/Resources/cua_node/bin/nodeContents/Resources/cua_node 直下。過去メモのパスから移動している)。
    Playwright は ~/dev/terasu-lab/node_modules にあるので、スクリプトは ~/dev/terasu-lab/tools/ に置いて実行する(/tmp から実行すると ERR_MODULE_NOT_FOUND)。
  • 検証用の一時スクリプトを tools/ に置いたらcommit前に必ず消す
  • sections-data.json は git add しない。他セッションの差分が既に乗っていることがあり、巻き込んで commit してしまう(CIが全件再生成するので不要)。
  • ローカルプレビュー用の python3 -m http.serverセッションを跨ぐと死ぬ古いセッションのサーバーが同じポートを別ディレクトリで掴んでいることがある(8099で別リポの中身が出た)。起動直後に必ず自分のidで200を確認してから計測に入る。

2026-08-21 竹中大工道具館「植物×匠」5件(FV/NEWS/みどころ/展示構成/開催概要)

結果: 5件とも全要素で数値完全一致(143/44/78/71/72要素の w・h・x・y・font-size・color)。到達までに踏んだ地雷は以下。どれも他サイトでも必ず起きる汎用パターン

① セレクタのコンマ分割は「カッコ対応」でやる(CSS全滅の新パターン)

rule.selectorText.split(',') で分けると :where(ul, ol):where(ulol) に割れる
出力した <style>そこで波カッコの対応が壊れ、以降のルールが全部死ぬ(実測: 83ルール→19ルールしか解釈されない)。
- 症状は learning.md 既出の「base64二重書き込み」と同じ=文字だけ・リンクが青い下線、高さが元の4倍。
- 検出は必ず document.styleSheets[N].cssRules.length を数える(目視では「ちょっと崩れてる」に見えて原因に辿り着けない)。
- 対処: 深さカウンタ付きの splitTop(sel, ',') を使う(( [ で depth++)。scopeSel 側の分割も同じ関数で

:not(:focus) を含むルールが丸ごと落ちる

疑似クラスを剥がす処理で :not(:focus):not(:active):not():not() になり、querySelectorAll が throw → そのルールを「使っていない」と誤判定して捨てる
今回は .visually-hidden(スクリーンリーダー用テキスト)が効かず、隠し文が画面に出てレイアウトが下へ押された
→ 剥がす正規表現に :not\(\s*:[^()]*\) を丸ごと削除を足す。

③ JSが入れるCSS変数(--vw / --vh)を必ず補う

margin-right:calc(50% - 50 * var(--vw)) のように JSが setProperty("--vw", …) した変数前提のCSSがある。
無いと calc ごと無効化され、画面幅より広く出す設計(1390px幅の帯)が 100% に潰れる
- 着手時に grep -oE 'setProperty\("--[a-z-]+"' main.js で洗い出すのを定形にする。
- 対処は sec-<id>{ --vw:1vw; --vh:1vh; }JSと同じ値になり、貼り先でも自動追従する(px固定にしない)。

④ body の継承値は「px」で持ってこない(縦書きが潰れる)

:where(body){line-height:1}計算値は 16px。これをそのまま sec-<id>{line-height:16px} にすると、
font-size 29px の子が line-height 16px を継承して縦書きの行が重なる(幅 88px → 62px)。
px なら lineHeight / fontSize の無単位比率に直して出すtext-align font-weight も body から拾い忘れると効いてくる(本文の中央寄せが消える)。

⑤ SVGスプライト(<symbol> / clipPath)はセクションの外にある

矢印・有機シェイプのバッジ・FVのマスクは、body 直下の非表示 <svg><symbol id="i-arrow"> として置かれ、
セクション内は <use xlink:href="#i-arrow"> だけ。そのまま切り出すとアイコンが 0×0 で消える
→ 部分木の href="#…"url(#…) を集めて外にある定義を取り込みid-<短縮id> を付けて貼り先との衝突を防ぐ(参照も同時置換)。

shoot_section.mjs は動画ループのセクションで黙ってサムネが欠ける(本体修正済み)

waitUntil:'networkidle'ループ再生の <video> があると永久に来ないgoto タイムアウト → catch(e){return false} で握りつぶし、
カード用だけ出て __detail.png / __sp.png が無い状態になる(詳細ページが古いまま/空になる)。
→ 2026-08-21 に gotoSafe()(networkidle 20秒 → 失敗したら load + 3秒待ち)を追加して3箇所とも置換。同じ理由で check_responsive.mjs も動画セクションでは落ちるので、その件だけ同じ判定を自前スクリプトで回す。

⑦ このPCに ffmpeg は無い。動画のコマ送りは Playwright で「再生しながら」撮る

swiftc は CLT のモジュール衝突で通らない(redefinition of module 'SwiftBridging')。
currentTime を seek して canvas に drawImage する方式は、9コマ全部が同じ1フレームになる(seek 完了と描画が別)。
playbackRate=4play() しながら一定間隔で locator('#v').screenshot() が確実。
動画は python3 -m http.server で配ってから <video> に読ませる(file:// は canvas が汚染される)。

一発格納できた要因(次回もこの順で回す)

  1. 数値照合は 元と再現を同じ幅で開いて全要素の w|h|x|y|font-size|color を並べる(差が1件でも残っているうちは見た目を見ない)
  2. 差が出たらまず cssRules.length(CSSが死んでいないか)→ 次に器の幅(body margin / vw)→ 次に継承値(line-height / text-align)
  3. リネーム後にもう一度同じ数値照合(prefix対応表で名前を正規化して比較)。今回はリネームで壊れゼロを機械で確認できた

2026-08-21 GRUST(grust.co.jp)6件 — 「CSS抽出の一般解」を機械化できた回

参考元= https://www.grust.co.jp/ /受領=スクショ4+動画2=6件 → 6件とも作成・格納(追加者=さき)。
6件とも 元サイトと getBoundingClientRect() が完全一致(差はスクロールバー16px分のみ)。

一番効いた発見=「先頭コンパウンドの剥がし」は body の class/id に限定する

CSSからルールを拾う時、.p-phil-message__body p { … } のように知らないクラスが先頭にあるルールを
「先頭を捨てて p {} として拾う」実装にしていたら、他ページ用の指定が素タグ指定として大量に混入
1セクションで 914ルールに膨張した(svg90/p62/a30…)。
→ 剥がしてよいのは <body> の class / id(今回 is-loading is-opening is-change-start / #index)だけ
これに直すだけで 620 → 136ルール(719行)に落ち、見た目は不変。「長い=抽出ミス」の主犯はこれ

併せて要る判定4つ(どれも実害が出た)

判定 効果
負条件(:not)はページ文脈を見て落とす body:not(#index) .u-show-index{display:none} index を再現するのに拾うとロゴが消える(実際に消えた)
状態クラスはルートに寄せる body.init-end .l-header__nav__item .link{opacity:1} 登場アニメの発火条件。ラッパーに init-end を付けて .sec-<id>.init-end … にスコープ化=ナビが透明のままを回避
url() の絶対化は正規表現に注意 url\(\s*((["']?)[^)]*?)\s*\)空マッチして全部CSS自身のURLになる @font-face の src が style.css を指しフォントが全滅する(1回踏んだ)
ワイルドカード扱いのクラスだけの部分は捨てる .is-opening{} .ps__rail-y{} 単独 is-/js-/u-/swiper/ps を「実行時に付く」として通すと、実在クラスを1つも含まないルールまで拾う

スクロール駆動FVは「終端の状態クラス」を付けると単体で完成状態になる

p-top-mv の GROW FOR TRUST は .p-top-mv__fixItemposition:fixed+JSで進行。CSSに
.is-absolute(スクロール終端の状態) が用意されていて、これを付けると
.is-absolute.is-bottom .svg-mv-copy{opacity:0} が効き、波動画が文字マスクから覗く完成状態になった。
→ 単体化で困ったら、まず その効果の"終わりの状態クラス"がCSSに無いか探す(自作で寄せない)。

Swiper は「JSだけCDN・CSSは元サイトから」

CDNの swiper.min.css を読むと矢印が青い「❮ ❯」のテーマ矢印になる(元サイトは丸ボタン)。
site.css 側に swiper のCSSが入っていたので、CSSはスコープ化して同梱/JSだけCDNにしたら一致した。
(v2.5 のトップ10 #10 と同じ現象。「テーマCSSはCDNから取らない」は Swiper でも同じ

ヘッダー単体のサムネは「開いた変種を一時的に置いて撮る→本番ファイルを戻す」

閉じた状態だと採用ゲートに落ちる(色18種/輪郭0.6%)。手順:
1. cp code-previews/<id>.html /tmp/<id>_real.html必ず先に退避
2. 開く対象に shot-open を足し、@media(min-width:801px){ .sec-<id> .l-header__nav__item.shot-open .l-header__nav__box{opacity:1;transform:translate3d(0,0,1px);pointer-events:all} }</style> 前に追記/min-height も足す
3. node tools/shoot_section.mjs <id> → 4. 退避したファイルを戻す
class属性は抽出時に正規化されて末尾スペースが消えるので、... js-nav-toggle " のような
生DOMのままの文字列で置換すると当たらない(無言で失敗して同じ結果になる。1回踏んだ)

環境まわり(このMacで詰まった順)

  • ffmpeg / ffprobe が無い・swiftc も壊れている(arm64e swiftinterface エラー)→ 動画のコマ確認は
    Range対応の自前HTTPサーバ + ブラウザで video.currentTime を送ってスクショが最短。
    python3 -m http.serverRangeを返さないので seek できないreadyState が 1 で止まる)=ここが罠
  • Desktop直下がサンドボックスで見えないls が空・mdls だけ通る)→ dangerouslyDisableSandbox で確認したら
    実体は Desktop/名称未設定フォルダ/ にあった。「ファイルが無い」と判断する前に mdfind -name で実在確認
  • node がPATHに無いexport PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"
  • pjaxサイトは --dump-dom が別ページを返すことがある(GRUSTはphilosophyページのDOMが返ってきた)
    curl で生HTMLを取るのが確実(サーバーレンダリング済みなら全セクション入っている)
  • shoot_section.mjs動画ループで networkidle に届かず初回タイムアウトすることがある(再実行で通る
  • CIは他セッションのpushで自分のrunがcancelledになるgh run list最新runの成否を見て判定する

追記(同日): <body> の既定 margin 8px が「サムネの下端切れ」と「幅16pxズレ」の犯人

症状: ①お問い合わせ帯のサムネで枠の下辺だけ写らない(テキストは全部見えるので気づきにくい)
②数値照合でセクション最上位の幅だけ 1440 → 1424 と16px小さい(スクロールバー差だと誤診しやすい)

真因: プレビューHTMLの <body> に既定マージン8pxが残っていた。
- 左右 8+8=16px → ルート要素の幅が16px縮む(内側は max-width 中央寄せなので一致してしまい、原因が見えにくい)
- shoot_section.mjs は中身の実高さ(例242px)で ビューポートを242pxに縮めてから clip{y:8, height:242} を撮る。
必要なのは 8〜250px なのにビューポートが242pxしかない → 下端が撮れない。低いセクションほど致命的
(実測: 中身242px のはずが 202pxしか写っていなかった

直し方: <body style="margin:0">インライン属性で潰す。
- body{margin:0}<style> に書くと "ホストHPに貼ると崩れる外来グローバルCSS" になりスコープ漏れgrepにも引っかかる
- インライン属性なら sec-<id> の外に1行も漏れない(貼るのは <div class="sec-…"> 以下だけなので無害)
- 検証: img/sections/<id>.png の非白ピクセルの上下端を測り、実寸高さが元の高さと一致するかを数値で見る
python from PIL import Image im=Image.open(p).convert('L'); w,h=im.size; px=im.load() ys=[y for y in range(h) if any(px[x,y]<200 for x in range(0,w,4))] print('中身の実寸高さ', (ys[-1]-ys[0]+1)/2) # ← セクションの実高さと一致するか

追記(同日): 数値照合は document.fonts.ready を待たないと偽の差分が出る

ボタン幅が 188 vs 186 と2px合わず、computed(font-family/weight/size/letter-spacing/gap)は完全一致
原因は再現側だけWebフォント未適用のまま測っていた(フォールバックのヒラギノで描画)。
比較スクリプトに以下を入れたら 9/9 完全一致になった。この2行が無いと「原因不明の数px差」を延々追うことになる

await p.evaluate(()=>document.fonts.ready);
await p.evaluate(()=>document.fonts.load('700 18px "Zen Kaku Gothic New"'));
await p.waitForTimeout(1500);

追記(同日): ローカル検証サーバは --directory を絶対パスで

python3 -m http.server 8080 --directory code-previews は、シェルのcwdが戻る環境だと404を返し続ける
気づかずブラウザ計測すると「要素がnull」で混乱する。必ず絶対パス、起動直後に curl -o /dev/null -w "%{http_code}" で200を確認してから測る。


2026-08-21 「カーソル合わせてもアニメが出ない」の真因はホバーCSSの祖先クラス前置(恒久・毎回チェック)

大串FB(udatsu 3件): 「①上につまりすぎ、カーソル合わせてもアニメがでない ②カーソル合わせたら背景画像出るはずなのにでない、アニメがでない ③カーソル合わせてもアニメがでない」
3件とも同じ1つの原因。数値照合が全項目一致していても、ホバーは丸ごと死んでいた。寸法一致=完成ではない。

原因1:ホバーCSSが .no-touch 前置で全滅(最頻・最重要)

多くのサイトは Modernizr 系で html/bodyno-touch(他に desktop chrome mac wf-active 等)を付け、
ホバー指定を全部 .no-touch ~:hover で書く。udatsu は hoverルール50件中ほぼ全部がこの形だった。

.no-touch .ui-btn:hover.type-link .btn-circle:before{opacity:1;transform:scale(1)}

セクションを切り出すと .no-touch を持つ祖先が消えるので、.sec-<id> .no-touch .ui-btn:hover … となり1つも発火しない
対処=抽出時に root の祖先クラスを全部集めておき、スコープ化のときにセレクタ先頭の「祖先を指すトークン」を落とす

// 抽出時:root の祖先クラスを収集して JSON に入れる
const ancestors=[]; let e=roots[0].parentElement;
while(e){ String(e.className||'').split(/\s+/).filter(Boolean).forEach(c=>ancestors.push(c)); e=e.parentElement; }
# スコープ化時:先頭トークンのクラスが全部 ancestors に含まれるなら、そのトークンごと削除
# .no-touch .ui-btn:hover .btn-circle  →  .sec-<id> .ui-btn:hover .btn-circle

検品: grep -c 'no-touch' code-previews/<id>.html0 であること(>0なら確実にホバーが死んでいる)。

原因2:ホバー演出が :hover ではなく JSが付ける状態クラスのことがある

udatsu のフッターは、ホバーで section.home-footeris-service / is-company が付き、
そのクラスのCSSが「見出しの色反転・説明文フェードイン・背景写真の表示」を全部やっていた(:hover は1件も無い)。
抽出時は状態クラスが付いていないので .home-footer.is-service …root.matches() に落ちて全部捨てられる
対処は2段構え
1. 抽出の判定セレクタから .is-* / .has-* / .js-* を除去してマッチさせる(状態クラス側のルールを拾う)
2. 再現HTMLに 元と同じトリガーでクラスを付けるJSを入れる(推測の自作アニメではなく、実装の再現)

el.addEventListener('mouseenter',()=>sec.classList.add('is-service'));
el.addEventListener('mouseleave',()=>sec.classList.remove('is-service'));

見つけ方=元サイトで hover 前後の class と inline style の差分を取る(computed だけ見ていると原因が分からない)。

原因3::not(:hover) が空 :not() になって例外→ルールごと捨てる

擬似クラスを除去する実装だと .menu-lists:hover .menu-list:not(:hover):not() になり querySelector が throw。
「他のリンクだけ薄くなる」系が丸ごと消える。除去後に :not() を削ること。

原因4(①の「上につまりすぎ」):セクションの上余白は直前セクションの padding-bottom が作っている

.home-journal{padding:0 0 180px}=自分は下だけ。上の余白は直前 .home-renovation{padding-bottom:180px} 由来。
単体で切り出すと上が詰まって見える直前要素の padding/margin-bottom を実測して同量を padding-top で補う(MQごとに実測。今回 PC180px / ≤1024px 90px)。

登録前チェックに追加(数値照合と同格)

  1. grep -c 'no-touch'(および desktop no-mobile 等の環境クラス)が 0
  2. 主要インタラクションを実際に hover して computed 差分を数える(0件なら死んでいる)— 目視より確実
const before=snap(); await p.hover(sel); await p.waitForTimeout(1000); const after=snap();
// opacity/transform/color/borderColor/backgroundColor を ::before ::after 込みで比較し、変化0件なら❌
  1. 元サイトの同じ状態のスクショと並べて突き合わせる(「元もこう見える」ことまで確認して初めて完成)

2026-08-21 rossa.co.jp(六甲サービスセンター)3件 — Lottieアイコンと同時コミット事故

file:// では blob URL の fetch がブロックされる(Lottieが真っ白になる)

参考元の Lottie JSON(/assets/images/top/icon0N.json)は CORSヘッダーを返さないため、
lottie-playersrc に元URLを入れると lab のドメインから読めない(アイコンが出ない)。
実データをインラインで同梱して読ませる。このとき blob URL を経由してはいけない

// ❌ localhost では動くが file:// では fetch がブロックされ描画されない
p.load(URL.createObjectURL(new Blob([JSON.stringify(data[name])], {type:'application/json'})));
// ✅ オブジェクトを直接渡す(fetchが起きないのでどこでも描画される)
p.load(data[name]);

サムネ撮影(shoot_section.mjs)は file:// で開くので、
「localhost のプレビューは正しいのにサムネだけ中身が無い」時はまずこれを疑う。

freezeLottieseek('100%') が線描画アイコンを消す

shoot_section.mjsprepare() は Lottie を 最終フレームへ送って止める。
線を描いていくタイプのアイコンは 最終フレームが「描き終わって消えた」状態のことがあり、
サムネだけアイコンが欠ける(実測:4個中3個が消え、1個は途中の枠だけ)。静止の正は frame 0
--force で誤魔化さない。v2.6 の指針どおり Lottieだけ止めずに素撮りして差し替える
freezeVideo/freezeAnim/oneSlide/reveal は共通ツールと同じ順で当て、freezeLottie だけ抜く。
カード=1440×1080@2x を clip → normalize_thumbs.py、詳細=1440×1000@2x、SP=390×760@2x isMobile。3枚セットで作り直す)。
検品の「色N種/輪郭N%」で落ちた時は、まず素撮りを目視して
「本当に中身が出ていないのか/白基調で薄いだけなのか」を切り分けてから対処する。

同時作業中の git commitパス指定で打つ(index共有事故)

明示 git add していても、別セッションが同じリポで add した直後に自分が git commit すると
相手のstageぶんまで丸ごと入る
(実際に他人の nakamura 系5セクションを巻き込んだ)。
git add -A を避けるだけでは足りない。

git commit -m "..." -- entries/<id>.json img/sections/<id>*.png code-previews/<id>.html   # ← パス指定

巻き込んでしまったら git reset --soft HEAD~1(indexは保持=相手のstageは残る)→ 上のパス指定commitで切り直す。
.git/index.lock が出たら消さずに待つ(相手のgitが動いている)。

効いたやり方

  • CSS抽出は「セクション内で実際に使われている class/tag/属性/id」だけを通すフィルタ+全ルール sec-<id> 化。
    :not(...)中身をクラス条件から外すbody:not(.wp-admin) 系のベース指定が丸ごと落ちるのを防ぐ)。
    CSSコメントを先に除去しないと /*! destyle */*,:before,:after{...} の先頭セレクタが壊れて box-sizing が落ちる。
  • 単独プレビューの body 余白は <body style="margin:0">(属性)で消す。
    <style>body{margin:0} を書かないのでスコープ漏れgrepも通り、貼り付け時にも漏れない。
  • Splide は生成物を素のマークアップへ戻してから再mountする(生成済みの <button class="splide__arrow"> を残すと矢印が二重になる)。
  • 数値照合は getBoundingClientRect()両方セクションを表示状態にしてから取る。
    スクロール位置が違うと「is-active の transform が当たっていない」だけで差が出て、崩れと誤認する(今回1回踏んだ)。

2026-08-21 グローウィン・パートナーズ(growin-fv / growin-value)

「スクロールできない」の真犯人=body由来の overflow-x:hidden をスコープ根に持ち込んだこと

元サイトの body{overflow-x:hidden;overscroll-behavior-y:none}sec-<id> に寄せると、
ラッパー自身が独立したスクロールコンテナになる(overflow-x:hiddenoverflow-yauto に格上げする)。
結果、①親ページが動かない ②中の position:stickyまったく効かない(カードが重ならない)。
大串FB「スクロールできない」の中身はこれ。→ 抽出後に必ず打ち消す:

.sec-<id>{overflow-x:visible;overflow-y:visible;overscroll-behavior:auto}

body の overflow / overscroll / scroll-behavior はページ級=セクションに持ち込まないを恒久ルールにする。

サムネだけ絵が違う ②=oneSlidereveal が「反復要素・transformのズレ」を壊す

shoot_section.mjs の前処理は同型の要素が重なって並ぶもの=スライダーと見なすため、
FVの「20枚の円を transform で少しずつずらした残像カプセル」が 円1個に潰れた(実測)。
- oneSlide … 同じ形が重なる20枚を「スライド」と判定して1枚目だけ残す
- reveal … 薄い要素に transform:none !important を当ててズレそのものを消す
--keep-slider --no-reveal を付けて撮る--no-reveal は今回 tools/shoot_section.mjs に新設(既定はON のまま・オプトイン)。
判定の目安: 「同じ部品を何枚も少しずつズラして1つの絵にしている」セクションは両方付ける。
プレビューは正しいのにサムネだけ違う時は、まずこの2つを疑う(CSSを疑って時間を溶かさない)。

rem基準を「スコープ化しつつ数値を1pxも狂わせない」方法

元サイトが html{font-size:calc(7*1px + 0.625vw)}(可変rem)だと、sec-<id>{font-size:…} に移すだけでは
rem はルート基準のままなので効かない(トップ10 #2 の変種)。今回は全 Nrem を機械変換して解決:

Nrem → calc(N * var(--rfs))    /* --rfs は sec-<id> と各@mediaに定義 */

:root の変数定義に含まれる rem も一緒に変換されるので、var(--font-size-…) 経由も正しく効く。
結果、元サイトと getBoundingClientRect()完全一致(1440px・見出し96px/カード1400×700 まで一致)。

複数セッション同時実行で実際に起きた事故2つ(どちらも構造的)

  1. 作業ディレクトリを他セッションに消される/tmp/section_<site> は同じサイトを別セッションが触ると衝突する
    (今回 /tmp/section_growin を別セッションが使用中→作業途中で丸ごと消えた)。
    サイト+箇所で分ける/tmp/section_growinfv)。消えても困らないよう、成果物は早めに code-previews/ へ置く。
  2. git のindexに他セッションのstageが載っている。この状態で git commit すると他人の作業ごとコミットする。
    git commit -m "…" -- <自分のパス…> でパス指定コミット(indexの他人の分はそのまま残る)。
    git add -A 禁止と同じ理由で、git commit も無指定は禁止にする。
  3. push が重なると CI が cancelled になる。runの成否より本番の実体で確認する
    /sections に自分のidが出るか。詳細ページの 200フォールバックでも返るので当てにしない)。

2026-08-21 LIG(liginc.co.jp)3件 — Tailwind製サイトの抽出で踏んだ罠と、その機械的な直し方

動画3本(Solutionsカード横スクロール/Explore the Knowledge丸ボタン/Solutions全画面スライダー)。
3件とも「元サイトと寸法が完全一致」まで持っていけたので、そこに至る型を残す。

Tailwind製サイトからCSSを抜くときの2大事故(どちらも「md:が全部効かない」という同じ症状で出る)

  1. クラス名トークンの正規表現でバックスラッシュを許すと壊れる
    Tailwind の実CSSは .md\:h-\[373px\] のようにエスケープされている。
    トークン抽出の文字クラスに \ が含まれていると md\ で切れて、md: 系の指定が丸ごと落ちる
    (症状=カード幅が 23rem でなく 17rem になる/見出しが text-xl のまま)。
    \.((?:\\.|[^\s.,:>+~()\[\]{}#*"'=\\])+) のようにエスケープ側を先にマッチさせる。

  2. CSSを自前で分割するとき、セレクタ内の \' を文字列開始と誤認すると以降が全滅
    .before\:content-\[\'\'\]:before{content:""} でパーサが暴走し、そこから後ろのCSS全部が1個のゴミブロックになる。
    → クオートの直前が \ なら文字列扱いしない。検算=分割したブロック数がブラウザの document.styleSheets[i].cssRules.length とほぼ一致するかを必ず見る(今回 2697 vs 2690 で一致確認)。

rem は「スコープ化」では絶対に再現できない(コマンド本体の記述だけでは足りない)

html{font-size:…}sec-<id>{font-size:…} に寄せても、rem はルート要素基準なので効かない(em は効く)。
LIG は <768px: 1rem=4vw / >=768px: 1rem=1.66667vw(1440幅で 1rem=24px)。
宣言部の Nrem だけを calc(N * var(--sec-rem)) に機械変換し、--sec-rem をメディアクエリで切り替える。

.sec-x{--sec-rem:4vw}  @media (min-width:768px){.sec-x{--sec-rem:1.66667vw}}

変換していいのは {} の中身だけ。セレクタを変換すると .md\:w-\[23rem\].md\:w-\[calc(...)\] になってHTMLと一致しなくなる(これも「md:が効かない」症状で出るので上の2つと紛らわしい)。
JSが left: 2 + "rem" のようにremを書き込む箇所も同じ理由でズレる → スコープ根の font-size を実測して px で渡す(=サイトと同じ実寸)。

フォントが読めないときの詰め方(今回 acumin-pro / Adobe Typekit)

  • Typekit の kit(use.typekit.net/xxxx.css)はドメイン制限で他ドメインからは 412。Referer偽装でも通らない → 代替する。
  • 実サイトが自前配信している woff2 も、他ドメインからは CORS で読めないNo 'Access-Control-Allow-Origin')。
    日本語が謎の書体になったらこれ。→ 同じ書体を Google Fonts から読む(Noto Sans JP 等)。
  • 代替書体は可変幅(wdth軸)を持つものを選ぶと数値で詰められる。acumin-pro ≒ Archivo
    実テキスト幅を Range で測って比較 → font-stretch:105%誤差 0〜2% に収束(100%だと一律 -4〜5% 細い)。
// 実テキスト幅の測り方(ブロック幅では font の差が出ない)
const rg=document.createRange(); rg.selectNodeContents(el); rg.getBoundingClientRect().width

「JS実行後のDOM」を土台にするときの落とし穴(GSAPが効かなくなる)

--dump-dom で取った DOM には JSが書いた style="opacity:0;transform:translate(0%,110%)" が残っている
この状態で gsap.set(el,{yPercent:0}) を打ってもまったく効かない(GSAPが既存transformを y=205.9px としてキャッシュし、yPercent:0 と足し算になる)。
1文字divなどに残った transform/opacity のインラインstyleを外してから、元コードのJSに初期値を打たせる。
症状は「見出しだけ出てこない(親が overflow:hidden なので完全に消える)」。opacityだけ見るフォールバックでは検知できないので、
フォールバックは opacitytransform の両方を見る。

WebGLキャンバス背景は抽出不可 → 実測色を単色で置く

LIGの地色は #bgCanvas(three.js)。CSS上の htmlrgb(26,26,26) だが、実画面は rgb(50〜53)
CSSの値をそのまま使うとセクションだけ黒すぎる。→ 実スクショのピクセルを測って #333 を採用

from PIL import Image; Image.open('ref.png').convert('RGB').getpixel((30,300))

shoot_section.mjs は「transition遅延つきの出現要素」をフェード途中で固定することがある

スライダーの説明文(transition: … .3s … .1s の遅延つき)だけが毎回サムネから消えた(見出し・画像・ページャーは写る)。
プレビュー自体は正常(localhost/file:// で 3.5秒待てば opacity:1)。再現性がある=ツール側の固定タイミングの問題
→ v2.6 の指示どおりアニメを止めず素撮りして3枚とも差し替える。サイズはツール出力と同じにする:
<id>.png 2880x2160(1440x1080 @2x)/__detail.png 2880x2000(1440x1000 @2x)/__sp.png 780x1520(390x760 @2x)。

このMacの node のパスが変わっている

/Applications/ChatGPT.app/Contents/Resources/app.asar.unpacked/node_modules/cua_node/binもう無い
現在は /Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node(v24.19.0)。
playwright は ~/dev/terasu-lab/node_modules にあるので、検証スクリプトはリポ直下に置いて実行する(外に置くと ERR_MODULE_NOT_FOUND)。

今回効いた検証の順番(そのまま再利用できる)

  1. 元サイトを playwright で開いて html の font-size / body背景 / 主要要素の fontSize を実測(=rem基準の確定。ここを外すと全部ズレる)
  2. 抽出 → getBoundingClientRect() を元/再現で同幅(1440)で突き合わせw x h @x が一致するまで直す
  3. 一致しない時は症状からトップ10表を引く(今回は #2 rem基準・#3ではなく「セレクタまで変換した」自作バグ・#6 出現アニメ)
  4. check_responsive.mjs → スコープ漏れ grep → 外部ホスト grep(参考元ドメインが出るか/別サイトが混じっていないか
  5. document.styleSheets[].cssRules.length が二桁以上あるか(base64二重書き込み事故の検知)

waitUntil:"networkidle"計測タグが多いサイト(LIG等)ではタイムアウトするdomcontentloaded + waitForTimeout(9000) で取る。


送られてきた動画(画面収録)の中身を確認する手順(2026-08-21・Wpc. Patterns案件)

動画は「どのセクションか」を特定する唯一の材料なのに、この環境には ffmpeg / ffprobe が無い(pyobjc も無い)。
フレームを見ずに「スクショと同じ箇所だろう」と決めつけるとFB③(動画ぶんが作られない/別セクションを作る)に直結する
今回も、スクショ=ドロワーメニュー/動画=お知らせの横スクロールでまったく別のセクションだった。

手順(macOS標準 + playwright だけで完結・所要1分)

# ① macOS標準の avconvert で H.264 に変換(画面収録は HEVC で Chromium が読めない)
cp "<録画.mov>" /tmp/section_<site>/v.mov
avconvert --preset PresetAppleM4V720pHD --source /tmp/section_<site>/v.mov --output /tmp/section_<site>/v720.m4v

# ② file:// はブロックされるので http で配信して playwright で seek→スクショ
(python3 -m http.server 8392 --directory /tmp/section_<site> &)
#   player.html = <video id="v" src="v720.m4v">  を置き、currentTime を12点動かして locator('#v').screenshot()
# ③ 12枚をHTMLのgridに並べて1枚のコンタクトシートにして読む(画像1枚で済む=コンテキストを食わない)
  • file:// の動画は setContent したページから読めない(タイムアウトする)。必ず http 経由にする
  • 画面収録は HEVC。変換せず Chromium に渡すと readyState が上がらず無限待ちになる
  • フレームは必ずコンタクトシート1枚にまとめてから読む(12枚を個別に読むとコンテキストが飛ぶ)

同案件で効いた小ネタ

  • body の 8px マージンで位置が全部8pxズレる。既存プレビューの作法どおり <body style="margin:0;background:#fff"> を付ける(<style>body{} を書くとスコープ漏れgrepに落ちる)。これで @x,y が元サイトと完全一致した
  • destyle 系の h1..h6{font-size:inherit;font-weight:inherit} を入れ忘れると見出しだけ計算値がズレる。スコープ版リセットに必ず入れる
  • CSSに残っている「死んだルール」を拾わない。今回 .p-top-news h3{font-size:15px} があったが、実DOMは <p class="__title">当たっていなかった。これを .news__title に付け替えたせいでカード高が 507 vs 535 とズレた。セレクタは実DOMに当たっているかを確認してから移植する
  • flex の align-items:stretchli の高さは最も高いカードに揃う。1枚だけ文字量が違っても li の実測値は一致するので、寸法照合では器(wrapper/list/li)の一致を優先して見る

2026-08-21 データ・アプリケーション採用サイト(dal.co.jp/recruit)5件

動画2本ぶんを作り忘れ、スクショ3枚ぶんだけ作って報告した(本コマンドFB③の再発)

何が起きたか: 動画2本+スクショ3枚を受領。動画のフレーム抽出に失敗したまま、添付表示された静止画3枚(FV/バナー/フッター)だけを見て「3件できました」と報告。大串の「動画の二つは?」で発覚。実際の動画は Future(横スクロール)と Project(ホバー背景切替) で、作った3件と1件も重なっていなかった

真因: 「動画が開けない」を作業続行の判断材料にせず放置したこと。素材の棚卸し表(受領N件⇔id対応)を作っていれば、動画2件が未着手のまま報告に進めなかった。
動画が開けない時点で止めて開ける手段を確保する。棚卸し表と件数照合は「報告直前」でなく着手前に作る

HEVC変換なしで動画フレームを撮る(既存手順より1工程短い)

既存手順(avconvert で H.264 化)は不要だった。playwright を channel:'chrome'(実Google Chrome)で起動すれば HEVC/H.264 とも再生できる

const b = await chromium.launch({channel:'chrome'});   // ← ここだけ。Chromium同梱版だと読めない
// 動画は http 配信(file:// は不可)。seek は効かないことがあるので play() して間隔撮り
await p.evaluate(()=>document.getElementById('v').play());
for (let i=0;i<7;i++){ await p.waitForTimeout(650); await p.locator('#v').screenshot({path:`f_${i}.jpg`}); }
  • currentTime の seek は無音で失敗する(全フレーム同一になる)。同一サイズのファイルが並んだら seek 失敗を疑う。再生しながら撮れば確実
  • swiftc / osascript(JXA) + AVFoundation はどちらも失敗した(swiftc は module 再定義エラー、JXA は AVURLAsset がブリッジされない)。この2つは試すだけ時間の無駄

崩れ原因(トップ10に追加すべき2件)

症状 真因 対処
背景写真・背景レイヤーが丸ごと出ない 元サイトが z-index:-1 の背景レイヤー(.project_bg.project::before)で背景を敷いている。そこへスコープ根に background:#fff を塗ると、負のz-indexは根の背景よりに描かれるので完全に隠れる。共通リセットが .sec-<id>{background:#fff} を持っていると気付かず毎回踏む そのセクションだけ .sec-<id>{background:transparent} にし、白は body 側<body style="background:#fff">)で出す。元サイトも body で白を出している
サムネがホバー状態+変な余白色で撮れる shoot_section.mjs の reveal 強制が is-active 等を全要素に付けるため、ホバーで背景が変わる系は「ホバー中」で撮影される。余白色も背景写真から拾って茶色などになる node tools/shoot_section.mjs <id> --no-reveal で素の状態を撮る。ホバー/クリックで見た目が変わるセクションは既定で --no-reveal を疑う

rem基準が幅で3段階に変わるサイトの完全再現

dal は html{ font-size }100vw/37.5(〜767)→ 100vw/100(768〜1000)→ 62.5%=10px(1001〜)。rem は root 参照なので sec-<id>{font-size} に寄せても効かない。
全 rem を calc(N * var(--u)) に機械変換し、--u をブレークポイントごとに定義すれば完全にスコープ内で再現できる(負値 -1.5rem も拾うよう正規表現に -? を入れる/remove が誤ヒットするので残存チェックは目視確認する)。

.sec-x{ --u: 2.6666667vw; font-size: var(--u); }   /* font-size も合わせると em 継承まで一致する */
@media (min-width:768px){ .sec-x{ --u: 1vw; } }
@media (min-width:1001px){ .sec-x{ --u: 10px; } }
  • box-sizing を勝手に border-box にしない。dal は全域 content-box だった。元サイトを grep してからスコープ版リセットを書く(grep -n box-sizing が0件なら content-box を明示する)

検品・CIの誤判定

  • check_responsive.mjsnetworkidle でタイムアウトすることがある。参考元の重い画像を大量に読むセクション(背景写真3枚など)で発生。レイアウト不良ではないので再実行すると ✅ になる。落ちたら1回は再実行して切り分ける
  • gh run watchX(失敗)に見えて実は cancelled のことがある。他セッションのpushに追い越されただけgh run list で conclusion を見て、git merge-base --is-ancestor <自分のsha> origin/main で自分の変更が載っているか確認してから、最新のrunを待つ

2026-08-21 UVO(uv0.jp)5件で判明した落とし穴(全部が実害を出した・毎回チェック)

1. CSSの mask:url() はクロスオリジンだと効かない(アイコンが消える/四角い塊になる)

  • 症状: 矢印「>」や外部リンク「□」がまったく描画されない(mask失敗で要素ごと透明)、または白い四角のベタになる(mask読込失敗で非マスク描画)。
  • 真因: 元サイトは background-color:#fff + mask:url(../images/c_icon_arrow.svg) でアイコンを塗る作り。URLを絶対化しただけだと別ドメインからのmask取得になり効かない。
  • 対処: SVGをdata URI化して同梱する(500〜600バイト程度なので軽い)。
  • data URI 化のときは ( ) を必ず %28 %29 にエンコードする。SVGの transform="translate(-0.284 -0.428)" に括弧が入るため、素の url(data:...)最初の ) でCSSのurl()トークンが切れて全部壊れる
    Pythonなら urllib.parse.quote(svg, safe="/:=;,")( ) ' 空白を安全リストに入れない)。
    検証は拡大スクショで。15px程度のアイコンは等倍スクショでは潰れて見えないので「無い」と誤判定する。

2. destyle系の img{display:block;max-width:100%} を拾い忘れると画像が原寸で出る

  • 症状: 斜めカット3カラムの高さが 675px→800pxmax-height:50rem に張り付く)。
  • 真因: 元サイトのリセットに img{display:block;max-width:100%} があり、画像(1064x1500)が親幅に収まっていた。抽出時にリセットのimg指定だけ落とすと原寸で描画される。
  • 対処: スコープ版リセットに sec-<id> img{ display:block; max-width:100%; } を入れる。数値照合をやれば必ず捕まる(目視では「なんか大きい」で流しがち)。

3. <body style="margin:0"> がLabの既存慣例(body へのCSSルールは書かない)

  • 素のままだと body の 8px マージンで全幅セクションが 1440→1424 になり、左右に白フチが出る。
  • body{margin:0} をCSSに書くとスコープ漏れ検証に引っかかる → HTMLの属性で当てる(既存25件が同じやり方)。

4. position:fixed の全画面ナビは、ヘッダーの中に置いたままだとヘッダーの箱に閉じ込められる

  • 元サイトの .l-header-nav は header の子だが position:fixed でビューポート全面を覆う。単独プレビューで absolute にすると親(header)の 1320x167 の箱にクリップされ、帯状にしか出ない。
  • 対処: ヘッダーの top/left を負値で打ち消すtop:-3.4375rem; left:-3.75rem; width:100vw; height:100vh)。
    ナビを header の外に出す解決はやらない:header(z-index:20) の中に nav(z-index:10) がある構造が、
    「ロゴだけ幕の上に残り、MENU/SHOPは幕の下に沈む」という元の見え方そのものを作っているため。

5. 確認用ローカルサーバーのポートが他セッションと衝突する(違うHPを見る新しい経路)

  • 実害: python3 -m http.server 8099 が落ちた後、別セッションのサーバーが同じ8099を占有していて、
    こちらは気づかず 404ページをスクショして「できた」と判断しかけた
  • 対処: セッション固有のポートを使う(例 8177)。撮影前に必ず curl -s http://localhost:<port>/ | grep <自分のid>中身を確認してから撮る。
    → コマンド本体の「違うHPのプレビューを混ぜない」にポート衝突という経路を足すこと。

6. 白地に文字だけのセクションはサムネ採用ゲートに落ちるflatRatio>=0.92 && edgeRatio<0.03

  • NEWS一覧が「中身が写らない(色9種/輪郭2.9%)」で不合格。画面の95%が白だったため。
  • 真犯人は前セクションとの間隔padding-top:13.75rem=220px)。これは"隣との余白"であってセクションの中身ではないので詰めてよい。
    4.375rem に詰めたら一発で ✅(中身605px)。--force を使う前に、まず余白を疑う。

7. モリサワ(TypeSquare)のフォントは外部から読めない=再現の上限

  • morisawafonts.net/c/<key>/mf.css はドメイン限定配信。A1ゴシック M は当たらないので Hiragino Sans にフォールバック。
  • 影響は文字幅で最大4px(数値照合で確認済み)。英字は元サイトと同じ Google Fonts の Roboto が使えるので差は出ない。
  • 報告時に「差は◯px・原因はフォント」と数字で言えば、崩れとの区別がつく。

2026-08-21 non-no web(FV=Swiper cube/カバーフロー型カルーセル)— 抽出の型が1段階上がった回

「使用クラスで絞る」より 実DOMで matches 判定して抽出する方が精度が高い

クラス名の集合で base.css を絞ると、.inner .wrap .slide のような汎用名が他セクションのルールまで大量に拾う(実測 1491行)。
代わりに Playwright で元サイトを開き、対象セクション配下の全要素に対して el.matches(sel) が真になるルールだけ
selectorText + style.cssText で抜くと 93件 / 62件まで落ち、しかもカスケード順が保たれる(出力 504行 → 目安内)。

for (const sheet of document.styleSheets) for (const r of sheet.cssRules)
  if (r.type===1) for (const one of r.selectorText.split(','))
    if (els.some(e => e.matches(clean(one)))) keep.push({media, sel:one, decl:r.style.cssText});
  • clean():hover/:focus/:active/::before 等を外してから判定する(付いたままだと hover ルールが全部落ちる)
  • 抽出前に対象セクションを scrollIntoViewIfNeeded() して初期化させる
    non-no は IntersectionObserver で画面内に入ってから Swiper を起動する作りで、未初期化のまま抽出すると
    .wrap-txt.active(表示中の説明文)のような状態クラスのルールが丸ごと落ちる
    =プレビューで本文が出ない。
    「元サイトで見えているのに再現で出ない」ときは、まずこれを疑う。

今回踏んだ崩れ(全部 再発防止済み)

症状 真因 対処
カウンターの花形バッジが完全に消える mask-imageクロスオリジンだとChromeに拒否される(リクエストすら飛ばない)。マスクが全消しになり、中の数字ごと消える UIアイコンのSVGは data URI 化(写真は参考元URLのままでよい)
矢印アイコンが出ない CSS内の相対 url(../image/…) が404 CSSのURLを基準に urljoin で絶対化(トップ10の#4)
矢印とカウンターが17pxずれる 未使用CSSを削るフィルタが、クラスを持たない素タグのリセット(ul{margin:0})まで落とした フィルタは「クラスなしセレクタは残す」(実DOMマッチ済みなので安全)
PC/タブレットで8pxはみ出し(検品⛔) <body> 既定の margin 8px <body style="margin:0"> とインライン属性で消す。CSSに body{} を書くとスコープ漏れ検査に引っかかる
タブレットで横スクロール 元サイトの .wrap-content{width:1280px} 固定(元も768〜1280では横スクロールする設計) max-width:1280px; width:100% に。PC1440の見た目は不変

rem基準は sec-<id>{font-size:10px} では直らない

remhtml基準なので、html{font-size:62.5%} をスコープルートに移しても効かない(10px想定が16pxで描画され全部1.6倍)。
抽出CSS内の NremN*10px に一括変換するのが正解。事前に @media のクエリに rem が無いことだけ確認する。

ライブラリは同梱してよい(外部依存ゼロにできる)

Swiper 11.0.5(148KB)を <script> にインライン同梱 → 参考元ドメインへのJS依存が消え、バージョンも完全一致。
最終的な外部ドメインは fonts.googleapis.com / fonts.gstatic.com / 画像CDN だけになり、ドメイン照合が一目で通る

JS駆動スライダー(cube等)はサムネ撮影が落ちる

shoot_section.mjs は「中身が写らない(色17種/輪郭0.2%)」で不採用になる=回転の途中で撮れている
freezeAnim はWeb Animations APIしか止めないので、Swiperの3D変換は止まらない
Swiper APIで正面に固定してから自前で撮るautoplay.stop()setTranslate(snapGrid[activeIndex]*-1)updateProgress())。
撮ったら <id>.png / <id>__detail.png / <id>__sp.png の3枚を置いて normalize_thumbs.py を通す(1枚だけ差し替えると詳細ページが古いまま=v2.6の指摘)。

この環境(さきPC)の道具

  • node はPATHに無い → export PATH="/Applications/ChatGPT.app/Contents/Resources/cua_node/bin:$PATH"
    playwright は ~/dev/terasu-lab の node_modules にしかないので、スクリプトはリポ直下に置いて実行する(/tmp から実行すると ERR_MODULE_NOT_FOUND
  • ffmpeg が無いswiftc はSDK不整合で使えない → ObjC + AVFoundation を clang でビルドして動画からフレーム抽出できる
    AVAssetImageGenerator / -framework Foundation -framework AVFoundation -framework CoreMedia -framework AppKit
  • リポ直下の作業用スクリプトを rm -f _tmp_*.mjs で消さない。他セッションがコミット済みの _tmp_*.mjs が17個あり、巻き込んで削除した(git checkout -- で復元)。自分が作ったファイル名だけを消す
  • push が cannot pull with rebase: You have unstaged changes で止まるときは git -c rebase.autoStash=true pull --rebase(他セッションの未コミット変更を退避→自動復元)

2026-08-21 リビタ(www.rebita.co.jp)6件 — 「矢印/中身が消える」の新パターン=ルート状態依存CSSの誤適用

  • html:not(.wf-active, .wf-timeout) *{overflow:hidden}(Webフォント読込中に中身を隠すFOUT対策)を全要素に当ててしまう事故
    抽出時に先頭の html を剥がすと :not(.wf-active…) * が残り、sec-<id> * に化けて全要素が overflow:hiddenになる。
    症状は「カルーセルの矢印が消える」「セクション内の余白が数px〜15pxずれる」。要素は存在し getComputedStyle も正常なので気づきにくい
    elementsFromPoint にその要素が出てこないのが決定的な手がかり=祖先にクリップされている)。
    対策=normalize で html/body/:root を剥がした残りが :[ で始まるルールは捨てる(単独プレビューではルートの状態が再現できないため)。
  • :not(a, b) をカンマで split するとセレクタが壊れる。セレクタリストの分割は括弧の外のカンマだけで行う。
  • rem基準が vw のサイトhtml{font-size:.6944vw}=1440で10px / SPは 2.667vw)は、rem をスコープに寄せても効かない(remは:root基準)。
    sec-<id>{--rb: .6944vw} を定義し、CSS中の N remcalc(var(--rb) * N) に機械置換すれば、vw連動もブレークポイントもそのまま再現できる。
  • 状態クラスが付いたルールは matches 判定で落ちる.appear など)。判定時だけ状態クラス群を剥がして拾い、
    HTML側に最終状態(appear)を焼き込む。ScrollSmoother/lenis のサイトは window.scrollTo が効かないので mouse.wheel で降ろす
  • サムネ検品は「薄いグレー地+細字」で誤検知する(メガメニューが 色54種/輪郭2.7% で不合格)。
    画像を自分で目視してから --force で採用してよい。真っ白との区別は必ず目で行う。
  • ローカルの python3 -m http.server は同じポートを別プロセスが掴んでいると古い内容を配信し続ける
    「直したのに変わらない」時は file:// で直接開いて検証するのが確実(ポートの取り違えを構造的に排除できる)。
  • 作業用 /tmp はセッションをまたぐと消える。抽出スクリプトと中間JSONは ~/section_work/<site>/ のような永続パスに置く
  • 数値照合はクラス名を比較キーに入れない(オリジナル化でリネームすると全件差分になる)。寸法・font-size・color だけを突き合わせる
  • Typekit(use.typekit.net)は参照ドメイン制限で 412 → Lab からは読めない。
    neue-haas-grotesk-text"Helvetica Neue" / source-han-sans-japanese"Noto Sans JP"(Google Fonts) が実測で最も近い。文字幅が数px変わるのは許容。

2026-08-23 GBSメディカル(Next.js + Tailwind v4)4件 — 「CSSOM抽出 + :where() スコープ + --sec-rem」で一発格納できた回

受領=スクショ1+動画3の計4件(FV/3つの価値/改善ストーリー/メガメニュー)。4件とも数値照合ほぼ0差で通った。
汎用ツールを 22_AI運用ルール/コマンド集/section/tools/extract.mjs build.py cmp.mjs)に置いた。Tailwind製サイトは次回これを使う

使い方(3コマンド)

WORK=/tmp/section_<site>; mkdir -p $WORK   # サイトごとに分ける(混線防止)
node extract.mjs "<URL>" "<rootSelector>" "<id>" $WORK/x.json [openmenu] ["後から付くクラス名 空白区切り"]
PREVIEW_BG="#fff" BODY_PAD="70vh" SKIP_FONTFACE="zen old mincho|noto sans jp" \
  python3 build.py $WORK/x.json <id> "<URL>" "<title>" out.html "$(cat head.html)" [extra.css] [extra.js]
node cmp.mjs "<URL>" "<rootSel>" "http://127.0.0.1:PORT/out.html" ".sec-<id> > <rootTag>" none 40
  • extract.mjs = CSSOMで「実際にマッチするルールだけ」抜く(Tailwind全量インラインは重すぎ・grep抽出は漏れる)。ルート複数指定は "a||b||c"
  • build.py = スコープ化+URL絶対化+リンク無効化+自己完結HTML化。
  • cmp.mjs = 元サイトと同幅で getBoundingClientRect() 突き合わせ(登録前ゲート)。

実害が出た10点(全部この回で踏んで直した)

# 症状 真因 対処
1 メガメニューの×が出ない・ハンバーガーのまま スコープ前置 .sec-id .foo で詳細度が上がり、Tailwindの「同詳細度=後勝ち」が壊れて .scale-0in-[html.is-menu-opened]:scale-100 に勝った 前置は :where(.sec-id) で包む(詳細度0)。html.sec-id 置換も同じ
2 タブレットで横スクロール・全部デカい 元サイトが html{font-size:calc(100/var(--dynamic-layout-base)*16vw)}rem自体をvwスケール。font-size を .sec-id に移しても rem は html 基準のままなので効かない .sec-id{--sec-rem:<元の式>} を定義し、CSS中の Nremcalc(N*var(--sec-rem)) に機械置換(htmlを触らずに再現=貼っても漏れない)
3 タブレットで余白がPC値のまま --spacing-container-x-gap 等がメディアクエリで変わる変数なのに1440pxの実測値を焼き込んだ 390/768/1024/1440 で実測し、差分だけ @media (min-width:Npx) で段組み
4 in-[html.is-menu-opened]:scale-0 が丸ごと死ぬ html置換の正規表現がエスケープされたクラス名の中の html まで壊した クラストークン \.(?:[\w-]|\\.)+ を退避してから置換
5 非アクティブ楕円の高さが合わない h-0 のように状態切替で後から付くクラスは初期DOMに無く抽出漏れ 抽出時にプローブ要素(そのクラスを付けたdiv)を挿して拾う
6 見出しの寸法が測るたび変わる Typekit/Google Fonts の読み込み前後で文字寸法が変わる 照合前に await document.fonts.ready+0.8s
7 元サイト側が「アニメ前」の値で測れる 対象が画面外だと登場アニメ未発火 照合前に元サイトも scrollIntoView してから測る
8 FV背景が scale(1.058) opacity .7 で固定 オープニングアニメ途中の inline style をHTMLごと拾った WAIT_MS=16000 まで待って抽出(data-opening 付き要素があるサイトは要注意)
9 :where(...) 始まりのセレクタが壊れる ^::?[a-zA-Z-]+ で疑似要素と誤判定して .sec-id:where(...) にしていた 疑似要素は :: 始まりだけ:is()/:where() は html置換に回す
10 色/サイズが全部おかしい --gfont-* 等 next/font の変数は :root に無い ルート要素の computed から全カスタムプロパティを実測して焼く

動きは「実測 → 数式化 → 再現」で合う(推測で書かない)

  • 改善ストーリー(重なるカード): スクロール量ごとに scale を測ると 1 → 0.84 / 0.92 / 1.0 でクランプ。後続カードが「画面下端→sticky位置」に来る間に 0.08×残枚数ぶん縮むと判明 → GSAP ScrollTrigger(cdnjs 3.13.0)で start:'top bottom+=28' / end:'top <stickyTop>px' / scrub にしたら全スクロール位置で一致(最大0.6%差)
  • 3つの価値(楕円の巡回): JSはクラス付替でなくReactの再レンダリング。アクティブ/非アクティブのクラス文字列を両状態で採取し、位置は (j-active) mod 3上中央(5rem,-4.5rem)/左下(-6.25rem,6.25rem)/右下(6.25rem,6.25rem) を巡回。閾値は li の上端が520pxを下回った最大index、移動 0.4s(実測)。
  • メガメニュー: htmlis-menu-opened を付け、dialogの --clip-bottom を 100%→0%、オーバーレイ opacity 0→1(0.6s)。Labでは開いた状態を初期表示にして×とオーバーレイで閉じられるようにする。

付随Tips

  • Next.js/Vercel と Typekit の資産は外部から使える(woff2・画像・kit CSS とも access-control-allow-origin: *)。<link href="https://use.typekit.net/xxx.css"> でTypekitフォントもそのまま出る。
  • @font-face が244個(next/font のunicode-range分割)ある場合は Google Fonts の同一フォントに置換(252KB→44KBに縮む・視覚同一)。SKIP_FONTFACE で外す。
  • ffmpeg が無いMacでの動画フレーム抽出swift はSDK不整合で不可、PyObjCも無い。playwright(channel:'chrome')でローカルHTTP配信した .mov を <video> に読ませ、play()しながら requestAnimationFrame で canvas に描くのが唯一通った(currentTime のseekだけだと全部同じ先頭フレームになる)。file:// は setContent 経由では読めないのでHTTPで配信する。
  • サムネは shoot_section.mjs 一発でOK(v2.6で normalize 自動)。メガメニューは初期表示が開いた状態なら真っ白にならない。

2026-08-24 STUDIO(studio.design)製サイトの抽出 — shibuyasl-event-list(イベント一覧カード)

対象: https://shibuyastreetlive.jp/event の「イベント情報一覧」(サムネ+ステータスバッジ+日付+タイトル+黒丸矢印の横長カード/スクロールで右から64pxスライドイン)。
結果: 数値照合10項目すべて一致・レスポンシブ✅・動きも再現して格納(shibuyasl-event-list-saki-0824)。

STUDIO製の見分け方(これが出たら以下の手順に切り替える)

.StudioCanvas / .render-canvas / class="sd" / [data-s-<uuid>] の属性セレクタ / /_nuxt/entry.<hash>.css
デザイン指定は要素ごとの data-s-<uuid> 属性セレクタで、基幹レイアウトだけ entry.css に入っている(この分離が事故の元)。

実害が出た6点(全部この回で踏んで直した)

# 症状 真因 対処
1 カードが全部縦積み・画像0×0 entry.css<link>crossorigin が付いていて cssRules がCORSでブロックされ、.sd{display:flex;...} の基幹ルールを1本も収集できていなかった(ブラウザ内マッチだけだと気づけない 先に curl で落とし、page.addStyleTag({content: css})注入してから収集する。注入分は出力の先頭に置く(元の cascade 順)
2 同上(注入しても直らない) 基幹ルールが :where(.render-canvas) .sd{...}祖先前提で書かれており、^(html\|body\|:root...) の除去正規表現に掛からず捨てられた 先頭の :where(<ページ級祖先>) も剥がしてからスコープ化する(トップ10 #5の派生形。:where() 包みは要注意)
3 サムネ画像が全部404 uuid を一括リネームしたら、STUDIOの画像URLに含まれる uuid まで書き換えて壊れた 置換は文脈限定data-s-<uuid>--<var>-<uuid> だけ)。画像URLは触らない=原URL維持のルールとも整合
4 動きが再現できない(静止画になる) STUDIOは再生後に appear クラスを外すので、ライブDOMからの通常マッチでは .sd[data-s-X].appear{opacity:0;translate:64px 0} を1本も拾えない セレクタから .appear/.appear-active外した形でマッチ判定して収集 → HTML側にも appear を戻す → IOで appearappear-active に付け替え(stagger は CSS の transition-delay が担当するので数値は触らない)
5 タブレットで横スクロール(786>768) 装飾シェイプ(position:absolute の巨大な丸)は元サイトでは親の overflow:clip で切られていたが、その親はセクション外なので落ちる スコープルートに overflow-x: clip を持たせる(元のPC表示は不変)
6 サムネだけ大見出しが出ない__detail.png も) shoot_section.mjs1.2秒で凍結するのに、見出しは delay 300–950ms + duration 500–1400ms でまだ動作中/② そもそもoverflow:hidden の内側に102pxずらして隠れている子は IntersectionObserver が永久に発火しない(見えないから交差しない=鶏卵) ②を直す=親が出るとき、overflow で切られている子孫も一緒に出す(表示は変わらない)。①は撮影のときだけ transition-duration:0s !important を差し込んだ一時版で撮り、公開ファイルはアニメ入りに戻す(3枚とも直る

手順(STUDIO製はこの順で通る)

  1. curl/_nuxt/entry.<hash>.css を保存 → Playwright で本番を開き addStyleTag で注入
  2. セクションルートは余白を持つ親まで広げる(今回は 1440×3019 の器。内側だけ取ると 64px の左右余白が落ちる)
  3. マッチしたルールを entryinline の順に出力、:where(祖先)html/body/:root は剥がして .sec-<id> に寄せる
  4. :root--s-color-* / --s-font-*必ず先頭に--f-3d90083b: 'Noto Sans JP' 等。落とすと色もフォントも全滅)
  5. リネーム: data-s-<uuid>data-el-eN--s-color---c---s-font---f-.sd.elCSS側の .sd を忘れない)/CMS条件クラス(_Z4zCxeYb 等)はCSSが1本も無いので削除

caption の18字ゲート

add_section.py は「分類|会社名」が18字を超えると停止する。英字社名が長い時はサイト自身が使っている日本語表記にする
ブログ一覧|SHIBUYA STREET LIVE=25字で停止 → ブログ一覧|渋谷ストリートライブ=16字でOK)。業種で代用はしない。

動画フレーム抽出(ffmpeg無しMac・今回通った手順)

swift は CommandLineTools の modulemap 衝突(redefinition of module 'SwiftBridging')でコンパイル不可、PyObjCも無し。
Playwright + channel:'chrome'(H.264/HEVCが再生できる実Chrome)で、
動画と同じディレクトリに置いた HTML から <video src="rec.mov"> を読ませcurrentTime をセット→onseeked を待って screenshot で1枚ずつ取れた。
- file://….mov直接 goto するとダウンロード扱いpage.goto: Download is starting になる
- setContent + file:// の src は about:blank 由来で読めない → 兄弟ファイルとして置くのが要点
- ポート8137は ~/local-previews の launchd 常駐サーバーが使用中。空きポートを確認してからローカル配信する


2026-08-24 TERASU公式HP FV(terasu-fv-saki-0824)— 自社サイトを抜く時の落とし穴4つ

<head> のインライン <style> を拾わないと寸法が合わない(今回の最大の罠)

外部CSS(style.css)だけ抽出して満足するとposition もサイズも別物になる。TERASU公式HPは index.html
<head>インライン <style> が16ブロックあり、そのうちFVに実際に効く上書きが10件あった。
- .p-top__mvBgImg{position:absolute!important}(外部CSSは fixed
- .p-top__mvBgImg{width:130vw!important;height:130vh!important;margin-left:-65vw!important;margin-top:-15vh!important}
- .p-top__mvBgBody{background-color:#252525} / .js-bgslider img{object-position:50% 0} ほか
これを落とすと 1967x13291649x1034 になる(数値照合で即バレる/目視では気づけない)。
手順に追加: 外部CSS抽出のあと、必ず <style> ブロック全部をパースして
「セレクタのクラス集合 ⊆ 使っているクラス集合」のものを全部拾う!important 付きが多いので上書き順は最後に置く)。
data-shape= data-edge= のようなURLパラメータ前提の実験CSSは除外する(live DOM に属性が無いことを確認して落とす)。

② インライン <script> にも本番挙動がある

.p-top__mv.is-fvclip-unlocked を付ける ver12 のインラインJS(円の描画半径が画面四隅を超えたら clip-path:none にする合成レイヤ対策)が
index.html 側にあった。これが無いとスクロール後半で clip-path が巨大化したままになり、元サイトと挙動が変わる。
<script>(src無し)も必ず全部 grep するcircle-scale|mvcircle|slider|mvtxt 等の当該クラス名で)。

③ JSの分岐条件を「引数名から推測」してはいけない

pageonload(e)e を isPC だと決め打ちして if(isPC) でスクロール連動を囲んだが、
元サイトを390px/768pxで実測したら円は普通に動いていた--circle-scale 50→351/クローンも5枚生成済み)。
e は isPC ではない。分岐は推測せず、元サイトを各幅で開いて計算値を読むgetComputedStyle(el).getPropertyValue('--x'))。
見抜き方=let o=5; matchMedia('(max-width:1024px)') → o=20 のようにモバイル用の値が用意されているなら、
そのブロックがモバイルで死ぬ実装はありえない。

④ スクロール駆動セクションは「寸法」だけでなく「アニメの計算値」を突き合わせる

getBoundingClientRect() の一致だけでは、スクロール連動の傾き(interpolate の範囲・trigger/end)が合っているか分からない。
同じ scrollY に両方を送って --circle-scale / overlay opacity / blur / 付与クラスを読むと小数まで検証できる。
今回 y=450 / 900 / 1533 の3点で circle-scale=86.991369 / 123.982737 / 176.017263bgOpacity=0.586701blur=29.3478px
is-fvclip-unlocked の切替タイミングまで元サイトと完全一致を数値で確認できた。
%で比較してはダメ(元サイトは後続セクションがあるのでページ全長が違う=同じ%は別の場所)。絶対 scrollY を揃える

自社サイトは正本リポから抜く(DOMより確実)

TERASU公式HPの正本は ~/dev/terasu-hp/index.html + _assets/theme/style.css + assets/js/app.js)。
FVコピー「出会いを生む/ホームページを。」はSVGパスなのでDOM検索で本文がヒットしない(grep 出会いを生む → 0件)。
リポから抜けばインライン <style>/<script> の上書きも同時に見つかる(①②はDOMだけ見ていたら気づきにくい)。

単独プレビューの body 既定マージン8pxで全要素が8pxずれる

.sec-<id> *{margin:0}body自身には効かないので、単独プレビューだと全部が x8 y8 になる。
素の body{margin:0} を書くとスコープ規約違反(貼ると崩れる)→ <body class="tfv-standalone"> + .tfv-standalone{margin:0;padding:0}
にすれば、スコープ漏れ grep も通り、ホストHPに貼っても無害(ホストのbodyにそのクラスは無い)。

動画フレーム抽出のもう1つの手(clang + AVFoundation)

swift は modulemap 衝突で使えないが、Objective-C を clang でコンパイルすれば AVAssetImageGenerator が使える(依存ゼロ・数秒)。

clang -fobjc-arc -framework AVFoundation -framework AppKit -framework CoreMedia -o grab grab.m
./grab "<動画>" <出力dir> 12     # 等間隔12枚
sips -Z 1400 <出力dir>/*.png     # 2000px以下に必須リサイズ

ffprobe 不在でも mdls -name kMDItemDurationSeconds -name kMDItemPixelWidth <動画> で尺と解像度は取れる。