# 記事の図解414本を同じ色の決まりに ― 青を見れば主役が分かる

- 記録: LOG469（ひとり研究所の裏側）
- 公開: 2026/11/8
- 記事のページ: https://pixelstomotors.com/record/unify-article-figure-colors-by-role
- サイト: Pixels to Motors（ADAPT DESIGN / 島津 栄一） https://pixelstomotors.com/
- 要約: 記事の図解とグラフは、色に役割を決め（青＝主役・灰＝比べる相手・赤＝悪い・緑＝良い）字をそろえると同じ読み方ができる。pixelstomotors.com の図解414本とグラフ7枚、Google の表27か所をこの決まりに直した。

> AI が読むための記事の全文です（コード・実行ログを含む）。引用するときは上の「記事のページ」の住所を使ってください。

---

pixelstomotors.com の記事の図解とグラフは、色に役割を決めて（青＝その図の主役、灰＝比べる相手、赤＝悪い、緑＝良い）、字を Inter と Noto Sans にそろえると、どの記事の図でも同じ読み方ができる。サイトにある記事の図解414本と、記事のグラフ画像7枚、配膳ロボットのシミュレーションの報告動画を、この1つの決まりに直した。

**色の意味がそろったことで、読む人は図ごとに凡例を読み解かなくても、青を見れば「この図で見てほしいもの」が、赤を見れば「困っている所」が分かるようになった。** 変わったことは3つある。

- 図の読み方がそろう：前は、記事を書いた時期によって、黒一色と模様の図、オレンジと青の図、緑と赤の図が混ざっていた。今は414本すべてが同じ字・同じ地の色・同じ役割の色になった
- AI と検索が表の中身を読める：Google スプレッドシートの表を埋め込んでいた28か所のうち27か所を、サイトの中の HTML の表に作り直した。そのうち12か所は元のシートが消えて真っ白になっていたので、過去のチャットに残っていた CSV から中身を戻した（残る1か所は中身が見つからず、断り書きにした）
- 次の図を作る時間が減る見込み：色・字・形の決まりを1つのファイル（STYLE.md と style.py）にまとめたので、新しい図やグラフを作るたびに色を考え直さなくてよい。前に1本あたり何分かかっていたかは測っていないので、これからの記事で確かめる

作業は、朝の約3時間半（最初の図解の公開が 8:33、最後が 12:02）でサイトに出した。そのうち283本は、Claude の作業を9つに分けて同時に動かし、1つのまとまりを15〜18分で仕上げた。

困っていたのは、図ごとに色の意味がばらばらだったことだ。ある図では青が「スタッフ」、別の図では青が「Unity の値」、また別の図では「今の決まり」だった。黒い枠線の太さも図ごとに違い、Google の表は消えても気づかないまま白い箱になっていた。読む人にとっては、図を1本見るたびに、その図だけの約束を覚え直すことになる。

| やったこと | 本数 | 結果 |
|---|---|---|
| 報告動画・散布図の色と字の決まりを作る | 3種類のスクリプト | STYLE.md と style.py に1本化。色は役割で決める |
| 記事の図解を直す（1回目） | 131本 | 字・地の色・白いカード・角丸・細かい斜線・役割の色 |
| 線の強さの逆転を直す | 9本 | 目盛り＜軸≦データ＜強調の順番が逆の所だけ |
| 消えた Google の表をサイトの表に戻す | 13か所 | 12か所を復元、1か所は見つからず断り書き |
| 表示中の Google の表もサイトの表に | 15か所 | シート14・ドキュメント1 |
| 記事のグラフ画像を描き直す | 7枚 | 数は元のまま、色と字だけ |
| 記事の図解を直す（2回目） | 283本 | 9つに分けて同時に作業。トップページの部品は触らない |

[図解: LOG469_color_roles.html](https://pixelstomotors.com/embeds/LOG469_color_roles.html)


## 作業ログ ― 報告動画の色を、場面ではなく役割で決める

サービスロボット導入シミュレーションでは、Claude Code が Unity で回した結果を、依頼が終わるたびに短い報告動画にしている（LOG435）。報告動画・パネル付き動画・詰まりの地図（散布図）の3種類があり、それぞれ別のスクリプトで作っていた。

> このようなレポート動画のデザインをルール化したい。ルールは他のレポートに合わせたいが、揃っていないことがあれば調べて何が問題か教えてください。

調べると、形（字幕帯・グラフの置き方）はそろっていたが、色がスクリプトごとに違っていた。グラフの Unity の線が黒のものと青のもの、札の「良くなった」が青のものと緑のもの、地図の通れない所が黄色のもの。黄色は Unity の録画でお客さんの色なので、地図で使うと読み違える。

最初は「青はスタッフの色だから、ほかでは使わない」と決めて、グラフの線と地図の待機点から青を外した。見本を見たエイイチから、こう返ってきた。

> 折れ線やグラフの青やロボットの待機所の薄い青はなぜなくしたの（見えやすかったけど）

> 「グラフでは Unity、地図では待機点。1つの画面の中では1つの意味だけ」 これはこれだけに決めちゃうと他で使えなくなってしまわない？

場面ごとに色の意味を決めると、新しい種類の図が出るたびに決まりを足すことになる。そこで、**色は場面ではなく役割で決める**ことにした。青は「その画面の主役」で、グラフなら Unity の線、地図なら待機点になる。スタッフが出る画面（Unity の録画）だけは、青をスタッフに使い、主役に青を使わない。

- ねらい：新しい種類の図でも、「何が主役か」を決めれば色が決まるようにする
- 変わったこと：色の値を style.py の1か所に置き、3つのスクリプトはそこから読む。札の色は good・bad・quiet・next の4つだけにし、ほかの名前は止まる
- 見込める効果：報告動画を作るたびに色を選ぶ手間がなくなり、読み手は前の報告と同じ読み方で次の報告を読める（見込み）

![報告動画の1枚の前と後。上の「今」は目盛りが6.9・5.1・3.4のような半端な数、下の「決まりのあと」は0・2・4・6・8のきりのいい数。Unity の線は青、サイトの試算は灰の点線](https://pixelstomotors.com/media/LOG469_01_report_graph_before_after.png)

字は、サイトと同じ組み合わせにした。日本語は Noto Sans、英数字は Inter。1つの文字列の中で、英数字と半角の記号だけ Inter、かな・漢字・全角の記号は Noto で描く。Pillow は1つの文字列に1つの字しか使えないので、文字列を英数字とそれ以外に分けて描く関数を style.py に作った。

### KIOXIA の service_sim/status_videos/tools/STYLE.md（色と字の決まり）

```markdown
# ServiceSim レポートの色と字（決まり）

報告動画・パネル付き動画・詰まりの地図（散布図）の3つと、サイトの記事の図解（article_rules.md の HTML図解）は、すべてこの色と字と形にそろえる。2026-11-08 エイイチ了承。変えるときはエイイチに確かめてから。
色と字の値は `tools/style.py` にだけ書く。3つのスクリプト（make_video.py・make_panel_video.py・make_jam_map.py）はそこから読み、スクリプトの中で色や字を決めない。

## 字

- 日本語は Noto Sans（Noto Sans CJK JP）、英数字は Inter。サイト pixelstomotors.com と同じ組み合わせ
- Inter は `tools/fonts/` に同梱（OFL）。Noto Sans CJK JP は入っている所から探し、無ければ止まる（別の字で作らない）
- 1つの文字列の中で、英数字と半角の記号だけ Inter、かな・漢字・全角の記号・①などは Noto で描く

## 下地（サイトと同じ）

| 名前 | 色 | 使う所 |
|---|---|---|
| PAPER | #f7f7f7 | 背景 |
| SHEET | #ffffff | 白い面・地図の通れる所・待機点の中 |
| INK | #242424 | 字・「次にやること」の札・字幕帯・受け渡し口の星・型D の × |
| MUTED | #555555 | 補足の字・目盛りの字 |
| FAINT | #888888 | 卓の枠と番号など、目立たせないもの |
| RULE | #e6e6e6 | 罫線・グリッド・地図の狭い道だけの所 |
| BAND | #ececec | 範囲の帯（パネルの今見ている所） |
| MAP_WALL | #c8c8c8 | 地図の通れない所 |

## 立体の色（最優先・この意味にしか使わない）

Unity の録画の立体と同じ色。ほかの意味に使わない。

| 名前 | 色 | 意味 |
|---|---|---|
| GUEST | #e8c520 | お客さん（黄） |
| STAFF | #2a55c8 | スタッフ（青）。スタッフが出る画面（録画・パネル）でだけ使う |
| ROBOT | #ffffff | ロボット（白）。白い面では INK の縁を付ける。色の説明の丸だけ #f2f2f2 |

## 役割の色：青＝その画面の主役

色は場面ごとではなく役割で決める。新しい種類の図でも、何が主役かを決めれば色が決まる。

| 名前 | 色 | 役割 |
|---|---|---|
| MAIN | #2f6fdf | その画面の主役（一番見てほしいもの）の線・点・枠。例：グラフの Unity の線、地図の待機点 |
| MAIN_SOFT | #e8f0ff | 主役の面（地図の待機点の中など） |
| QUIET | #9a9a9a | 比べる相手・基準（サイトの試算の点線など）・記録だけ・取りやめ |

- 1つの画面の中で、青に2つの意味を持たせない。青が何かは、凡例か線の横の名前で必ず書く
- 例外：スタッフが出る画面（Unity の録画・パネル付き動画）では、青はスタッフ（STAFF）。その画面では主役に青を使わず、太さや INK で示す
- 主役でないものに青を使わない。地図のスタッフの持ち場は灰の三角にする

## 意味の色（意味を固定）

| 名前 | 色 | 意味 |
|---|---|---|
| BAD | #d85a30 | 悪くなった・詰まり・待ち・止まり・助けが要る |
| GOOD | #1d9e75 | 良くなった・合格 |

- 黄はお客さんだけ。地図の塗りにも使わない

## 報告動画の札とグラフ

- 札の色は4つだけ：`good`（良くなった）・`bad`（悪くなった・残った問題）・`quiet`（記録だけ・取りやめ・別の場で）・`next`（次にやること・良し悪しのないもの。INK）。ほかの名前は止まる。札には青を使わない（線の青と混ざるため）
- 同じものに付ける札は、良し悪しで色を決める（店の番号や台数で決めない）
- グラフの線：主役（ふつうは Unity）は MAIN の実線、比べる相手（サイトの試算）は QUIET の点線。悪い値だけを示す線に BAD を使ってよい
- 縦の目盛りは 0 から、きりのいい数（1・2・2.5・5 ×10ⁿ の刻み）で 4〜5 本
- 古い spec の `acc`・`good`（線）は `unity`、`acc`（札）は `next` として読む

## 斜線と塗り（1年のページ /one-year と同じ書き方）

- 主役（今・後）は塗る。比べる相手（前・基準・試算）は、主役と同じ色の細かい斜線で描く：線2px・4px おき・右上がり（CSS は `repeating-linear-gradient(-45deg, 色 0 2px, transparent 2px 4px)`、SVG は縦の線の pattern に `patternTransform="rotate(45)"`。`45deg` は右下がりになるので使わない）
- 同じ模様（斜線どうし）の帯や面を隣り合わせに並べない。隣り合う所は、片方を薄い塗りにするか、間を白く空ける
- 同じ色の中で「斜線＝比べる相手、塗り＝主役」と分ける。灰は、記録だけ・取りやめ・試していないものに使う
- 太い斜線（7px おきなど）や縦線の模様は使わない

## 角丸：入れ物は丸く、データは四角

- 角丸にするのは入れ物と押すものだけ：カード（図の外側）と中の箱（説明の欄・数字の欄・図の下地）は 8px、ボタン・札は 4px。角丸の値はこの2つだけ
- データを表すものは角丸にしない：棒・横帯・割合の帯・斜線や塗りの面・線グラフの線と軸・目盛り線・しきい値の線・地図の壁や卓や待機点の四角。棒や帯は端の位置が値なので、丸めると値が読みにくくなる
- 線グラフの線の端は四角（butt）、曲がり角は丸（round）。点は丸（円は角丸の対象外）
- 外側の黒い枠線は使わない。カードは白い面（SHEET）で、ページの地（PAPER）から浮かせる。中の区切りは RULE の細い線か、PAPER の面にする

## 線の太さ：数字ではなく、図の中の強さの順番で決める

- 線は絵と一緒に縮む（SVG の non-scaling-stroke は使わない）。スマホでは細くなり、細かいデータが詰まって見えない
- 図の中の強さの順番：目盛り・区切り ＜ 軸 ≦ データの線 ＜ 選んだ物の囲み・強調。この順番が逆になっている所（強く見せたい線が弱い）だけを直す
- 同じ役割の線は、1つの図の中で同じ太さにする
- 細い線のほうがよいときは細くしてよい（点の多い線グラフ・細かい模様・地図の細部）。全部の図で太さの数字をそろえることはしない（2026-11-08 見本で、そろえた後のほうが重く見えた）

## 地図（散布図）

- 背景 PAPER、通れる所 SHEET、狭い道だけ RULE、通れない所 MAP_WALL
- 待機点が主役：MAIN_SOFT の四角に MAIN の破線、字も MAIN
- 詰まりの円は BAD（半透明・INK の細い縁）、型D は INK の ×
- 受け渡し口の前は INK の星。卓は FAINT。スタッフの持ち場は FAINT の三角

## 更新履歴

- 2026-11-08：線の太さの決まりを足した。数字でそろえず、図の中の強さの順番（目盛り・区切り＜軸≦データ＜選んだ物・強調）で決め、逆になっている所だけ直す（エイイチ「細い線の方がいいときはある」）
- 2026-11-08：新設（Noto Sans＋Inter、サイトの下地、立体の色、意味の色）
- 2026-11-08：斜線と塗り（1年のページの書き方）と、角丸の決まり（入れ物は丸く、データは四角）を足した（エイイチ了承）。記事の図解にも同じ決まりを使う
- 2026-11-08：青を「その画面の主役」の役割に戻した。スタッフ専用にすると、グラフの Unity の線と地図の待機点が見えにくくなったため（エイイチ「青は見えやすかった」）。場面ごとに意味を決めると新しい図で使えなくなるので、役割で決める形にした
- 2026-11-08：斜線の向きを「右上がり」と書き、同じ模様を隣に並べない決まりを足した（エイイチの指示。LOG468 の図解を右下がりで作ったため）
```

### KIOXIA の service_sim/status_videos/tools/style.py（色と字の値・字を分けて描く関数）

```python
"""ServiceSim のレポート（報告動画・パネル付き動画・詰まりの地図）の色と字。決まりは STYLE.md（2026-11-08 エイイチ了承）。
3つのスクリプトはここから色と字を読む。スクリプトの中で色や字を決めない。変えるときはエイイチに確かめてから。
字：日本語は Noto Sans（Noto Sans CJK JP）、英数字は Inter（サイト pixelstomotors.com と同じ組み合わせ）。
"""
import os

HERE = os.path.dirname(os.path.abspath(__file__))

# ---- 下地（サイトの Base.astro と同じ）----
PAPER = "#f7f7f7"   # 背景
SHEET = "#ffffff"   # 白い面
INK = "#242424"     # 字・Unity の線・「次にやること」の札・字幕帯
MUTED = "#555555"   # 補足の字
FAINT = "#888888"   # 目盛り・卓の枠など、目立たせない線と字
RULE = "#e6e6e6"    # 罫線・グリッド
BAND = "#ececec"    # 範囲の帯（パネルの「今見ている所」など）
MAP_WALL = "#c8c8c8"  # 地図の通れない所（狭い道だけの所は RULE、通れる所は SHEET）

# ---- 立体の色（Unity の録画と同じ。最優先。この3つの意味にしか使わない）----
GUEST = "#e8c520"   # お客さん＝黄
STAFF = "#2a55c8"   # スタッフ＝青
ROBOT = "#ffffff"   # ロボット＝白（白い面に置くときは INK の縁を付ける）
ROBOT_SWATCH = "#f2f2f2"  # 色の説明の丸だけ（白だと背景に溶けるため）

# ---- 役割の色 ----
# 青＝その画面の主役（一番見てほしいもの）。何が主役かは図ごとに決め、凡例か線の横の名前で必ず書く。
# ただしスタッフが出る画面（録画・パネル）では青はスタッフ。その画面では主役に青を使わない。1つの画面で青に2つの意味を持たせない
MAIN = "#2f6fdf"       # 主役の線・点・枠
MAIN_SOFT = "#e8f0ff"  # 主役の面（地図の待機点など）

# ---- 意味の色（意味を固定）----
BAD = "#d85a30"     # 悪い・詰まり・待ち・止まり・助けが要る
GOOD = "#1d9e75"    # 良くなった・合格
QUIET = "#9a9a9a"   # 比べる相手・基準（サイトの試算の点線など）・記録だけ・取りやめ

# 報告動画の札：意味の名前 → 色。ここにない名前は使わない
TAG = {"good": GOOD, "bad": BAD, "quiet": QUIET, "next": INK}
# グラフの線：主役（ふつうは Unity）は MAIN の実線、比べる相手（サイトの試算）は QUIET の点線
SERIES = {"unity": MAIN, "main": MAIN, "site": QUIET, "bad": BAD}
# 古い spec の名前の読み替え（acc は青だった。良し悪しのない札・次にやることなので next にする）
TAG_ALIAS = {"acc": "next"}
SERIES_ALIAS = {"acc": "unity", "good": "unity"}

# ---- 字 ----
INTER = os.path.join(HERE, "fonts", "Inter-Regular.ttf")
INTER_B = os.path.join(HERE, "fonts", "Inter-Bold.ttf")
_NOTO_R = ["/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
           os.path.expanduser("~/Library/Fonts/NotoSansCJKjp-Regular.otf"),
           os.path.expanduser("~/Library/Fonts/NotoSansCJK-Regular.ttc"),
           "/Library/Fonts/NotoSansCJKjp-Regular.otf"]
_NOTO_B = ["/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
           os.path.expanduser("~/Library/Fonts/NotoSansCJKjp-Bold.otf"),
           os.path.expanduser("~/Library/Fonts/NotoSansCJK-Bold.ttc"),
           "/Library/Fonts/NotoSansCJKjp-Bold.otf"]


def _first(paths, what):
    for p in paths:
        if os.path.exists(p):
            return p
    raise SystemExit("字が見つからない（%s）。Noto Sans CJK JP を入れてから作る: %s" % (what, paths))


NOTO = _first(_NOTO_R, "Noto Sans CJK JP Regular")
NOTO_B = _first(_NOTO_B, "Noto Sans CJK JP Bold")
for _p in (INTER, INTER_B):
    if not os.path.exists(_p):
        raise SystemExit("字が見つからない: %s" % _p)

CSS_FONT = "'Inter','Noto Sans CJK JP','Noto Sans JP',sans-serif"
# Inter は英数字と半角の記号だけ（unicode-range）。①・→ などは Noto で描き、Pillow の is_latin と同じ分け方にする
_RANGE = "unicode-range:U+0000-00D6,U+00D8-024F"
CSS_FACE = ("@font-face{font-family:'Inter';font-weight:400;src:url('file://%s');%s}"
            "@font-face{font-family:'Inter';font-weight:700;src:url('file://%s');%s}") % (INTER, _RANGE, INTER_B, _RANGE)


def is_latin(ch):
    """英数字と半角の記号は Inter。それ以外（かな・漢字・全角の記号・①など）は Noto"""
    return ord(ch) < 0x250 and ch != "×"


# ---- Pillow：1つの文字列を Inter と Noto に分けて描く ----
_cache = {}


def _font(size, bold, latin):
    from PIL import ImageFont
    k = (size, bold, latin)
    if k not in _cache:
        path = (INTER_B if bold else INTER) if latin else (NOTO_B if bold else NOTO)
        _cache[k] = ImageFont.truetype(path, size, index=0)
    return _cache[k]


def _runs(s):
    out = []
    for ch in s:
        lat = is_latin(ch)
        if out and out[-1][0] == lat:
            out[-1][1] += ch
        else:
            out.append([lat, ch])
    return out


def text_width(s, size, bold=False):
    return sum(_font(size, bold, lat).getlength(t) for lat, t in _runs(s))


def text(d, xy, s, size, fill, bold=False, anchor="la"):
    """d.text の代わり。anchor は横 l/m/r と縦 a/m/s（Pillow と同じ書き方）。高さは Noto の字で決める"""
    x, y = xy
    w = text_width(s, size, bold)
    h, v = anchor[0], anchor[1]
    x = x - (w / 2 if h == "m" else w if h == "r" else 0)
    asc, desc = _font(size, bold, False).getmetrics()
    base = y + (asc if v == "a" else (asc - desc) / 2 if v == "m" else 0)
    for lat, t in _runs(s):
        f = _font(size, bold, lat)
        d.text((x, base), t, font=f, fill=fill, anchor="ls")
        x += f.getlength(t)


# ---- matplotlib ----
def mpl_setup():
    import matplotlib
    from matplotlib import font_manager as fm
    for p in (INTER, INTER_B, NOTO, NOTO_B):
        fm.fontManager.addfont(p)
    jp = fm.FontProperties(fname=NOTO).get_name()
    matplotlib.rcParams["font.family"] = ["Inter", jp]   # 英数字は Inter、無い字は Noto（matplotlib 3.6 以降）
    matplotlib.rcParams["text.color"] = INK
    matplotlib.rcParams["axes.labelcolor"] = INK
    matplotlib.rcParams["axes.edgecolor"] = FAINT
    matplotlib.rcParams["xtick.color"] = MUTED
    matplotlib.rcParams["ytick.color"] = MUTED
    matplotlib.rcParams["figure.facecolor"] = PAPER
    matplotlib.rcParams["savefig.facecolor"] = PAPER
```

### KIOXIA の service_sim/status_videos/tools/make_jam_map.py（詰まりの地図を決まった形で描く）

```python
#!/usr/bin/env python3
# ServiceSim 詰まりの地図（散布図）を、決まった形で描く。形の決まりは JAM_MAP_FORMAT.md（2026-11-08 エイイチ了承）。
# 形は依頼119 の fig119.py（fig_where_by_type_sb119.png）に固定した。色と字は 2026-11-08 に style.py へ移した。手で別の形を作らない。
#
# 使い方：
#   python3 make_jam_map.py out.png --layout-dir DIR --store p6_large_center \
#     --set "EP.csv|RUN_GLOB|並べる回の名前" --set "EP2.csv|RUN_GLOB2|名前2" \
#     --note "⑧5台・すぐ押す（種0〜9 の 10 回）"
#   EP.csv   … agg*.py が作る ep_<label>_r5.csv（列：run,dur_s,x,y,type …）
#   RUN_GLOB … その回のフォルダ（stops.csv・summary.json がある所）を指す glob
#   --view x0,x1,y0,y1 … 見る範囲（m）。無ければ待機点の外側から 左2.4・右1.3・下1.95・上0.85 m
import argparse, base64, csv, glob, gzip, json, os, statistics as stt, sys
from collections import defaultdict
import numpy as np
import matplotlib
matplotlib.use('Agg')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import style as st   # 色と字は style.py（決まりは STYLE.md）。ここで色や字を決めない（2026-11-08）
st.mpl_setup()        # 日本語は Noto Sans、英数字は Inter
import matplotlib.pyplot as plt
import matplotlib.patches as mp

# ---- 形（変えるときはエイイチに確かめる）----
PANEL_W, PANEL_H = 9.5, 7.0          # 1枚あたりのインチ（2枚で 19x7）
DPI = 120
MIN_S = 30                           # 30 秒以上の詰まりだけ
RED = st.BAD       # 詰まり＝悪い色
def size(sec): return max(8, sec / 3)  # 円の大きさ＝詰まりの秒（秒/3・最小 8）
LEGEND_SEC = (30, 300, 900)
MARGIN = (2.4, 1.3, 1.95, 0.85)      # 左・右・下・上（m）

def _rgb(h): return tuple(int(h[i:i + 2], 16) / 255 for i in (1, 3, 5))
WALL, NARROW = _rgb(st.MAP_WALL), _rgb(st.RULE)   # 地図の灰2段
FREE = _rgb(st.SHEET)                           # 通れる所＝白


def load_layout(ldir, store):
    lay = json.load(open(os.path.join(ldir, store + '.json'), encoding='utf-8'))
    m = json.load(open(os.path.join(ldir, store + '_map.json'), encoding='utf-8'))
    fc = np.frombuffer(gzip.decompress(base64.b64decode(m['free_class'])), dtype=np.uint8).reshape(m['ny'], m['nx'])
    return lay, (m['res'], m['nx'], m['ny'], fc)


def draw_base(ax, lay, mp_, view):
    x0, x1, y0, y1 = view
    res, nx, ny, fc = mp_
    img = np.empty(fc.shape + (3,)); img[:] = FREE
    img[fc == 0] = WALL                    # 通れない＝濃い灰
    img[fc == 1] = NARROW                  # 狭い道だけ＝薄い灰（黄はお客さんの色なので使わない）
    ax.imshow(img, origin='lower', extent=(0, nx * res, 0, ny * res), interpolation='nearest')
    for t in lay['tables']:
        f = t['footprint']
        if f[2] < x0 - 1 or f[0] > x1 + 1 or f[3] < y0 - 1 or f[1] > y1 + 1:
            continue
        ax.add_patch(mp.Rectangle((f[0], f[1]), f[2] - f[0], f[3] - f[1], fc='none', ec=st.FAINT, lw=0.8))
        ax.text((f[0] + f[2]) / 2, (f[1] + f[3]) / 2, t['id'], ha='center', va='center', fontsize=6, color=st.FAINT, clip_on=True)
    for i, r in enumerate(p['rect'] for p in lay['places'] if p['kind'] == 'RobotStandby'):
        ax.add_patch(mp.Rectangle((r[0], r[1]), r[2] - r[0], r[3] - r[1], fc=st.MAIN_SOFT, ec=st.MAIN, lw=0.8, ls='--'))   # 待機点＝この図の主役
        ax.text((r[0] + r[2]) / 2, r[3] + 0.05, '待機点%d' % (i + 1), ha='center', va='bottom', fontsize=6, color=st.MAIN)
    ps = next(p['point'] for p in lay['places'] if p['kind'] == 'FoodPass')
    ax.plot([ps[0]], [ps[1]], marker='*', ms=11, color=st.INK)
    ax.text(ps[0], ps[1] + 0.15, '受け渡し口の前', ha='center', va='bottom', fontsize=7, color=st.INK)
    for p in lay['places']:
        if p['kind'] == 'StaffPost' and x0 <= p['point'][0] <= x1 and y0 <= p['point'][1] <= y1:
            ax.plot([p['point'][0]], [p['point'][1]], marker='^', ms=4, color=st.FAINT)   # スタッフの持ち場（青は主役に使うので灰の三角）
    ax.set_xlim(x0, x1); ax.set_ylim(y0, y1); ax.set_aspect('equal')
    ax.set_xlabel('x（m）'); ax.set_ylabel('y（m）')


def med(xs):
    xs = [x for x in xs if x is not None]
    return stt.median(xs) if xs else None


def fmt(v, f):
    return '—' if v is None else f(v)


def main():
    a = argparse.ArgumentParser()
    a.add_argument('out')
    a.add_argument('--layout-dir', required=True)
    a.add_argument('--store', default='p6_large_center')
    a.add_argument('--set', action='append', required=True, help='EP.csv|RUN_GLOB|名前')
    a.add_argument('--note', required=True, help='下の説明の頭（店・台数・回数）')
    a.add_argument('--view')
    o = a.parse_args()

    lay, mp_ = load_layout(o.layout_dir, o.store)
    if o.view:
        view = tuple(float(v) for v in o.view.split(','))
    else:
        R = [p['rect'] for p in lay['places'] if p['kind'] == 'RobotStandby']
        view = (min(r[0] for r in R) - MARGIN[0], max(r[2] for r in R) + MARGIN[1],
                min(r[1] for r in R) - MARGIN[2], max(r[3] for r in R) + MARGIN[3])
    sets = [s.split('|', 2) for s in o.set]
    for s in sets:
        if len(s) != 3:
            sys.exit('--set は "EP.csv|RUN_GLOB|名前" の形で: %s' % s)

    n = len(sets)
    nrun = None
    fig, axs = plt.subplots(1, n, figsize=(PANEL_W * n, PANEL_H), squeeze=False)
    axs = axs[0]
    for ax, (ep, rg, name) in zip(axs, sets):
        R = [r for r in csv.DictReader(open(ep, encoding='utf-8')) if float(r['dur_s']) >= MIN_S]
        nd = [r for r in R if not r['type'].startswith('D')]
        dd = [r for r in R if r['type'].startswith('D')]
        dirs = sorted(glob.glob(rg))
        nrun = nrun or len(dirs)
        if not dirs:
            sys.exit('回のフォルダが見つからない: %s' % rg)
        per = defaultdict(float)
        for r in nd:
            per[r['run']] += float(r['dur_s'])
        jam = med([per[os.path.basename(d)] for d in dirs])
        stop = med([sum(float(x['duration_s']) for x in csv.DictReader(open(d + '/stops.csv', encoding='utf-8'))) for d in dirs])
        S = [json.load(open(d + '/summary.json', encoding='utf-8')) for d in dirs]
        ent = med([s.get('site_metrics', {}).get('entry_wait_mean') for s in S])
        call = med([s.get('stuck_rescue', {}).get('calls') for s in S])
        draw_base(ax, lay, mp_, view)
        ax.scatter([float(r['x']) for r in nd], [float(r['y']) for r in nd], s=[size(float(r['dur_s'])) for r in nd],
                   c=RED, alpha=0.55, edgecolors=st.INK, linewidths=0.3, zorder=5)
        if dd:
            ax.scatter([float(r['x']) for r in dd], [float(r['y']) for r in dd], s=60, marker='x', c=st.INK, zorder=6,
                       label='抜けられなかった行ったり来たり（型D）%d 件・%.0f 秒（合計に入れない）' % (len(dd), sum(float(r['dur_s']) for r in dd)))
            ax.legend(fontsize=7, loc='lower left', framealpha=0.9)
        tot = sum(float(r['dur_s']) for r in nd)
        ax.set_title('%s\n待機場所のそばの30秒以上の詰まり：%d 件・%s 秒（%d 回の和）\n1回あたり %s 秒\nロボットの止まりの合計：1回あたり %s 秒\n30秒の呼び出し：1回あたり %s 回　入店待ち：%s 分'
                     % (name, len(nd), format(round(tot), ','), len(dirs), fmt(jam, lambda v: format(round(v), ',')),
                        fmt(stop, lambda v: format(round(v), ',')), fmt(call, lambda v: '%.1f' % v), fmt(ent, lambda v: '%.2f' % v)),
                     fontsize=13, weight='bold')
    for s_ in LEGEND_SEC:
        axs[-1].scatter([], [], s=size(s_), c=RED, alpha=0.55, edgecolors=st.INK, linewidths=0.3, label='円の大きさ：%d 秒' % s_)
    axs[-1].legend(fontsize=7, loc='lower left', framealpha=0.9)
    fig.suptitle('%s：待機場所のそばの、ロボット同士の 30 秒以上の詰まりの始めの位置（止まった台の位置・赤い円の大きさ＝詰まりの秒）。1回あたりの数は %d 回の中央値' % (o.note, nrun),
                 fontsize=10, y=0.02)
    fig.savefig(o.out, dpi=DPI, bbox_inches='tight'); plt.close(fig)
    print(o.out)


if __name__ == '__main__':
    main()
```

### KIOXIA の service_sim/status_videos/tools/JAM_MAP_FORMAT.md（詰まりの地図の形の決まり）

```markdown
# ServiceSim 詰まりの地図（散布図）の形（決まり）

報告に付ける「待機場所のそばの詰まりの地図」は、すべてこの形にそろえる。2026-11-08 エイイチ了承（依頼119 の fig_where_by_type_sb119.png の見た目を標準にした）。変えるときはエイイチに確かめてから。
作り方：`tools/make_jam_map.py out.png --layout-dir <店の形のフォルダ> --store p6_large_center --set "ep.csv|回のフォルダの glob|並べる回の名前" --set ... --note "⑧5台・すぐ押す（種0〜9 の 10 回）"`
（形はこのスクリプトに固定してある。依頼ごとに fig*.py を写して手で直さない）

- 並べ方：比べる回を横に並べる（1枚 9.5x7 インチ・120dpi。2つなら 19x7）
- 下地：地図（通れる所＝白・狭い道だけ＝薄い灰・通れない＝濃い灰）、卓（薄い灰の枠と番号）、待機点（主役：薄い青の四角に青の破線と「待機点N」）、受け渡し口の前（黒い星）、スタッフの持ち場（灰の小さい三角）
- 見る範囲：待機点の外側から 左2.4・右1.3・下1.95・上0.85 m（⑧では x 16.3〜24.8・y 9.0〜12.6）。別の場所を見るときだけ --view
- 点：30 秒以上のロボット同士の詰まりの始めの位置。BAD の円（半透明・INK の細い縁）、円の大きさ＝詰まりの秒（秒/3・最小 8）。右端の図に「円の大きさ：30・300・900 秒」の見本
- 型D（抜けられなかった行ったり来たり）は INK の × で別に描き、合計に入れない（件数と秒は左下の説明に書く）
- 図の上（太字・13pt）：回の名前 → 待機場所のそばの30秒以上の詰まり：件数・秒（N 回の和）→ 1回あたりの秒 → ロボットの止まりの合計：1回あたりの秒 → 30秒の呼び出し：1回あたりの回数・入店待ち：分
- 「1回あたり」はすべて回の中央値。数字の元は回のフォルダの stops.csv・summary.json と、agg*.py が作る ep_<label>_r5.csv
- 図の下（10pt）：「店・台数・回数：何の図か。1回あたりの数は N 回の中央値」
- 色と字：`tools/STYLE.md`（値は `tools/style.py`）。日本語は Noto Sans、英数字は Inter（2026-11-08。それまでは Hiragino・独自の色だった）
```

### KIOXIA の service_sim/status_videos/tools/make_video.py（報告動画）

```python
"""ServiceSim の報告動画を作る。spec.json（スライドの並び）→ 1920x1080 の mp4。
使い方: python3 make_video.py spec.json out.mp4
スライドの種類: title / chart / rows / steps
"""
import json, sys, subprocess, html, pathlib, tempfile, math, os
from playwright.sync_api import sync_playwright
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import style as st   # 色と字は style.py（決まりは STYLE.md）。ここで色を決めない

W, H = 1920, 1080
INK, QUIET, LINE, BG = st.INK, st.MUTED, st.RULE, st.PAPER
E = html.escape


def tag_color(c):
    c = st.TAG_ALIAS.get(c or "next", c or "next")
    if c not in st.TAG:
        raise SystemExit("札の色 %r は使えない。good / bad / quiet / next のどれか" % c)
    return st.TAG[c]


def series_color(c):
    c = st.SERIES_ALIAS.get(c or "unity", c or "unity")
    if c not in st.SERIES:
        raise SystemExit("線の色 %r は使えない。unity（main）/ site / bad のどれか" % c)
    return st.SERIES[c], c == "site"


def nice_step(v):
    """v 以上で一番小さい 1・2・2.5・5 ×10^n"""
    if v <= 0:
        return 1.0
    e = 10 ** math.floor(math.log10(v))
    for m in (1, 2, 2.5, 5, 10):
        if v <= m * e + 1e-12:
            return m * e


def ticks(top):
    """目盛りはきりのいい数で 4〜5 本（0 から）。上端は目盛りの数に切り上げる"""
    step = nice_step(top / 4)
    n = math.ceil(top / step - 1e-9)
    return [round(step * k, 6) for k in range(n + 1)]

CSS = f"""
*{{margin:0;padding:0;box-sizing:border-box}}
{st.CSS_FACE}
body{{width:{W}px;height:{H}px;background:{BG};font-family:{st.CSS_FONT};color:{INK};position:relative;overflow:hidden}}
.top{{position:absolute;left:96px;top:64px;font-size:30px;color:{QUIET}}}
h1{{position:absolute;left:96px;top:120px;right:96px;font-size:60px;font-weight:700;line-height:1.3}}
.main{{position:absolute;left:96px;right:96px;top:260px;bottom:220px}}
.sub{{position:absolute;left:0;right:0;bottom:0;height:180px;background:{INK};color:#fff;font-size:40px;line-height:1.5;padding:30px 120px;display:flex;align-items:center;justify-content:center;text-align:center}}
.row{{display:flex;align-items:center;gap:28px;font-size:38px;line-height:1.4;margin-bottom:30px}}
.tag{{flex:none;min-width:150px;padding:8px 20px;border-radius:10px;font-size:30px;font-weight:700;text-align:center;color:#fff}}
.note{{font-size:28px;color:{QUIET};margin-top:16px}}
"""

def svg_chart(s):
    xs = s["x"]; series = s["series"]; unit = s.get("unit", "")
    cw, ch = 1728, 500; l, r, t, b = 120, 360, 56, 70
    vals = [v for se in series for v in se["values"] if v is not None]
    tk = ticks(s.get("ymax") or max(vals) * 1.1)
    vmax = tk[-1]
    pad = 48   # 最初の点を目盛りの字から離す
    X = lambda i: l + pad + i * (cw - l - r - pad) / (len(xs) - 1)
    Y = lambda v: t + (ch - t - b) * (1 - v / vmax)
    out = [f'<svg width="{cw}" height="{ch}" xmlns="http://www.w3.org/2000/svg" font-family="{st.CSS_FONT.replace(chr(39), "")}">']
    for v in tk:
        y = Y(v)
        out.append(f'<line x1="{l}" x2="{cw-r}" y1="{y}" y2="{y}" stroke="{LINE}"/>')
        out.append(f'<text x="{l-16}" y="{y+10}" font-size="26" fill="{QUIET}" text-anchor="end">{v:g}</text>')
    for i, x in enumerate(xs):
        out.append(f'<text x="{X(i)}" y="{ch-24}" font-size="28" fill="{QUIET}" text-anchor="middle">{E(x)}</text>')
    for se in series:
        c, is_site = series_color(se.get("color"))
        dash = ' stroke-dasharray="12 10"' if (se.get("dash") or is_site) else ""
        pts = [(X(i), Y(v)) for i, v in enumerate(se["values"]) if v is not None]
        out.append(f'<polyline points="{" ".join(f"{a},{b2}" for a, b2 in pts)}" fill="none" stroke="{c}" stroke-width="6"{dash}/>')
        for i, v in enumerate(se["values"]):
            if v is None: continue
            out.append(f'<circle cx="{X(i)}" cy="{Y(v)}" r="10" fill="{c}"/>')
            if se.get("labels", True):
                out.append(f'<text x="{X(i)}" y="{Y(v)+(46 if se.get("below") else -22)}" font-size="30" font-weight="700" fill="{c}" text-anchor="middle">{v:.{s.get("dp",1)}f}</text>')
        lx, ly = pts[-1]
        out.append(f'<text x="{lx+30}" y="{ly+(44 if se.get("below") else -6)}" font-size="32" font-weight="700" fill="{c}">{E(se["name"])}</text>')
    out.append(f'<text x="{l}" y="{t-28}" font-size="26" fill="{QUIET}">{E(unit)}</text></svg>')
    return "".join(out)

def body(s):
    if s["kind"] == "title":
        return f'<h1 style="top:300px;font-size:76px">{E(s["title"])}</h1><div class="main" style="top:520px;font-size:40px;color:{QUIET};line-height:1.6">{"<br>".join(E(x) for x in s.get("lines", []))}</div>'
    h = f'<h1>{E(s["title"])}</h1>'
    if s["kind"] == "chart":
        note = f'<div class="note">{E(s["note"])}</div>' if s.get("note") else ""
        return h + f'<div class="main">{svg_chart(s)}{note}</div>'
    if s["kind"] in ("rows", "steps"):
        rows = "".join(f'<div class="row"><span class="tag" style="background:{tag_color(r.get("c"))}">{E(r["tag"])}</span><span>{E(r["text"])}</span></div>' for r in s["rows"])
        return h + f'<div class="main">{rows}</div>'
    raise ValueError(s["kind"])

def main(spec_path, out):
    spec = json.load(open(spec_path, encoding="utf-8"))
    tmp = pathlib.Path(tempfile.mkdtemp())
    segs = []
    with sync_playwright() as p:
        br = p.chromium.launch(); pg = br.new_page(viewport={"width": W, "height": H})
        for i, s in enumerate(spec["slides"]):
            doc = f'<html><head><meta charset="utf-8"><style>{CSS}</style></head><body><div class="top">{E(spec["header"])}　{i+1}/{len(spec["slides"])}</div>{body(s)}<div class="sub">{E(s["sub"])}</div></body></html>'
            pg.set_content(doc); pg.wait_for_timeout(150)
            png = tmp / f"s{i:02d}.png"; pg.screenshot(path=str(png))
            seg = tmp / f"s{i:02d}.mp4"
            subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-loop", "1", "-i", str(png), "-t", str(s.get("sec", 10)),
                            "-vf", "fade=in:0:8,format=yuv420p", "-r", "30", "-c:v", "libx264", "-preset", "veryfast", str(seg)], check=True)
            segs.append(seg)
        br.close()
    lst = tmp / "list.txt"; lst.write_text("".join(f"file '{x}'\n" for x in segs))
    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", str(lst), "-c", "copy", out], check=True)
    print(out)

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])
```

### KIOXIA の service_sim/status_videos/tools/make_panel_video.py（パネル付き動画）

```python
"""ServiceSim の録画に、店の様子のパネル（timeline.csv）を横に付ける。
使い方: python3 make_panel_video.py rec.mp4 frames.csv timeline.csv out.mp4 [--from 秒] [--to 秒] [--speed 倍] [--title 文字]
録画は 1コマ = frames.csv の t_s（店の時刻・秒）。パネルは timeline.csv の t_s ごと（60秒おき）に描き直す。
"""
import csv, sys, argparse, subprocess, tempfile, pathlib, bisect
from PIL import Image, ImageDraw

W, H, PW = 1920, 1080, 560          # 全体・パネルの幅
VW = W - PW                          # 録画の幅
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import style as st   # 色と字は style.py（決まりは STYLE.md）。ここで色や字を決めない（2026-11-08）
BG, INK, QUIET, LINE = st.PAPER, st.INK, st.MUTED, st.RULE
BAD = st.BAD
T = st.text                           # 日本語は Noto Sans、英数字は Inter

PAD = "#66605b"                       # --fit の余白（Unity の背景に近い色。録画側の色なので style には入れない）
STAFF_C, ROBOT_C = st.STAFF, st.ROBOT   # 立体の色に合わせる（スタッフ青・ロボット白）
STAFF = [("staff_guide", "案内"), ("staff_serve", "料理"), ("staff_call", "呼び出し"),
         ("staff_pay", "会計"), ("staff_clear", "片付け"), ("staff_rescue", "ロボットの助け"),
         ("staff_idle", "手すき")]

def hm(t):
    t = int(t); h, m = t // 3600, t % 3600 // 60
    return f"{h}時間{m:02d}分" if h else f"{m}分"

def panel(row, rows, robots, t_from, t_to, title):
    im = Image.new("RGB", (PW, H), BG); d = ImageDraw.Draw(im)
    x0 = 40
    T(d, (x0, 36), title, 26, QUIET)
    T(d, (x0, 76), f"開店から {hm(row['t_s'])}", 52, INK, bold=True)
    y = 170
    # スタッフ
    n = sum(int(row[k]) for k, _ in STAFF)
    T(d, (x0, y), f"スタッフ {n}人 がしていること", 28, INK, bold=True); y += 52
    for k, name in STAFF:
        v = int(row[k])
        T(d, (x0, y), name, 26, INK if v else QUIET)
        for i in range(v):
            d.rounded_rectangle((x0 + 210 + i * 46, y + 4, x0 + 246 + i * 46, y + 38), 6, fill=STAFF_C)
        y += 42
    y += 20
    # ロボット
    rb = int(row["robots_busy"])
    # 下げ膳にも使う回（timeline.csv に robots_bussing の列がある回）だけ、動いている台のうち下げ膳の台を「下げ膳」と出す（2026-11-04・依頼91）
    bus = min(int(row.get("robots_bussing") or 0), rb)
    T(d, (x0, y), f"ロボット {robots}台", 28, INK, bold=True); y += 52
    # 1段に3台まで（パネルの幅 560px に収める。4台以上は段を足す。2026-11-08）
    for i in range(robots):
        busy = i < rb
        bx, by = x0 + (i % 3) * 160, y + (i // 3) * 62
        d.rounded_rectangle((bx, by, bx + 146, by + 50), 10, fill=ROBOT_C if busy else BG, outline=INK if busy else LINE, width=3)
        T(d, (bx + 73, by + 25), ("下げ膳" if i >= rb - bus else "運んでいる") if busy else "待機", 24, INK if busy else QUIET, bold=True, anchor="mm")
    if robots == 0:
        T(d, (x0, y + 8), "入れていない", 26, QUIET)
    y += 62 * max(0, (robots - 1) // 3)
    y += 76
    # 数字
    for k, name, c in [("waiting_groups", "入口で待つ組", BAD), ("seated_groups", "席にいる組", INK),
                       ("tables_dirty_untaken", "片付けを待つ卓", BAD)]:
        v = int(row[k])
        T(d, (x0, y + 8), name, 28, INK)
        T(d, (PW - 50, y), str(v), 48, c if v else QUIET, bold=True, anchor="ra")
        y += 60
    # 入口で待つ組の一日
    y += 14; gx0, gx1 = x0, PW - 40
    T(d, (x0, y), "入口で待つ組（営業の始めから終わり）", 22, QUIET); y += 36
    gh = min(120, H - 24 - y)   # 下の端からはみ出さない高さ（ロボットが4台以上で段が増えたとき）
    if gh < 50:
        raise SystemExit("パネルが縦に収まらない（グラフの高さ %d px）。形を見直す" % gh)
    ts = [r["t_s"] for r in rows]; vs = [int(r["waiting_groups"]) for r in rows]
    vmax = max(max(vs), 1); tmax = ts[-1] or 1
    X = lambda t: gx0 + (gx1 - gx0) * t / tmax; Y = lambda v: y + gh - gh * v / vmax
    d.rectangle((X(t_from), y, X(t_to), y + gh), fill=st.BAND)
    d.line([(X(t), Y(v)) for t, v in zip(ts, vs)], fill=BAD, width=3)
    d.line((gx0, y + gh, gx1, y + gh), fill=LINE, width=2)
    cx = X(row["t_s"]); d.line((cx, y, cx, y + gh), fill=INK, width=3)
    return im

def legend(path):
    im = Image.new("RGBA", (560, 64), (247, 247, 247, 230)); d = ImageDraw.Draw(im); x = 20
    for c, name in [(st.GUEST, "お客さん"), (st.STAFF, "スタッフ"), (st.ROBOT_SWATCH, "ロボット")]:
        d.ellipse((x, 18, x + 28, 46), fill=c, outline=st.MUTED, width=2); T(d, (x + 38, 32), name, 26, INK, anchor="lm")
        x += 70 + 30 * len(name)
    im.save(path)

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("rec"); ap.add_argument("frames"); ap.add_argument("timeline"); ap.add_argument("out")
    ap.add_argument("--from", dest="t_from", type=float, default=0); ap.add_argument("--to", dest="t_to", type=float, default=None)
    ap.add_argument("--speed", type=float, default=1.0); ap.add_argument("--title", default="ServiceSim")
    ap.add_argument("--fit", action="store_true", help="録画を切らずに横幅に合わせて縮め、上下に余白を足す（字幕の入った寄りの部品用）")
    a = ap.parse_args()
    fr = [(int(r["frame"]), float(r["t_s"])) for r in csv.DictReader(open(a.frames))]
    rows = []
    for r in csv.DictReader(open(a.timeline)):
        r = dict(r); r["t_s"] = float(r["t_s"]); rows.append(r)
    robots = int(rows[0]["condition_id"].split("_r")[1].split("_")[0]) if "_r" in rows[0]["condition_id"] else 0
    t_to = a.t_to if a.t_to is not None else fr[-1][1]
    fps = 30
    f_from = next(f for f, t in fr if t >= a.t_from); f_to = max(f for f, t in fr if t <= t_to)
    v_from, v_dur = f_from / fps, (f_to - f_from) / fps
    tmp = pathlib.Path(tempfile.mkdtemp())
    # パネル：timeline の行ごと。その行が見える録画の長さ（速さで割る）だけ出す
    sel = [r for r in rows if a.t_from - 60 < r["t_s"] <= t_to]
    lst = []
    for i, r in enumerate(sel):
        t0 = max(r["t_s"], a.t_from); t1 = sel[i + 1]["t_s"] if i + 1 < len(sel) else t_to
        if t1 <= t0: continue
        dur = (t1 - t0) / 0.5 / fps / a.speed
        p = tmp / f"p{i:04d}.png"; panel(r, rows, robots, a.t_from, t_to, a.title).save(p)
        lst.append(f"file '{p}'\nduration {dur:.4f}\n")
    lst.append(f"file '{p}'\n")
    (tmp / "p.txt").write_text("".join(lst))
    legend(tmp / "lg.png")
    vf = (f"[0:v]trim=start={v_from}:duration={v_dur},setpts=(PTS-STARTPTS)/{a.speed},"
          + (f"scale={VW}:-2,pad={VW}:{H}:0:(oh-ih)/2:color={PAD}[v0];" if a.fit else f"scale=-2:{H},crop={VW}:{H}[v0];") +
          f"[2:v]format=rgba[lg];[v0][lg]overlay=32:32[v];"
          f"[1:v]fps={fps},setpts=PTS-STARTPTS[p];[v][p]hstack=inputs=2,format=yuv420p")
    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", a.rec, "-f", "concat", "-safe", "0", "-i", str(tmp / "p.txt"), "-loop", "1", "-i", str(tmp / "lg.png"),
                    "-filter_complex", vf, "-t", f"{v_dur / a.speed:.3f}", "-r", str(fps),
                    "-c:v", "libx264", "-preset", "veryfast", "-crf", "22", a.out], check=True)
    print(a.out, f"{v_dur / a.speed:.1f}s")

if __name__ == "__main__":
    main()
```


## 作業ログ ― 記事の図解131本を、見本を見てから直す

報告動画の決まりができたところで、同じ決まりをサイトの記事の図解（HTML で作った、動く図）にも使うことにした。

> 現行サイトの記事内に使われているグラフやレポート解説図などデザインルール化で決めた内容で作り直して掲載し直してください。

ここで1つ失敗した。130本を字と色だけ置き換えて、見本を見せる前に公開してしまった。エイイチから「見た目が変わる所、一旦どのような見た目になるのか先に見せてください」と言われ、ここからは**見本を2〜3本作って見てもらってから全部に広げる**流れに変えた。直す前の130本は、iCloud の site_archive/embeds_before_style_20261108/ に tar でまとめて残してある。

見本を見たエイイチの指摘で、形の決まりが3つ増えた。

> 周りの枠線いる？角丸とか、色とかも新しいルールに揃えていいんだけど。

> https://pixelstomotors.com/one-year このページのグラフの書き方を取り入れることはできますか？（斜線の描き方とか）

> 角丸と角丸がないのと（例えば線グラフとか）を分けるルールを作れますか？

1. 外側の黒い枠線をやめ、白いカード（角丸8px）を地の灰（#f7f7f7）から浮かせる
2. 比べる相手（前・基準・試算）は、主役と同じ色の細かい斜線（線2px・4px おき）で描く。主役は塗る。1年のページ（/one-year）と同じ書き方
3. 角丸にするのは入れ物（カード・箱は8px）と押すもの（ボタン・札は4px）だけ。棒・帯・線・軸・地図の形は四角のまま。棒の端の位置が値なので、丸めると値が読みにくくなる

![SSH の待ち時間を削った図の前と後。前は黒い枠の箱と黒い棒・縦じま。後は灰の面の箱、ブロッキング待機の棒が赤、ノンブロッキングが青](https://pixelstomotors.com/media/LOG469_02_ssh_before_after.png)

作業は2段に分けた。1段目はスクリプトで、どの図にも共通する所（字・地の色・外側のカード・枠のある箱・ボタン・斜線）を置き換える。2段目は1本ずつ中身を読んで、何がその図の主役かを決め、役割の色を入れる。2段目は判断が要るので、決まりの文（recolor_spec.md）を書いて Claude の作業を分け、同時に動かした。

### /tmp/claude-0/restyle_embeds.py（字と下地の色の置き換え）

```python
"""記事の図解（public/embeds）を article_rules.md の新しい決まり（2026-11-08）にそろえる。
字：IBM Plex Mono → Inter（英数字）、Noto Sans JP の前に Inter。色：#0d0d0d → #242424、#f5f5f0 → #f7f7f7。
数字と文・作りは変えない。"""
import re, sys, pathlib
files = [l.strip() for l in open(sys.argv[1]) if l.strip()]
root = pathlib.Path('public/embeds')
for name in files:
    p = root / name
    h = p.read_text(encoding='utf-8'); o = h
    # Google Fonts の読み込み
    h = h.replace('family=IBM+Plex+Mono:', 'family=Inter:')
    # CSS / JS の字の名前
    h = re.sub(r"(['\"])IBM Plex Mono\1\s*,\s*(['\"])Noto Sans JP\2", r"\1Inter\1,\2Noto Sans JP\2", h)
    h = re.sub(r"(['\"])IBM Plex Mono\1", r"\1Inter\1", h)
    h = h.replace('IBM Plex Mono', 'Inter')
    h = re.sub(r"(?<![\w,]\s)(?<![\w,])(['\"])Noto Sans JP\1(\s*,\s*sans-serif)", r"\1Inter\1,\1Noto Sans JP\1\2", h)
    h = re.sub(r"(['\"])Inter\1\s*,\s*(['\"])Inter\2", r"\1Inter\1", h)
    # JS の属性で字を1つだけ書いている所（'font-family':'Noto Sans JP'）
    h = re.sub(r"(['\"]font-family['\"]\s*:\s*)(['\"])Noto Sans JP\2", r"\1\2Inter, Noto Sans JP\2", h)
    h = re.sub(r"(font-family=\")Noto Sans JP(\")", r"\1Inter, Noto Sans JP\2", h)
    h = re.sub(r"(['\"]font-family['\"]\s*:\s*)(['\"])Inter\2", r"\1\2Inter, Noto Sans JP\2", h)
    # 色
    h = re.sub(r"#0d0d0d", "#242424", h, flags=re.I)
    h = re.sub(r"#f5f5f0", "#f7f7f7", h, flags=re.I)
    h = re.sub(r"#0d0d0d", "#242424", h, flags=re.I)
    # 数字の桁（前の等幅の代わり）
    if 'tabular-nums' not in h:
        h = h.replace('<style>', '<style>html{font-variant-numeric:tabular-nums}', 1)
    if h != o:
        p.write_text(h, encoding='utf-8')
print('ok', len(files))
```

### /tmp/claude-0/shape_embeds.py（白いカード・角丸・細かい斜線）

```python
"""記事の図解に共通の形を入れる（STYLE.md の「斜線と塗り」「角丸」・article_rules の HTML図解。2026-11-08）。
外側の枠をやめて白いカード、中の箱は灰の面、ボタンは角丸4px、小さい英字ラベルは灰、斜めの斜線は細かく。
色の意味づけ（青・赤・緑）は入れない（1本ずつ別に入れる）。"""
import re, sys, pathlib

INK = r"(?:#242424|var\(--(?:ink|black|fg|blk|bk)\)|#000|#0a0a0a|#111)"
PAPERS = ("#f7f7f7", "var(--paper)", "var(--bg)")

def split_rules(css):
    out = []; i = 0; n = len(css)
    while i < n:
        j = css.find('{', i)
        if j < 0: out.append(('raw', css[i:])); break
        sel = css[i:j]
        if sel.strip().startswith('@'):
            depth = 1; k = j + 1
            while k < n and depth:
                if css[k] == '{': depth += 1
                elif css[k] == '}': depth -= 1
                k += 1
            out.append(('at', sel, css[j+1:k-1])); i = k; continue
        k = css.find('}', j)
        if k < 0: out.append(('raw', css[i:])); break
        out.append(('rule', sel, css[j+1:k])); i = k + 1
    return out

def join_rules(rs, fix):
    s = []
    for r in rs:
        if r[0] == 'raw': s.append(r[1])
        elif r[0] == 'at':
            inner = r[2]
            if '{' in inner and r[1].strip().startswith('@media'):
                inner = join_rules(split_rules(inner), lambda sel, d: fix(sel, d, media=True))
            s.append(r[1] + '{' + inner + '}')
        else:
            s.append(r[1] + '{' + fix(r[1], r[2]) + '}')
    return ''.join(s)

def has(d, prop): return re.search(r'(^|;)\s*' + prop + r'\s*:', d) is not None
def drop(d, prop): return re.sub(r'(^|;)\s*' + prop + r'\s*:[^;]*', r'\1', d)
def setp(d, prop, val):
    d = drop(d, prop); d = d.rstrip().rstrip(';')
    return (d + ';' if d.strip() else '') + prop + ':' + val

def fix_factory(wrap_sel):
    def fix(sel, d, media=False):
        s = sel.strip()
        # body
        if re.fullmatch(r'(html\s*,\s*)?body', s) and has(d, 'padding'):
            d = setp(d, 'padding', '8px 0')
        # 外側のカード
        if wrap_sel and re.search(r'(^|[\s,])' + re.escape(wrap_sel) + r'\s*$', s):
            d = drop(d, 'border')
            if not media:
                d = setp(d, 'background', '#fff'); d = setp(d, 'border-radius', '8px'); d = setp(d, 'padding', '28px 24px')
            elif has(d, 'padding'):
                d = setp(d, 'padding', '20px 14px')
            return d
        isbtn = 'button' in s or re.search(r'\.(btn|tab|seg|chip)\b', s)
        pressed = re.search(r'aria-pressed|aria-selected|\.on\b|\.active|\.sel\b|:hover|\.cur\b|\.is-', s)
        if isbtn:
            if pressed:
                if re.search(r'background\s*:\s*' + INK, d): d = setp(d, 'border-color', '#242424'); d = re.sub(r'color\s*:\s*(#f7f7f7|var\(--paper\)|var\(--bg\))', 'color:#fff', d)
            else:
                if re.search(r'border\s*:\s*1(\.5)?px\s+solid', d):
                    d = re.sub(r'border\s*:\s*1(\.5)?px\s+solid\s+[^;]+', 'border:1px solid #e6e6e6', d); d = setp(d, 'border-radius', '4px')
                    d = re.sub(r'background\s*:\s*(#f7f7f7|var\(--paper\)|var\(--bg\))', 'background:#fff', d)
            return d
        # 小さい英字のラベル
        m = re.search(r'letter-spacing\s*:\s*\.(1[0-9]|2[0-9])em', d)
        fs = re.search(r'font-size\s*:\s*(\d+(?:\.\d+)?)px', d) or re.search(r'font\s*:[^;]*?(\d+(?:\.\d+)?)px', d)
        if m and fs and float(fs.group(1)) <= 13:
            d = re.sub(r'letter-spacing\s*:\s*\.\d+em', 'letter-spacing:.08em', d)
            if not re.search(r'(^|;)\s*color\s*:', d): d = setp(d, 'color', '#888')
        # 表のマス
        if re.search(r'(^|[\s,>])(td|th)\b', s) and re.search(r'border(-\w+)?\s*:\s*1(\.5)?px\s+solid', d):
            d = re.sub(r'(border(?:-\w+)?)\s*:\s*1(\.5)?px\s+solid\s+[^;]+', r'\1:1px solid #e6e6e6', d)
            return d
        # 枠と余白のある箱 → 灰の面・角丸8px（データの棒などは padding がないので外れる）
        if re.search(r'(^|;)\s*border\s*:\s*1(\.5)?px\s+solid\s+' + INK, d) and has(d, 'padding') and not re.search(r'bar|seg|cell|dot|col\b|fill|chart|svg|canvas|grid', s):
            dark = re.search(r'background(-color)?\s*:\s*' + INK, d)
            d = drop(d, 'border'); d = setp(d, 'border-radius', '8px')
            if not dark: d = setp(d, 'background', '#f7f7f7')
            return d
        # 区切りの線（上か下だけ）
        if re.search(r'border-(top|bottom)\s*:\s*1(\.5)?px\s+solid\s+' + INK, d) and has(d, 'padding') and not re.search(r'chart|axis|ax\b|bar|plot|track|scale|tl\b', s):
            d = re.sub(r'(border-(?:top|bottom))\s*:\s*1(\.5)?px\s+solid\s+' + INK, r'\1:1px solid #e6e6e6', d)
        return d
    return fix

def hatch(css):
    # 斜めの斜線を、1年のページと同じ細かさ（線2px・4px おき・-45°）にする。色は1本目の色のまま
    def r(m):
        ang, col, rest = m.group(1), m.group(2), m.group(3)
        return 'repeating-linear-gradient(-45deg,%s 0 2px,transparent 2px 4px)' % col
    return re.sub(r'repeating-linear-gradient\(\s*(-?45deg|135deg)\s*,\s*((?:#[0-9a-fA-F]{3,8}|var\(--[\w-]+\)|rgba?\([^)]*\)))\s+0(?:px)?\s+[\d.]+px\s*,\s*([^)]*?)\)', r, css)

def process(path):
    h = path.read_text(encoding='utf-8'); o = h
    m = re.search(r'<style>(.*?)</style>', h, re.S)
    if not m: return False
    css = m.group(1)
    w = re.search(r'([.#][A-Za-z_][\w-]*)\s*\{[^}]*max-width\s*:\s*7[0-9]0px', css)
    css2 = join_rules(split_rules(css), fix_factory(w.group(1) if w else None))
    css2 = hatch(css2)
    h = h[:m.start(1)] + css2 + h[m.end(1):]
    h = hatch(h)  # JS の中の style 文字列
    if h != o: path.write_text(h, encoding='utf-8'); return True
    return False

if __name__ == '__main__':
    n = 0
    for name in open(sys.argv[1]).read().split():
        n += process(pathlib.Path('public/embeds') / name)
    print('changed', n)
```

### /tmp/claude-0/recolor_spec.md（1回目の、役割の色を入れる作業の決まり）

```markdown
# 図解の色の意味づけ（作業の決まり）

対象：/home/claude/p2m-site/public/embeds/ の、渡された図解 HTML。すでに共通の形（白いカード・角丸・細かい斜線・Inter＋Noto）は入っている。
やること：1本ずつ中身を読んで、何がその図の「主役」かを決め、役割の色を入れる。見本は /tmp/claude-0/LOG349_replay_attempts.html・LOG440_serve_vs_bus.html・LOG456_standby_entry.html（この3本を最初に読んで、同じ密度にそろえる）。

## 色（これ以外の色を足さない）
- 字・線の基本 #242424、補足の字 #555、目立たせない字や枠 #888、細い区切り #e6e6e6、地 #f7f7f7、白 #fff
- 主役（その図で一番見てほしいもの・今・後・選んだ物）：青 #2f6fdf（塗り）。面に薄く敷くときは #e8f0ff
- 比べる相手（前・基準・試算・もう一方）：主役と同じ色の細かい斜線 `repeating-linear-gradient(-45deg,色 0 2px,transparent 2px 4px)`。主役が青なら青の斜線
- 悪い（詰まり・待ち・止まり・失敗・増えて困るもの）：赤 #d85a30。面は赤の斜線か塗り。薄い面は #fbe6de
- 良い（合格・成功・減って良くなったもの）：緑 #1d9e75。薄い面は #dff3ec
- 記録だけ・取りやめ・試していない・関係ない物：灰 #9a9a9a（斜線か塗り）
- 1つの図の中で青に2つの意味を持たせない。凡例があれば凡例の色も合わせる。主役が決められない図（手順・一覧・文章の図）は、無理に色を入れず黒のままでよい。選んでいるボタン・タブは黒の塗りのまま（青にしない）
- 黄色は使わない（お客さんの色）。Unity の画面のようにスタッフが出る絵では、スタッフを青にしてあれば、その図では主役に青を使わない

## 形
- 角丸は入れ物と押すものだけ（箱・カード 8px、ボタン・札 4px）。棒・帯・面・線・軸・地図の形は四角のまま
- 中に残っている黒い 1.5px の枠の箱（説明の欄・数字の欄）は、#f7f7f7 の面・角丸8px・枠なしにする。データの枠（棒の外枠など）は残してよい
- 縦線や太い斜線の模様は、細かい斜め（上の書き方）にする
- 線グラフの線は stroke-linecap:butt・stroke-linejoin:round

## 変えてはいけないもの
- 数字・データ・文の中身・作り・動き（JS の動作）・iframe の高さを送る最後の script。文で変えてよいのは、色や模様を説明している言葉だけ（例「黒が配膳だけ、斜線が…」→「斜線が配膳だけ、塗りが…」）
- git の commit・push はしない（まとめ役がする）。渡されたファイル以外を触らない

## 確かめ方
- 直したら `python3 /tmp/claude-0/render_one.py <ファイル>` で画像にして、Read で開いて見る。字の重なり・はみ出し・色の意味のずれがないかを見る。動く図は、最初の画面で見えていればよい
- 終わったら、ファイルごとに1行で「主役に何を青にしたか／赤・緑・灰を何に使ったか／色を入れなかった理由」を返す
```

公開したあと、3つの不具合が見つかった。

- 図の枠の中にスクロールバーが出ていた。図の高さが小数（例 812.4px）で、親のページが切り捨てた高さで枠を作り、1px 足りなかった。p2m-site の Base.astro で、受け取った高さに2px 足すようにした（`Math.ceil(h) + 2`）
- LOG455 の紙吹雪が出なかった。紙吹雪を画面の上の外から出していて、iframe の中では見えなかった。床の楕円から打ち上げる動きに直した
- LOG468 の図解が一覧から漏れていた。一覧を作ったあとに公開された記事だったので、後から同じ決まりで直した

![Motion UX 三相モデルの図の前と後。黒い枠の箱が灰の面に、札が白地と細い枠に、比べる2つのうち自分のモデルの欄が薄い青になった](https://pixelstomotors.com/media/LOG469_03_motion_ux_before_after.png)


## 作業ログ ― 線の太さを、数字ではなく強さの順番で直す

> 枠線と統一されていますか？太いのと細いのがあるようですが（同じ太さにしろという意味ではなくて、枠の大きさによって同じように見えるように統一してほしいという意味です）

最初は、SVG の線を図の大きさに関係なく同じ太さで描く指定（non-scaling-stroke）を全部に入れた。見本で見ると、細い線で描いていた図が重くなった。

> 456のSPは前の方がいいね

> 379も修正前の方がいい 線の太さが統一されていればいいというわけではなく、細い線の方がいいときはあるよ

太さの数字をそろえるのをやめ、**図の中の強さの順番（目盛り・区切り ＜ 軸 ≦ データの線 ＜ 選んだ物の囲み・強調）が逆になっている所だけを直す**ことにした。131本を調べ、逆になっていたのは9本だった。前と後の画像を見てもらってから公開した。

| 図解 | 直した所 |
|---|---|
| LOG348 腕を伸ばす先 | グラフの外枠を 1.5→1、ひし形の点の縁を 2→1.5（データより目立たないように） |
| LOG357 通路の幅 | 凡例の四角の縁を 1→1.5（図の中の同じ印とそろえた） |
| LOG360 卓までの距離 | 選んだ卓の囲みを 0.18→0.22 |
| LOG364 すれ違いの道 | 1・2・3の点線の枠を 1.5→1 |
| LOG383 基準のずれ | 区切りの縦線を 1.5→1 |
| LOG412 センサーの代わり | 選んだ所の点線の矢印を 1.5→2 |
| LOG425 声の流れ | 凡例に区切りの線の見本を足した |
| LOG447 カメラの高さ | 床の線を 4px→2px |
| LOG452 ロボットが待つ理由 | 選んだ列の囲みを 1px の灰から 2px の黒に |

![LOG447 のカメラの高さの図の前と後。床の黒い線が 4px から 2px になり、カメラの枠（2px）と同じ強さになった](https://pixelstomotors.com/media/LOG469_04_line_strength_LOG447.png)

### /tmp/claude-0/lines_spec.md（線の強さの点検の決まり）

```markdown
# 図解の線の太さの点検（作業の決まり）

対象：/home/claude/p2m-site/public/embeds/ の、渡された図解 HTML（色と形はもう新しい決まりで入っている）。
やること：1本ずつ画像にして見て、線の「強さの順番」が逆になっている所だけを直す。逆になっていなければ何もしない（直さない図が多くてよい）。

## 強さの順番（1つの図の中で）
目盛り・区切り ＜ 軸 ≦ データの線 ＜ 選んだ物の囲み・強調

- 逆の例：選んだ棒の囲みが薄い灰の細い線なのに、下の軸が黒い太い線（→ 囲みを軸と同じか少し強く、黒にする。または軸を細くする）。目盛りの線がデータの線より太い・濃い。区切り線が強調の線より目立つ
- 同じ役割の線は、1つの図の中で同じ太さにそろえる（例：同じ図の中で、凡例の線見本と本体の線の太さが違う）
- 直し方は、弱すぎる側を少し強めるか、強すぎる側を弱めるか、見た目が軽くなるほうを選ぶ。全体を太くしない

## してはいけないこと
- 線の太さを全部同じ数字にそろえない。細い線のほうがよいとき（点の多い線グラフ・細かい模様・地図の細部）は細いままにする
- SVG に vector-effect:non-scaling-stroke を入れない（線は絵と一緒に縮めてよい）
- 色・数字・文・動き・作り・最後の高さを送る script は変えない
- git の commit・push はしない。担当以外のファイルは触らない

## 確かめ方
- `python3 /tmp/claude-0/render_one.py <ファイル>` で画像にして Read で見る。直す前の原本は /tmp/claude-0/lw_before/ に同じ名前である（見比べに使う。書き換えない）
- 最後に、直したファイルだけを「ファイル名：どの線がどう逆だったか → どう直したか」の1行ずつで返す。直さなかったファイルは数だけ返す
```

### /tmp/claude-0/lines.py（線の太さの一覧を出す）

```python
"""図解の線を3段（1px・2px・3px）にそろえ、伸び縮みしても画面の上で同じ太さに見えるようにする（2026-11-08）。"""
import re, sys, pathlib
def snap(v):
    v = float(v)
    return '1' if v <= 1.25 else ('2' if v <= 2.6 else '3')
def process(p):
    h = p.read_text(encoding='utf-8'); o = h
    # 1. SVG の線は伸び縮みしない（画面の上の px で太さが決まる）
    if 'non-scaling-stroke' not in h:
        h = h.replace('<style>', '<style>svg *{vector-effect:non-scaling-stroke}', 1)
    # 2. SVG の stroke-width を 1・2・3 にそろえる（属性・JS のオブジェクト・テンプレート文字列）
    h = re.sub(r'(stroke-width=")([0-9.]+)(")', lambda m: m.group(1) + snap(m.group(2)) + m.group(3), h)
    h = re.sub(r"(['\"]stroke-width['\"]\s*:\s*)([0-9.]+)", lambda m: m.group(1) + snap(m.group(2)), h)
    h = re.sub(r"(['\"]stroke-width['\"]\s*:\s*['\"])([0-9.]+)", lambda m: m.group(1) + snap(m.group(2)), h)
    h = re.sub(r"(stroke-width\s*:\s*)([0-9.]+)(px)?", lambda m: m.group(1) + snap(m.group(2)) + 'px', h)
    # 3. HTML の線：1.5px の線は役割で分ける（軸・区切りは 1px、凡例の線見本は 2px）
    h = re.sub(r'(border(?:-(?:top|bottom|left|right))?\s*:\s*)(1\.5|1\.2)px', r'\g<1>1px', h)
    h = re.sub(r'(border(?:-(?:top|bottom|left|right))?\s*:\s*)(2\.5|2\.2)px', r'\g<1>2px', h)
    # 選んだ物の囲み（outline）は黒の 2px
    h = re.sub(r'outline\s*:\s*[0-9.]+px\s+solid\s+[^;}"]+', 'outline:2px solid #242424', h)
    if h != o: p.write_text(h, encoding='utf-8'); return True
    return False
if __name__ == '__main__':
    n = sum(process(pathlib.Path(a)) for a in sys.argv[1:]); print('changed', n)
```

あわせて、LOG468 の図解の「スタッフの手すき」の棒を、オレンジから灰にした。手すきは悪い値ではないので、赤（悪い）の意味に合わないため。


## 作業ログ ― 消えた Google スプレッドシートの表を、過去のチャットから戻す

DAY3 の記事を開いたエイイチが、表が出ていないことに気づいた。

> 画像が表示されていないページがありました。何が原因か調べてください。

古い DAY の記事は、Studio のころに Google スプレッドシートの「ウェブに公開」の表を iframe で埋め込んでいた。その公開の URL を開くと、1つのブック（2PACX-1vSAGYq…）は 410（もう無い）、進捗管理表のブック（2PACX-1vQnqeo…）は 400 を返した。元のスプレッドシートが消えるか公開をやめたため、記事の中が白い箱になっていた。

> できればスプシとドキュメントの内容を読み込んで、サイトに直接反映してほしい（ページ共有はなし）

iCloud とドライブには写しがなく、ゴミ箱も空だった。残っていたのは、2026年3月の claude.ai のチャット「Difyへのデータ形式について」で、Dify（チャットボットを作るサービス）に読ませるために、このブックの全部のタブを CSV に変えていた記録だった。

| 埋め込み | タブ | 中身を戻した元 |
|---|---|---|
| day3-ZeArA2HL | ターゲットユーザーと利用シナリオ | 3月のチャットの CSV |
| day3-tmB19lEB・day39-jAoD9zMo | ハードウェアと安全性の寸法要件 | 3月のチャットの CSV |
| day3-vuWqzByT | ロボット本体のUI要素 | 3月のチャットの CSV |
| day4-Y4qhSa0d・day5-Ti5Y0a1o・day10-99rShwHn | UI/UXデザイン画面リスト（14画面） | 3月のチャットの CSV |
| day6-pBNtWWqI | 顔パターン（12） | 3月のチャットの CSV |
| day11-ZHEDulpe | 24時間シフト | 3月のチャットの CSV |
| day38-iidr8BNd | HRIデザイン変数一覧（12） | 3月のチャットの CSV |
| day55-gmfRY92v | 側面発光3ステート | 3月のチャットの CSV |
| day9-vrBBzDi4 | 4コママンガプロット（最初の3話） | ドライブの進捗管理表 |
| day17-YAH8yYE9 | 時間帯別ターゲット | ドライブに残っていた同じ表 |
| day13-m6YVYtWS | SNS投稿管理表 | 見つからず。表の代わりに断り書きを1行 |

DAY9 は、エイイチの指示で最初の数話（3話）だけを載せた。DAY11 のシフト表の「次次帯還開始」は CSV に変えたときの読み違いと見て、「順次帰還開始」「順次出動開始」に直した（元の言葉は推測）。

表は、図解と同じ白いカードにした。スマホの幅では、1行を1枚のカードにして縦に並べる。列の多い表（画面リスト）は、カテゴリごとに見出しの行を入れ、列を6つに絞った。

![DAY31 の「ロボティクスUXデザイナー戦略」をサイトの中の表にしたもの。1つのシートに3つの表が並んでいたので、表ごとに名前を付けた。列が多い2つ目の表は1行を1枚のカードにした](https://pixelstomotors.com/media/LOG469_05_sheet_to_table_day31.png)

### /tmp/claude-0/sheets/build.py（消えた表13か所を作り直す）

```python
# 消えた Google スプレッドシートの埋め込みを、サイトの中の表に置き換える（2026-11-08）
import csv, io, html, sys
OUT = sys.argv[1]
JS = open('/tmp/claude-0/sheets/height.js', encoding='utf-8').read()

CSS = """<style>html{font-variant-numeric:tabular-nums}
*{box-sizing:border-box}
body{margin:0;background:#f7f7f7;color:#242424;font-family:'Inter','Noto Sans JP',sans-serif;padding:8px 0}
.wrap{max-width:720px;margin:0 auto;background:#fff;border-radius:8px;padding:24px 24px 20px}
.lab{font-size:12px;letter-spacing:.08em;color:#888;margin:0 0 12px}
table{width:100%;border-collapse:collapse;font-size:13px;line-height:1.7}
th{white-space:nowrap;text-align:left;font-weight:700;font-size:12px;color:#555;padding:8px 10px;border-bottom:1px solid #242424;vertical-align:bottom}
td{padding:10px;border-bottom:1px solid #e6e6e6;vertical-align:top}
tr:last-child td{border-bottom:0}
td.k{font-weight:700;white-space:nowrap}
td.n{white-space:nowrap}
.src{margin:14px 0 0;font-size:11px;color:#888;line-height:1.6}
/* 列の多い表と、スマホでは、1行を1つの欄にする */
.stack table,.stack tbody,.stack tr,.stack td{display:block;width:100%}
.stack thead{display:none}
.stack tr{background:#f7f7f7;border-radius:8px;padding:14px 16px;margin:0 0 10px}
.stack tr:last-child{margin-bottom:0}
.stack td{border:0;padding:0 0 8px}
.stack td:last-child{padding-bottom:0}
.stack td::before{content:attr(data-h);display:block;font-size:11px;letter-spacing:.08em;color:#888}
.stack td.k{font-size:15px;white-space:normal;padding-bottom:10px}
.stack td.k::before{display:none}
@media (max-width:560px){
 .wrap{padding:20px 16px 16px}
 .auto table,.auto tbody,.auto tr,.auto td{display:block;width:100%}
 .auto thead{display:none}
 .auto tr{background:#f7f7f7;border-radius:8px;padding:14px 16px;margin:0 0 10px}
 .auto tr:last-child{margin-bottom:0}
 .auto td{border:0;padding:0 0 8px;white-space:normal}
 .auto td:last-child{padding-bottom:0}
 .auto td::before{content:attr(data-h);display:block;font-size:11px;letter-spacing:.08em;color:#888}
 .auto td.k{font-size:15px;padding-bottom:10px}
 .auto td.k::before{display:none}
}
</style>"""

def page(title, lab, csvtext, mode='auto', note=None, nowrap=()):
    rows = list(csv.reader(io.StringIO(csvtext.strip())))
    head, body = rows[0], rows[1:]
    e = html.escape
    th = ''.join('<th>%s</th>' % e(h) for h in head)
    trs = []
    for r in body:
        tds = []
        for i, (h, v) in enumerate(zip(head, r)):
            cls = ' class="k"' if i == 0 else (' class="n"' if i in nowrap else '')
            tds.append('<td%s data-h="%s">%s</td>' % (cls, e(h), e(v).replace('\n', '<br>')))
        trs.append('<tr>' + ''.join(tds) + '</tr>')
    n = '<p class="src">%s</p>' % e(note) if note else ''
    return ('<!doctype html>\n<html lang="ja">\n<head>\n<meta charset="utf-8">\n'
            '<meta name="viewport" content="width=device-width,initial-scale=1">\n'
            '<title>%s</title>\n<link rel="preconnect" href="https://fonts.googleapis.com">\n'
            '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">\n'
            '%s\n</head>\n<body>\n<div class="wrap %s">\n<p class="lab">%s</p>\n'
            '<table><thead><tr>%s</tr></thead><tbody>\n%s\n</tbody></table>\n%s\n</div>\n%s\n</body>\n</html>\n'
            ) % (e(title), CSS, mode, e(lab), th, '\n'.join(trs), n, JS)

def screens(lab, csvtext):
    rows = list(csv.reader(io.StringIO(csvtext.strip())))[1:]
    e = html.escape
    H = ['画面ID', '画面名', 'ターゲットユーザー', '機能概要・要件', '優先度', '対応進捗']
    out, cur = [], None
    for name, cat, sid, dev, who, what, pri, st in rows:
        if cat != cur:
            cur = cat
            out.append('<tr class="g"><td colspan="6">%s<span>%s</span></td></tr>' % (e(cat), e(dev)))
        v = [sid, name, who, what, pri.split('（')[0], st]
        out.append('<tr>' + ''.join('<td%s data-h="%s">%s</td>' % (c, e(h), e(x)) for h, x, c in zip(H, v, ['', ' class="k"', '', '', ' class="n"', ' class="n"'])) + '</tr>')
    t = page(lab, lab, '画面ID,画面名,ターゲットユーザー,機能概要・要件,優先度,対応進捗\nx,x,x,x,x,x', 'auto scr')
    t = t.replace('<td class="k" data-h="画面ID">x</td>', '', 1)
    a, b = t.index('<tbody>') + 7, t.index('</tbody>')
    extra = ('<style>.scr td:first-child{white-space:nowrap;color:#555}.scr td.k{white-space:normal}'
             '.scr tr.g td{background:#f7f7f7;font-weight:700;font-size:12px;padding:8px 10px;border-bottom:0}'
             '.scr tr.g td span{font-weight:400;color:#888;margin-left:10px}'
             '@media (max-width:560px){.scr tr.g{background:none;padding:12px 0 4px;margin:0}.scr tr.g td{background:none;padding:0}.scr tr.g td::before{display:none}'
             '.scr tr.g td span{display:block;margin:0}.scr td.k{font-size:15px}}</style>')
    t = t[:a] + '\n' + '\n'.join(out) + '\n' + t[b:]
    return t.replace('</head>', extra + '\n</head>', 1)

T = {}
T['target'] = ("ターゲットユーザーと利用シナリオ", """項目,薬剤搬送ロボットの定義案,補足・要件への影響
ターゲットユーザー,病院の看護師、薬剤師、医療スタッフ,忙しい環境での迅速な操作、非専門家でも直感的に使えるUIが必須。
主な利用環境,病院の病棟、廊下、エレベーター、ナースステーション、薬局,狭い通路での走行安定性、エレベーター連携、低騒音、衛生面が要件に。
主なタスク,薬剤（注射薬、内服薬など）や検体（血液など）の指定場所への搬送,セキュリティ（薬剤盗難防止）、温度管理、搬送状況のリアルタイム監視が要件に。
HRIの目的,スタッフの負担軽減、作業効率向上、心理的安全性の確保,愛着や親しみやすさを持てるデザイン（顔UI）、明確なステータス表示が重要。
""", 'auto')
T['hw'] = ("ハードウェアと安全性の寸法要件", """要件カテゴリ,寸法/値,根拠と要件への反映
ロボットの高さ,1150 mm（約1.15 m）,看護師の作業、車椅子利用者との目線に配慮した設計。
薬剤取り出し高さ,約750 mm,人間工学的な高さ。看護師が自然な姿勢で薬剤を取り出せる。
ロボットの幅/奥行,520 mm,病院の標準的な通路や病室のドアを通過できる最小限の幅を確保。
通路の想定幅,約2000 mm（2.0 m）,ストレッチャー（600 mm）と搬送ロボット（520 mm）がすれ違う環境を想定。
最小安全クリアランス,400 mm 超,安全規格に基づいたすれ違い時の最小間隔の確保。
重量,最大積載時でも医療スタッフが緊急時に手動で動かせる程度（想定）,故障時や緊急停止時の対応能力に影響。
""", 'auto')
T['ui'] = ("ロボット本体のUI要素", """UI要素,要件,デザイナーの役割
顔（ディスプレイ）,感情表現（笑顔、困惑など）、タスクステータス表示,キャラクターデザイン、アニメーション設計。親しみやすさと状況理解のしやすさのバランス。
ステータス表示,進行方向、バッテリー残量、積載部の施錠状態（色やアイコンで表示）,視認性の高いインジケーターデザイン（側面の光るリングなど）。
操作インターフェース,タッチパネルによる目的地入力、薬剤受け渡し確認（ID/パスワード）,直感的な情報階層、ミスタッチ防止、多忙なスタッフでも迅速に操作できるフローの設計。
""", 'auto')
T['screens'] = ("UI/UXデザイン画面リスト", """画面名,カテゴリ,画面ID,使用デバイス,ターゲットユーザー,機能概要・要件,優先度,対応進捗
待機・表情画面,1. Robot HMI,R-01,ロボット本体（タッチパネル）,全員,アイドリング時の表情／充電・待機ステータス,★★★（高）,完了
認証画面,1. Robot HMI,R-02,ロボット本体（タッチパネル）,薬剤師・看護師,顔認証／パスワード入力,★★★（高）,完了
目的地選択画面,1. Robot HMI,R-03,ロボット本体（タッチパネル）,薬剤師,病棟・ステーション選択／フロアマップ表示,★★★（高）,完了
薬剤投入・確認画面,1. Robot HMI,R-04,ロボット本体（タッチパネル）,薬剤師,ドロワー解錠・施錠／投入完了ボタン,★★★（高）,完了
走行中ステータス画面,1. Robot HMI,R-05,ロボット本体（タッチパネル）,すれ違う人,進行方向表示／メッセージ表示,★★★（高）,完了
受取・取出画面,1. Robot HMI,R-06,ロボット本体（タッチパネル）,看護師,到着時認証／ドロワー解錠／受取完了,★★★（高）,完了
エラー・緊急停止画面,1. Robot HMI,R-07,ロボット本体（タッチパネル）,全員・保守,障害物検知時の表情／緊急停止ボタン,★★★（高）,完了
ダッシュボード,2. Admin Web,A-01,PC／タブレット（Web）,運用管理者,全機体位置表示／稼働ステータス一覧,★★★（高）,完了
搬送タスク管理,2. Admin Web,A-02,PC／タブレット（Web）,運用管理者,配送リクエスト一覧／スケジュール管理,★★☆（中）,完了
履歴・ログ確認,2. Admin Web,A-03,PC／タブレット（Web）,運用管理者,搬送履歴ログ／エラー詳細ログ,★★☆（中）,完了
機体設定・管理,2. Admin Web,A-04,PC／タブレット（Web）,保守担当者,バッテリー詳細／ファームウェア更新,★☆☆（低）,完了
到着通知・パス表示,3. Staff App,M-01,スマートフォン／タブレット,看護師,到着通知ポップアップ／解錠パス表示,★★☆（中）,完了
集荷リクエスト,3. Staff App,M-02,スマートフォン／タブレット,看護師,呼出ボタン／到着予定確認,★☆☆（低）,完了
プロフィール,3. Staff App,M-03,スマートフォン／タブレット,看護師,プロフィール／アプリ設定／サポート,★☆☆（低）,完了
""", 'screens')
T['face'] = ("ロボットの顔パターン案（12パターン）", """パターン,デザインのポイント,印象,適した場面
標準笑顔,縦長の目、細やかな口,シンプル、親しみやすい,通常時、待機中
にっこり笑顔,口角を上げる、目を細める,とても親しみやすい,挨拶、完了時
真剣な表情,目をキリッと、口を引き締める,頼りがいがある,搬送中、重要表示
丸い目,目をまん丸にする,可愛い、愛嬌がある,小児科、和ませたい時
眉毛を追加,目の上に眉毛を追加,表情豊か、人間らしい,困った時、考え事
ウィンク,片目を弓なりに閉じ、もう片方は開けたままにする。口角は上げる。,お茶目、親しみの情、遊び心,すれ違いざまの挨拶、軽いタスク完了時、ジョークへの反応。
スリープ（充電中）,両目を閉じた形（「U」の字を逆にしたような弧線、または横棒）にする。口は小さくする。,安らか、休息中、静か,充電ステーションにいる時、待機モードで省エネ中の時。
驚いた顔,目を普段より大きく真ん丸に見開き、口も小さく丸く（「O」の形）開ける。,びっくり、予期せぬ事態、純粋な反応,突然障害物を検知した時、大きな音がした時、新しい情報を認識した瞬間。
困った顔（考え中）,眉毛を「ハ」の字にし、口を少し「へ」の字（または波線）にする。視線をわずかに斜め上に向けるとなお良い。,少し困惑、思考中、人間味がある,複雑なルート計算中、軽いエラーで再試行中の時、指示が聞き取れなかった時。
キラキラした目,目の白い部分の中に、星形や菱形のハイライトを追加して輝きを強調する。,期待、喜び、達成感、スペシャルな感じ,重要な任務を完了した時、褒められた時、特別なイベント時。
テレ顔（頬紅）,目の下に、淡い水色（または薄いピンク）の楕円形や斜線を追加して、頬を赤らめているように表現する。,照れくさい、謙虚、可愛らしい,患者さんやスタッフに「ありがとう」と言われた時、注目を浴びた時。
めまい,黒目が左右で上下反対になっている。口元が波打っているように表現する。,不安定な様子,何かにあたってよろけてしまった時、段差を乗り越えて不安定な時。
""", 'auto')
T['shift'] = ("ロボット24時間シフト表（12台）", """時間帯,チームA（5台）,チームB（5台）,フリー（2台）,現場の状態
8:00,稼働,充電・待機,メンテ／待機,5台稼働
13:50,順次帰還開始,順次出動開始,待機,★交代タイム
14:00,充電・待機,稼働,待機,5台稼働
19:50,順次出動開始,順次帰還開始,待機,★交代タイム
20:00,稼働,充電・待機,待機,5台稼働
1:50,順次帰還開始,順次出動開始,待機,★交代タイム
2:00,充電・待機,稼働,待機,5台稼働
7:50,順次出動開始,順次帰還開始,待機,★交代タイム
""", 'auto')
T['hri'] = ("薬剤搬送ロボットハコブくん：HRIデザイン変数一覧", """変数名,カテゴリ,設計値,HRIデザインの意図
通常移動速度,速度,0.7 m/s,看護師の平均歩行速度よりやや遅く設定し、威圧感を排除。
夜間モード速度,速度,0.4 m/s,静音性（40dB以下）を優先し、患者の睡眠を妨げない低速走行。
加減速カーブ,速度,0.2 m/s²,急な始動・停止を避け、搬送中の薬剤の安定と周囲への予測可能性を確保。
機体全幅,距離,510 mm,有効幅員1.2mの廊下で人間とストレスなくすれ違えるサイズ（制約520mm以下）。
安全停止距離,距離,0.6 m,LiDAR検知に基づき、接触を完全に回避する絶対停止ライン。
回避・戻り開始,距離,2.5 m,遠方から進路を微調整し、看護師に「気づいている」ことを伝える予備動作距離。
通常動作音,音,45 dB,病院の日中環境音（50dB）を下回り、業務を阻害しない静かさ。
夜間動作音,音,40 dB,図書館レベルの静寂。振動音や通知音の周波数を低域にシフト。
完了通知音,音,Soft Chime,「薬剤取り違え」を防ぐための、注意を引くが不快でない高調波設計。
サイドLED状態,光,RGB / 300cd,青：正常移動、緑：完了、赤：エラー。遠くからでも状態が直感的にわかる。
庫内高演色LED,光,CRI > 90,薄暗い夜間の病棟でも、薬剤名や色の識別ミスを防ぐ高精度な照明。
引出し高さ,身体,750 mm,看護師が腰を曲げずに自然な姿勢で操作できる、机の高さに合わせた人間工学設計。
""", 'auto')
T['light'] = ("ハコブくん側面発光仕様（3ステート）", """ステート,色,動き・リズム,表現する意図・メッセージ,Serve Roboticsでの対応表現（推測含む）
待機中,白／淡青,ゆっくりとしたブリージング（呼吸）,正常稼働中、指示待ち。穏やか。,ニュートラルな表情、ゆっくり周囲を見回す
移動中,青／グリーン,定常光、または進行方向へのフロー,活動中、移動エネルギー。進路注意。,進行方向を見つめる視線、ウィンカー
ヘルプ要請,黄／アンバー,早めのパルス点滅、不規則なゆらぎ,軽度の異常、介入要求。困惑。,困った表情、停止して周囲を見る、特殊な点滅パターン
""", 'auto')
T['time'] = ("時間帯別ターゲット", """時間帯,ターゲット層,心理状態・状況,投稿内容の方向性,4コマ漫画の活用例
朝（7:00〜9:00）,通勤・通学者／早朝勤務者,これから一日が始まる／情報収集／気合を入れたい,エール、活力／前向きなメッセージ／一日の活力になる内容,ハコブくんが元気に働く様子／「今日も一日頑張ろう！」というメッセージ／クスッと笑えて目が覚めるネタ
昼（12:00〜13:00）,学生／会社員／主婦（主夫）,昼休憩／リラックス／気分転換,癒し、共感、話題／午後の活力になる内容／誰かと話したくなるネタ,ハコブくんの失敗談や「あるある」ネタ／ほっこりするエピソード／ランチタイムに関連したネタ
夜（20:00〜23:00）,帰宅後の人々／夜勤者／学生,一日の終わり／リラックス／趣味の時間／明日への準備,労い、癒し、深い共感／一日の疲れを癒す内容／考えさせられる内容／明日への希望,一日の仕事を終えたハコブくん／「お疲れ様」のメッセージ／少し感動的なエピソード／夜勤中のハコブくんの様子
""", 'stack')
T['manga'] = ("4コママンガプロット（第1〜3話）", """タイトル,テーマ,登場人物,1コマ目（起）,2コマ目（承）,3コマ目（転）,4コマ目（結）,Design Note
第1話　チョコレート差し入れ,ロボットが提供する機能以上の「癒やし」の効果。,夜勤中の疲れた看護師、ハコブくん（ロボット）,忙しい夜勤の合間、必要な薬剤を運んできたロボットが到着。看護師が操作する。,ロボットの下部にある収納トレイが開き、必要な薬剤を取り出す。看護師の表情はまだ業務モードで硬い。,薬剤ケースの下に、先輩からの小さなメモ用紙と、個包装のチョコレートが隠れているのを見つけ、驚く。,看護師がチョコレートを片手に持ち、フッと優しい笑顔になる。去っていくロボットの背中を見送る。,ロボットを擬人化しすぎず、あくまで「機械としての健気さ」を描くことで、人間と機械のちょうど良い距離感（ソーシャルロボティクス）を表現。
第2話　言葉なきコミュニケーション,緊迫した現場での、非言語的な状況伝達デザインの重要性。,ベテラン看護師（超多忙）、新人医師（焦っている）、ハコブくん,救急搬送が重なり、殺気立つナースステーション。新人医師が「あの薬、まだですか！？」と叫ぶ。,その背後からハコブくんが到着。しかし、いつもの「到着しました」という音声アナウンスは出さない。（空気を読んでいる）,その代わり、顔ディスプレイの色を「緊急を示す赤」ではなく、「落ち着きを促す深い青」でゆっくり点滅させ、静かにトレイを差し出す。,ベテラン看護師がそれに気づき、フッと息を吐く。「…ありがと、良いタイミングね」。医師も少し冷静さを取り戻す。,「状況に応じたモダリティ（音声のON/OFF）の切り替え」や「色彩心理を用いたアンビエントな情報伝達」といった、高度なインタラクションデザインの視点を表現。
第3話　「最短ルート」の罠,エンジニアリング的な効率性と、人間的な空間認識のズレ。,患者（松葉杖でリハビリ中）、ハコブくん,広い廊下で、患者が一生懸命松葉杖で歩行訓練をしている。,ハコブくんが向こうからやってくる。システム上の「最短ルート」は、患者のすぐ横をすり抜けるコース。,ハコブくんは減速せず、計算通り患者のギリギリ横を「シュンッ」と通過する。患者、驚いてバランスを崩しかける「うおっ！？」。,ハコブくんのディスプレイには「効率的搬送中（キリッ）」の表示。遠くで見ていた看護師長が「あとで設定見直しね…（怒）」と呟く。,技術的に正しい挙動が、必ずしも人間にとって快適ではないという「パーソナルスペース」や「心理的安全性」の課題を理解していることを示唆。
""", 'stack')

FILES = {
 'day3-ZeArA2HL': 'target', 'day3-tmB19lEB': 'hw', 'day39-jAoD9zMo': 'hw', 'day3-vuWqzByT': 'ui',
 'day4-Y4qhSa0d': 'screens', 'day5-Ti5Y0a1o': 'screens', 'day10-99rShwHn': 'screens',
 'day6-pBNtWWqI': 'face', 'day11-ZHEDulpe': 'shift', 'day38-iidr8BNd': 'hri',
 'day55-gmfRY92v': 'light', 'day17-YAH8yYE9': 'time', 'day9-vrBBzDi4': 'manga',
}
NOTE = {'manga': 'プロット表のうち、最初の3話だけを載せています。'}
for f, k in FILES.items():
    lab, data, mode = T[k]
    if mode == 'screens':
        open('%s/%s.html' % (OUT, f), 'w', encoding='utf-8').write(screens(lab, data)); print(f); continue
    nowrap = ()
    open('%s/%s.html' % (OUT, f), 'w', encoding='utf-8').write(page(lab, lab, data, mode, NOTE.get(k), nowrap))
    print(f)
```

まだ表示されていた Google の埋め込み15本も、同じ形に作り直した。公開中のシートは `pub?output=csv&gid=…` で CSV を読める。1つのシートに表がいくつも並んでいるもの（空の行で区切られ、先頭の列に表の名前がある）は、表ごとに分けた。DAY22 はブックがまるごと（タブ15枚）埋め込まれていたので、記事の文に合う「ROS 2 基本コマンド（エラー対応メモ）」と「デバック情報まとめ」の2つだけを載せた。DAY78 は Google ドキュメントだったので、見出しと箇条書きを残した本文にした。

- ねらい：表の中身を、サイトの HTML の文字として持つ
- 変わったこと：Google の表は iframe の中の別のページで、記事のページを読む AI や検索には中身が見えなかった。今は記事の図解の HTML に文字として入っている。元のシートが消えても、表は消えない
- 見込める効果：AI が記事を読んだときに、表の中身まで答えに使える（見込み。サイトの記録で、AI が読みに来た回数とあわせて確かめる）

### /tmp/claude-0/gs/build2.py（表示中の Google の埋め込み15本を作り直す）

```python
# 公開中の Google スプレッドシート・ドキュメントの埋め込み15本を、サイトの中の表と本文に置き換える（2026-11-08）
# 使い方: python3 build2.py <public/embeds のフォルダ>
import csv, html, re, sys, importlib.util
from html.parser import HTMLParser
OUT = sys.argv[1]
G = '/tmp/claude-0/gs/'
spec = importlib.util.spec_from_file_location('b1', '/tmp/claude-0/sheets/build.py')
src = open('/tmp/claude-0/sheets/build.py', encoding='utf-8').read()
CSS = re.search(r'CSS = """(.*?)"""', src, re.S).group(1)
JS = open('/tmp/claude-0/sheets/height.js', encoding='utf-8').read()
CSS = CSS.replace('</style>', """th{white-space:normal}
.blk+.blk{margin-top:28px}
td.g{background:#f7f7f7;font-weight:700;font-size:12px;padding:8px 10px;border-bottom:0}
.doc{font-size:14px;line-height:1.85}
.doc h1,.doc h2,.doc h3,.doc h4{font-size:16px;line-height:1.5;margin:24px 0 8px}
.doc h1:first-child,.doc h2:first-child,.doc h3:first-child{margin-top:0}
.doc p{margin:0 0 6px}
.doc ul,.doc ol{margin:0 0 10px;padding-left:1.4em}
.doc li{margin:0 0 2px}
@media (max-width:560px){ .auto td.g::before{display:none} .auto tr.g{background:none;padding:12px 0 4px;margin:0} .auto tr.g td{background:none;padding:0} }
</style>""")
e = html.escape


def head(title):
    return ('<!doctype html>\n<html lang="ja">\n<head>\n<meta charset="utf-8">\n'
            '<meta name="viewport" content="width=device-width,initial-scale=1">\n'
            '<title>%s</title>\n<link rel="preconnect" href="https://fonts.googleapis.com">\n'
            '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">\n'
            '%s\n</head>\n<body>\n<div class="wrap">\n') % (e(title), CSS)


def tail(note=None):
    n = '<p class="src">%s</p>\n' % e(note) if note else ''
    return n + '</div>\n' + JS + '\n</body>\n</html>\n'


def clean(v):
    return v.replace('￨', 'ー').strip()


def blocks(rows):
    out, cur = [], []
    for r in rows:
        r = [clean(c) for c in r]
        if not any(r):
            if cur: out.append(cur); cur = []
        else:
            cur.append(r)
    if cur: out.append(cur)
    return out


def table(rows, lab=None):
    n = max(len(r) for r in rows)
    rows = [r + [''] * (n - len(r)) for r in rows]
    title = None
    # 1つ目の行が「見出しだけ」（先頭の欄だけ）なら、表の名前にする
    if sum(1 for c in rows[0] if c) == 1 and rows[0][0] and len(rows) > 1:
        title = rows[0][0]; rows = rows[1:]
    # 先頭の列が見出しの行にしか無いなら、それは表の名前
    if rows[0][0] and len(rows) > 1 and not any(r[0] for r in rows[1:]) and any(c for c in rows[0][1:]):
        title = title or rows[0][0]
        rows = [r[1:] for r in rows]
    keep = [i for i in range(len(rows[0])) if any(r[i] for r in rows)]
    rows = [[r[i] for i in keep] for r in rows]
    hd, body = rows[0], rows[1:]
    # 先頭が番号だけの列なら、名前の列の頭に付けて1つにする（カードの見出しを名前にするため）
    if body and all(re.fullmatch(r'\d+', r[0] or 'x') for r in body):
        k = next((i for i, h in enumerate(hd) if '名' in h), None)
        if k:
            for r in body: r[k] = r[0] + '　' + r[k]
            order = [k] + [i for i in range(1, len(hd)) if i != k]
            hd = [hd[i] for i in order]; body = [[r[i] for i in order] for r in body]
    avg = sum(len(c) for r in body for c in r) / max(1, sum(len(r) for r in body))
    mode = 'stack' if len(hd) >= 6 or (len(hd) == 5 and avg > 16) else 'auto'
    th = ''.join('<th>%s</th>' % e(h) for h in hd)
    trs = []
    for r in body:
        if sum(1 for c in r if c) == 1 and r[0]:
            trs.append('<tr class="g"><td class="g" colspan="%d">%s</td></tr>' % (len(hd), e(r[0]))); continue
        tds = ''.join('<td%s data-h="%s">%s</td>' % (' class="k"' if i == 0 else '', e(h), e(v).replace('\n', '<br>')) for i, (h, v) in enumerate(zip(hd, r)))
        trs.append('<tr>' + tds + '</tr>')
    t = title or lab
    return ('<div class="blk %s">%s<table><thead><tr>%s</tr></thead><tbody>\n%s\n</tbody></table></div>\n'
            % (mode, '<p class="lab">%s</p>' % e(t) if t else '', th, '\n'.join(trs)))


def sheet_page(fname, name, csvs, note=None):
    body = ''
    for i, c in enumerate(csvs):
        bl = blocks(list(csv.reader(open(G + c, encoding='utf-8'))))
        for j, b in enumerate(bl):
            body += table(b, lab=(name[i] if isinstance(name, list) else name) if j == 0 else None)
    t = name[0] if isinstance(name, list) else name
    open('%s/%s.html' % (OUT, fname), 'w', encoding='utf-8').write(head(t) + body + tail(note))


class Doc(HTMLParser):
    KEEP = {'h1', 'h2', 'h3', 'h4', 'p', 'ul', 'ol', 'li', 'table', 'tr', 'td', 'br', 'strong', 'b'}
    def __init__(self):
        super().__init__(); self.out = []; self.on = False; self.skip = 0
    def handle_starttag(self, t, a):
        a = dict(a)
        if a.get('id') == 'contents': self.on = True; return
        if not self.on: return
        if t in ('style', 'script'): self.skip += 1
        if t in self.KEEP: self.out.append('<%s>' % t)
    def handle_endtag(self, t):
        if not self.on: return
        if t in ('style', 'script'): self.skip -= 1
        if t in self.KEEP and t != 'br': self.out.append('</%s>' % t)
    def handle_data(self, d):
        if self.on and not self.skip: self.out.append(e(d))


def doc_page(fname, title, path):
    p = Doc(); p.feed(open(G + path, encoding='utf-8').read())
    h = ''.join(p.out)
    h = re.sub(r'<p>\s*</p>', '', h); h = re.sub(r'<(\w+)>\s*</\1>', '', h)
    open('%s/%s.html' % (OUT, fname), 'w', encoding='utf-8').write(head(title) + '<div class="doc">%s</div>\n' % h + tail())


sheet_page('day18-3QlV0Qdj', 'Figma-カラー/機能', ['day18-3QlV0Qdj.csv'])
sheet_page('day18-s8a8NqF2', 'Figma-カラー/バリアブル運用', ['day18-s8a8NqF2.csv'])
sheet_page('day20-opjVEsl9', 'ROS「ノード」', ['day20-opjVEsl9.csv'])
sheet_page('day20-wWuDrA1m', 'UnityxROS解説', ['day20-wWuDrA1m.csv'])
sheet_page('day21-rHvWywD8', 'ROS 2 基本コマンド（エラー対応メモ）', ['day21-rHvWywD8.csv'])
sheet_page('day22-q3Z4LUDY', ['P2M テックログ：ROS 2 基本コマンド（エラー対応メモ）', 'P2M テックログ：デバック情報まとめ'], ['t1035248351.csv', 't220498241.csv'])
sheet_page('day25-LBQh79h4', 'Unity-視点操作ガイド', ['day25-LBQh79h4.csv'])
sheet_page('day31-jdGUh_aI', 'ロボティクスUXデザイナー戦略', ['day31-jdGUh_aI.csv'])
sheet_page('day33-R9vno25x', 'フィジカルAIによるパラダイムシフト', ['day33-R9vno25x.csv'])
sheet_page('day34-kFbqYm6g', 'フェーズ3', ['day34-kFbqYm6g.csv'])
sheet_page('day37-5nNtmwHN', 'フェーズ3', ['day37-5nNtmwHN.csv'])
sheet_page('day43-DNB5IfRA', '使用するツールセット', ['day43-DNB5IfRA.csv'])
sheet_page('day43-rk6cAPq3', '実験用パラメーター表', ['day43-rk6cAPq3.csv'])
sheet_page('day51-PCdp7_Yb', 'FigJam-ライブラリ部品', ['day51-PCdp7_Yb.csv'])
doc_page('day78-XSSoeDI1', 'デジタルツイン学習-R2R2R LP構成素案', 'd78.html')
print('ok')
```


## 作業ログ ― 記事のグラフ画像7枚を、数を変えずに描き直す

導入シミュレーションの記事には、コードで描いたグラフの画像（PNG）がある。206枚の画像を一覧にして見て、画面写真・Unity の画面・前の案と後の案を比べた平面図は、その時の見た目を残すための画像なので外した。描き直したのは、元のデータか作ったコードが残っている7枚。

| 画像 | 描き方 |
|---|---|
| LOG452_08 10分ごとの線グラフ | 依頼96 の CSV から。5台が青、2台が灰 |
| LOG456_07 待機点の直し方の図 | LOG456_sources の fig_fix_options.py の写し。待機点が青、戻ってくる台は黒い縁 |
| LOG456_08・LOG456_09・LOG468_01 詰まりの地図 | make_jam_map.py。件数と秒数が元の図と同じことを確かめた |
| LOG468_02 棒グラフ | LOG468 の表の中央値。今が青の塗り、前が青の斜線、人の道をふさいだ分が赤 |
| LOG468_04 待機場所のまわりの図 | make_jam_map.py の下地 |

元の画像は R2 に残し、新しい画像は名前の最後に `_v2` を付けて入れた。

LOG468_02 は、最初の見本の凡例に「オレンジの斜線」が無かった。

> 赤斜めの意味は？

色（青＝そのほかの呼び出し、赤＝人の道をふさいでいた分）と模様（塗り＝今、斜線＝前）に別々の意味を持たせていたのに、凡例は3つしかなかった。凡例を4つにした。

![LOG468_02 の前と後。前は灰と黒の棒と白い斜線。後は今の決まりが青の塗り、前の決まりが青の斜線、呼び出しのうち人の道をふさいだ分が赤で、凡例は4つ](https://pixelstomotors.com/media/LOG469_06_bar_graph_before_after.png)

![LOG468_01 の詰まりの地図の前と後。地図の通れない所が黄から灰に、待機点が薄い青の四角に、詰まりの円がオレンジになった](https://pixelstomotors.com/media/LOG469_07_jam_map_before_after.png)

描き直さなかった図もある。作ったコードが見つからない図（LOG456_03・04・05、LOG423_04、LOG452_04）は、手で描き直すと中身が変わるおそれがある。LOG456_06 は、当時の2列の待機点の店の形が残っていない。店の平面図の上に描いた止まりの地図（LOG398_04、LOG402_04、LOG423_01〜03）は、下地の平面図がサイトの試算と同じ部品で、ロボットの道がオレンジのまま。直すには平面図の部品から変えることになり、サイトの試算の見た目も変わるので、エイイチの判断を待っている。

### iCloud の graph_restyle_20261108/README.md

```markdown
# 導入シミュレーションの記事のグラフを、決まりの色と字で描き直したもの（2026-11-08）

色と字の決まりは KIOXIA の service_sim/status_videos/tools/STYLE.md（style.py）。数と形は元の図のまま、色と字だけを変えた。
元の画像は消さずに残し、記事は新しい名前（末尾 _v2）に差し替えた。

| 新しい画像 | 作り方 | 元のデータ |
|---|---|---|
| LOG452_08_p6_10min_v2.png | fig_452_08.py <csv> <出力> | KIOXIA unity/rec_large_20261106/large_p6/compare_r2_r5/compare_r2_r5_10min.csv |
| LOG456_07_fix_options_v2.png | fig_fix_options_v2.py <tools> <出力> | 絵（LOG456_sources/fig_fix_options.py の写し） |
| LOG456_08_straight_entry_112_v2.png | tools/make_jam_map.py（--set 3つ：sm101p6on cs87・sb112ret cs97・sb112se cs97） | KIOXIA unity/night/20261107_sb112/ep_*_r5.csv と回のフォルダ |
| LOG456_09_straight_entry_114_v2.png | tools/make_jam_map.py（--set 2つ：sb114ret・sb114se cs99） | KIOXIA unity/night/20261107_sb114/ep_*_r5.csv と回のフォルダ |
| LOG468_01_where_jams_sb119_v2.png | tools/make_jam_map.py（--set 2つ：sb119ret・sb119se cs101） | KIOXIA unity/night/20261107_sb119/ep_*_r5.csv と回のフォルダ |
| LOG468_02_people_avoid_effect_v2.png | fig_468_02.py <出力> | LOG468 の記事の表（依頼114・119 の中央値） |
| LOG468_04_standby_area_map_v2.png | fig_468_04.py <tools> <店の形のフォルダ> <出力> | p2m-site public/sim/layouts/large/unity/p6_large_center（large-99db3254b08b） |

店の形は p2m-site の public/sim/layouts/large/unity/ を使った。
```

### graph_restyle_20261108/fig_452_08.py

```python
#!/usr/bin/env python3
# LOG452_08 ⑧ロボット2台と5台の10分ごとの数を、STYLE.md の色と字で描き直す（2026-11-08）
# 数は依頼96 の cmp96.py が出した compare_r2_r5_10min.csv そのまま。灰の帯＝動画で長めに見せた30分（notes の 6900〜8700 秒）
import csv, sys
sys.path.insert(0, '/Volumes/KIOXIA/r2r2r-data/service_sim/status_videos/tools')
import style as st
st.mpl_setup()
import matplotlib.pyplot as plt

src, out = sys.argv[1], sys.argv[2]
R = list(csv.DictReader(open(src, encoding='utf-8-sig')))
X = [int(r['開店から（分）'].split('〜')[0]) + 5 for r in R]
keys = [k[:-3] for k in R[0] if k.endswith('_2台')]
NAME = {'入口で待つ組': '入口で待つ組', '席にいる組': '席にいる組', '片付けを待つ卓': '片付けを待つ卓',
        'ロボットの止まり（秒）': 'ロボットの止まり（秒・10分ごと）'}


def label(k):
    if k in NAME: return NAME[k]
    a, b = k.split('_', 1)
    return '%s：%s（%s）' % (a, b, '人' if a == 'スタッフ' else '台')


def nice(v):
    import math
    if v <= 0: return 1
    e = 10 ** math.floor(math.log10(v / 4))
    for m in (1, 2, 2.5, 5, 10):
        if v / (m * e) <= 4: return m * e
    return 10 * e


fig, axs = plt.subplots(5, 4, figsize=(20, 19), sharex=True)
for ax, k in zip(axs.flat, keys):
    y2 = [float(r[k + '_2台']) for r in R]; y5 = [float(r[k + '_5台']) for r in R]
    ax.axvspan(115, 145, color=st.BAND, zorder=0, linewidth=0)
    ax.plot(X, y2, color=st.QUIET, lw=1.8, solid_capstyle='butt', solid_joinstyle='round', label='2台（比べる相手）')
    ax.plot(X, y5, color=st.MAIN, lw=2.2, solid_capstyle='butt', solid_joinstyle='round', label='5台')
    ax.set_title(label(k), fontsize=13, loc='left', color=st.INK)
    ax.set_facecolor(st.SHEET)
    top = max(max(y2), max(y5))
    s = nice(top)
    ax.set_ylim(0, max(s, (int(top / s) + 1) * s)); ax.set_yticks([i * s for i in range(int(ax.get_ylim()[1] / s) + 1)])
    ax.yaxis.grid(True, color=st.RULE, lw=0.8); ax.set_axisbelow(True)
    for sp in ('top', 'right'): ax.spines[sp].set_visible(False)
    ax.spines['left'].set_color(st.FAINT); ax.spines['bottom'].set_color(st.FAINT)
    ax.tick_params(length=0, labelsize=10)
for ax in axs.flat[len(keys):]:
    ax.axis('off')
n = len(keys)
for i, ax in enumerate(axs.flat[:n]):
    if i + 4 >= n:   # その下に図が無いものに、横の目盛りと名前を付ける
        ax.tick_params(labelbottom=True); ax.set_xlabel('開店からの分', color=st.MUTED)
axs.flat[0].legend(frameon=False, fontsize=12, loc='upper left')
fig.suptitle('⑧ ロボット2台 と 5台（10分ごとの平均・灰の帯＝動画で長めに見せた30分）', fontsize=16, x=0.01, ha='left', weight='bold')
fig.tight_layout(rect=(0, 0, 1, 0.97))
fig.savefig(out, dpi=110)
print(out)
```

### graph_restyle_20261108/fig_468_02.py

```python
#!/usr/bin/env python3
# LOG468_02 人が待機点を通らない形の効果（⑧・5台・10回の中央値）を、STYLE.md の色と字で描き直す（2026-11-08）
# 数は LOG468 の表（依頼114・依頼119 の中央値）そのまま
import sys
sys.path.insert(0, '/mnt/user-data/outputs/tools')
import style as st
st.mpl_setup()
import matplotlib.pyplot as plt
import matplotlib.patches as mp
plt.rcParams['hatch.linewidth'] = 1.4

NAMES = ['斜めに寄る\n人は通る', 'まっすぐ入る\n人は通る', '斜めに寄る\n人は通らない', 'まっすぐ入る\n人は通らない']
AFTER = [False, False, True, True]          # 主役＝人は通らない（塗り）、比べる相手＝人は通る（斜線）
JAM = [3317, 1255, 2115, 809]
CALL = [115.5, 180.0, 31.0, 22.0]
BLK = [85.0, 164.5, 4.0, 3.0]


def bar(ax, x, y0, h, color, after):
    if after:
        ax.bar(x, h, 0.62, bottom=y0, color=color, edgecolor=color, linewidth=0)
    else:
        ax.bar(x, h, 0.62, bottom=y0, color=st.SHEET, edgecolor=color, hatch='//////', linewidth=0)


def frame(ax, title, ymax, step):
    ax.set_facecolor(st.SHEET)
    for s in ('top', 'right'):
        ax.spines[s].set_visible(False)
    ax.spines['left'].set_color(st.FAINT); ax.spines['bottom'].set_color(st.FAINT)
    ax.set_ylim(0, ymax); ax.set_yticks(range(0, ymax + 1, step))
    ax.yaxis.grid(True, color=st.RULE, linewidth=0.8); ax.set_axisbelow(True)
    ax.tick_params(length=0, labelsize=10)
    ax.set_xticks(range(4)); ax.set_xticklabels(NAMES, fontsize=10, color=st.INK)
    ax.set_title(title, fontsize=13, weight='bold', loc='left', pad=12)


fig, axs = plt.subplots(1, 2, figsize=(16, 6.6))
ax = axs[0]
frame(ax, '待機場所のそばの30秒以上の詰まり（1回の営業あたり）', 3500, 500)
for i in range(4):
    bar(ax, i, 0, JAM[i], st.MAIN, AFTER[i])
    ax.text(i, JAM[i] + 50, '{:,}秒'.format(JAM[i]), ha='center', va='bottom', fontsize=11, weight='bold')

ax = axs[1]
frame(ax, '30秒の呼び出し（1回の営業あたり）', 200, 50)
for i in range(4):
    bar(ax, i, 0, CALL[i] - BLK[i], st.MAIN, AFTER[i])
    bar(ax, i, CALL[i] - BLK[i], BLK[i], st.BAD, AFTER[i])
    ax.text(i, CALL[i] + 3, '{:g}回'.format(CALL[i]), ha='center', va='bottom', fontsize=11, weight='bold')
    ax.text(i, CALL[i] + 11.5, 'うち人の道 {:g}'.format(BLK[i]), ha='center', va='bottom', fontsize=9, color=st.BAD)

H = '//////'
leg = [mp.Patch(facecolor=st.MAIN, edgecolor=st.MAIN, label='今：人は待機点を通らない（依頼119）'),
       mp.Patch(facecolor=st.SHEET, edgecolor=st.MAIN, hatch=H, label='前：人は待機点を通る（依頼114）'),
       mp.Patch(facecolor=st.BAD, edgecolor=st.BAD, label='今の呼び出しのうち、止まった台が人の道をふさいでいた分'),
       mp.Patch(facecolor=st.SHEET, edgecolor=st.BAD, hatch=H, label='前の呼び出しのうち、止まった台が人の道をふさいでいた分')]
fig.legend(handles=leg, loc='upper center', ncol=2, frameon=False, fontsize=10, bbox_to_anchor=(0.5, 0.995), columnspacing=3)
fig.suptitle('大型ファミレス⑧・配膳ロボット5台（Unity のシミュレーション・種0〜9の10回の中央値）', fontsize=10, color=st.MUTED, y=0.035)
fig.subplots_adjust(left=0.05, right=0.99, top=0.80, bottom=0.2, wspace=0.14)
fig.savefig(sys.argv[1], dpi=120)
print(sys.argv[1])
```

### graph_restyle_20261108/fig_468_04.py

```python
# LOG468_04 ⑧のパントリーの前を上から見た図。make_jam_map.py の下地（STYLE.md の色と字）で描き直した版（2026-11-08）
# 使い方: python3 fig_468_04.py <tools のフォルダ> <店の形のフォルダ> <出力.png>
import sys
sys.path.insert(0, sys.argv[1])
import make_jam_map as J
import matplotlib.pyplot as plt
lay, m = J.load_layout(sys.argv[2], 'p6_large_center')
fig, ax = plt.subplots(figsize=(9.33, 4.67))
J.draw_base(ax, lay, m, (14, 30, 7, 15))
fig.savefig(sys.argv[3], dpi=120, bbox_inches='tight'); print(sys.argv[3])
```

### graph_restyle_20261108/fig_fix_options_v2.py

```python
# LOG456_07 ⑧ロボットの待機点の直し方（上から見た図）。fig_fix_options.py の形と中身はそのまま、色と字を STYLE.md にそろえた版（2026-11-08）
# 使い方: python3 fig_fix_options_v2.py <style.py のあるフォルダ> <出力.png>
import sys
sys.path.insert(0, sys.argv[1])
import style as st
st.mpl_setup()
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt, matplotlib.patches as mp
plt.rcParams['hatch.linewidth'] = 0.8
R = 0.25; SENS = 0.85; HALF = 35
COME = st.INK          # 戻ってくる台と、その道（灰の地の上で目立つ黒）
COME_FILL = st.BAND


def robot(ax, x, y, col=st.ROBOT, ec=st.INK, lab=None, z=5, lw=1.2):
    ax.add_patch(mp.Circle((x, y), R, fc=col, ec=ec, lw=lw, zorder=z))
    if lab: ax.text(x, y, lab, ha='center', va='center', fontsize=9, zorder=z + 1, color=st.INK)


def fan(ax, x, y, deg, col):
    ax.add_patch(mp.Wedge((x, y), SENS, deg - HALF, deg + HALF, fc=col, ec='none', alpha=0.28, zorder=3))


def base(ax, title, verdict, vcol):
    ax.set_facecolor(st.SHEET)
    ax.add_patch(mp.Rectangle((13.0, 11.8), 11.6, 0.6, fc=st.MAP_WALL, ec='none')); ax.text(15.0, 12.1, 'パントリー（壁）', fontsize=10, color=st.MUTED, va='center')
    ax.add_patch(mp.Rectangle((15.5, 10.6), 3.0, 1.2, fc='none', ec=st.BAD, hatch='////', lw=0, alpha=0.55))
    ax.plot(17.0, 11.45, '*', ms=15, color=st.INK, zorder=6); ax.text(17.0, 10.45, '受け渡し口の前\n（あけておく所）', ha='center', va='top', fontsize=9, color=st.BAD)
    ax.add_patch(mp.Rectangle((24.7, 10.2), 0.9, 1.6, fc=st.SHEET, ec=st.FAINT, lw=1)); ax.text(25.15, 11.0, 'T57', ha='center', va='center', fontsize=9, color=st.FAINT)
    ax.add_patch(mp.Rectangle((23.7, 9.0), 1.0, 2.8, fc=st.BAND, ec='none')); ax.text(24.2, 9.15, '右の卓へ\n上がる道', ha='center', va='bottom', fontsize=8, color=st.MUTED)
    ax.set_xlim(12.8, 25.8); ax.set_ylim(8.9, 12.5); ax.set_aspect('equal'); ax.set_xticks([]); ax.set_yticks([])
    for s in ax.spines.values(): s.set_visible(False)
    ax.set_title(title, loc='left', fontsize=14, fontweight='bold', color=st.INK)
    ax.text(1.0, 1.02, verdict, transform=ax.transAxes, ha='right', va='bottom', fontsize=11.5, color=st.SHEET, fontweight='bold',
            bbox=dict(boxstyle='round,pad=0.35,rounding_size=0.2', fc=vcol, ec='none'))


def slots(ax, xs, y=11.35, occ=None):
    for i, x in enumerate(xs):
        ax.add_patch(mp.Rectangle((x - 0.4, y - 0.4), 0.8, 0.8, fc=st.MAIN_SOFT, ec=st.MAIN, ls='--', lw=1, zorder=2))
        if occ is None or i in occ: robot(ax, x, y, lab=str(i + 1))


def gap(ax, x1, x2, y, txt):
    ax.annotate('', xy=(x1, y), xytext=(x2, y), arrowprops=dict(arrowstyle='<->', color=st.MUTED, lw=1))
    ax.text((x1 + x2) / 2, y + 0.06, txt, ha='center', va='bottom', fontsize=9, color=st.INK)


def come(ax, x, y, lab='3'):
    robot(ax, x, y, col=COME_FILL, ec=COME, lab=lab, lw=2)


ARW = dict(arrowstyle='->', color=COME, lw=2, connectionstyle='arc3,rad=-0.15')
staff = [(13.75, 11.25), (15.75, 11.25)]
fig, axs = plt.subplots(4, 1, figsize=(11, 15.5), dpi=130)
ax = axs[0]; xs = [19.1 + 0.8 * i for i in range(6)]; base(ax, '今：1列・間0.8m・6点', '× 隣の台がセンサーに入って止まる', st.BAD)
slots(ax, xs, occ=[0, 1, 3, 4]); gap(ax, xs[3], xs[4], 11.75, '0.8m')
rx, ry = 20.45, 10.9; ax.annotate('', xy=(xs[2], 11.2), xytext=(19.0, 9.3), arrowprops=ARW)
fan(ax, rx, ry, 120, st.BAD); come(ax, rx, ry)
ax.text(18.0, 9.3, '戻ってくる台3は斜めに寄るので、\n隣の台2がセンサー（薄い扇）に入る', fontsize=10, va='bottom', ha='right', color=st.INK)
ax = axs[1]; xs = [18.9 + 1.1 * i for i in range(5)]; base(ax, '(1) 1列・間1.1m・5点', '△ 斜めに寄ると、まだ隣に届くことがある', st.BAD)
slots(ax, xs, occ=[0, 1, 3, 4]); gap(ax, xs[3], xs[4], 11.75, '1.1m')
rx, ry = xs[2] - 0.35, 10.88; ax.annotate('', xy=(xs[2], 11.2), xytext=(19.3, 9.3), arrowprops=ARW)
fan(ax, rx, ry, 120, st.BAD); come(ax, rx, ry)
ax.text(18.6, 9.3, 'これより広い間は取れない\n（左の赤い斜線の所と右の灰の道があるため）', fontsize=10, va='bottom', ha='right', color=st.INK)
ax = axs[2]; xs = [19.1 + 0.8 * i for i in range(6)]; base(ax, '(2) 間0.8mのまま、真下まで来てからまっすぐ入る', '○ まっすぐ入るので、横の台がセンサーに入らない', st.GOOD)
slots(ax, xs, occ=[0, 1, 3, 4]); gap(ax, xs[3], xs[4], 11.75, '0.8m')
ax.plot([19.0, xs[2]], [9.3, 10.0], color=COME, lw=2, solid_capstyle='butt'); ax.annotate('', xy=(xs[2], 11.15), xytext=(xs[2], 10.0), arrowprops=dict(arrowstyle='->', color=COME, lw=2))
ax.plot(xs[2], 10.0, 'o', ms=6, color=COME); ax.text(xs[2] + 0.15, 9.95, '入る点（待機点の真下）', fontsize=9, va='top', color=st.INK)
fan(ax, xs[2], 10.45, 90, st.GOOD); come(ax, xs[2], 10.45)
ax.text(18.6, 9.3, '店の形は今と同じ。\nロボットの入り方の決まりを足す', fontsize=10, va='bottom', ha='right', color=st.INK)
ax = axs[3]; xs = [19.0 + 1.2 * i for i in range(4)]; base(ax, '(3) 1列・間1.2m・4点 ＋ 受け渡し口の左に1点', '○ 間は広いが、スタッフの持ち場を動かす', st.GOOD)
slots(ax, xs); gap(ax, xs[2], xs[3], 11.75, '1.2m')
slots(ax, [14.9], occ=[]); robot(ax, 14.9, 11.35, lab='5')
for (x, y) in staff:
    ax.add_patch(mp.RegularPolygon((x, y), 3, radius=0.2, fc=st.FAINT, ec='none', zorder=6))
ax.text(14.2, 10.55, '灰の三角＝今のスタッフの持ち場\n（重なる所は動かす）', fontsize=9, color=st.MUTED, ha='center', va='top')
fig.suptitle('⑧ ロボットの待機点の直し方（上から見た図）\n白い丸＝待っているロボット、黒い縁の灰の丸＝戻ってくるロボット、薄い扇＝前のセンサー（中心から0.85m。形は簡単にした絵）',
             x=0.02, ha='left', fontsize=12, color=st.INK)
plt.tight_layout(rect=(0, 0, 1, 0.955)); plt.savefig(sys.argv[2], facecolor=st.PAPER)
print(sys.argv[2])
```

### 詰まりの地図を描き直したコマンド（LOG456_08・09）

```bash
T=~/mnt/KIOXIA/r2r2r-data/service_sim/status_videos/tools; U=~/mnt/KIOXIA/r2r2r-data/service_sim/unity; N=$U/night; O=~/mnt/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/LOG456_images
cd $T && python3 -B make_jam_map.py $O/LOG456_08_straight_entry_112_v2.png --layout-dir ~/lay --store p6_large_center \
 --set "$N/20261107_sb112/ep_sm101p6on_r5.csv|$U/20261107_sm101p6on_cs87_large_p6_r5_s*|今のまま（sm101p6on）" \
 --set "$N/20261107_sb112/ep_sb112ret_r5.csv|$U/20261107_sb112ret_cs97_large_p6_r5_s*|どかされた台が戻る（sb112ret）" \
 --set "$N/20261107_sb112/ep_sb112se_r5.csv|$U/20261107_sb112se_cs97_large_p6_r5_s*|戻る＋まっすぐ入る（sb112se）" \
 --note "⑧5台・すぐ押す（種0〜9 の 10 回）"
python3 -B make_jam_map.py $O/LOG456_09_straight_entry_114_v2.png --layout-dir ~/lay --store p6_large_center \
 --set "$N/20261107_sb114/ep_sb114ret_r5.csv|$U/20261107_sb114ret_cs99_large_p6_r5_s*|どかされた台が戻る・斜めに寄る（sb114ret）" \
 --set "$N/20261107_sb114/ep_sb114se_r5.csv|$U/20261107_sb114se_cs99_large_p6_r5_s*|戻る＋まっすぐ入る・順番待ちは止まりにしない（sb114se）" \
 --note "⑧5台・すぐ押す（種0〜9 の 10 回）"
```

```
/sessions/rcw-01levextvchrhzmgcuinsz1t/mnt/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/LOG456_images/LOG456_08_straight_entry_112_v2.png
/sessions/rcw-01levextvchrhzmgcuinsz1t/mnt/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/LOG456_images/LOG456_09_straight_entry_114_v2.png
```

店の形（p6_large_center.json と p6_large_center_map.json）は、サイトの公開版（/sim/layouts/large/unity/）から取った。件数と秒数は、LOG456_08 が 368件・22,688秒／523件・31,746秒／277件・16,485秒、LOG456_09 が 537件・32,885秒／228件・12,442秒で、元の図と同じだった。


## 作業ログ ― 残りの図解283本を、9つに分けて同時に仕上げる

1回目で直したのは、LOG と DAY334 以降の記事の図解131本だった。サイトの図解を数えると全部で439本あり、Studio から移した記事の、名前の付いた図解と、DAY1〜100台の図解（day-○○ の名前）が、まだ古い見た目のまま残っていた。

> 図解HTMLはサイトの記事全体にあるのですが、その修正はどんなスケジュールですか？

| まとまり | 本数 | どうしたか |
|---|---|---|
| LOG と DAY334 以降の記事の図解 | 131 | 1回目で直した |
| 名前の付いた図解（Studio から移した記事） | 177 | 2回目で直した |
| DAY1〜100台の図解 | 88 | 2回目で直した |
| YouTube・Figma の埋め込み | 18 | 2回目で外側の形だけ直した |
| Google の埋め込み | 15 | サイトの中の表に作り直した（上の節） |
| トップページの部品など、記事の図解ではないもの | 4 | 触らない |
| どの記事でも使われていないもの | 6 | 触らない |

1回目と同じく、見本3本（SSH の速さの図、Motion UX 三相モデル、j0 の届く範囲の図）を先に見てもらった。古い図解は色を CSS の変数（--black・--white・--gray-mid など）で書いていて、1回目のスクリプトでは黒い枠の箱を見つけられなかった。変数の名前と、決まりにない灰（#e8e8e3・#999・#444 など）を置き換えるスクリプトを足した。見本の SSH の図では、黒く塗った「×50 FASTER」の箱が灰の面に変わり、白い字が消えて空の箱になっていた。黒く塗った箱は黒のまま残すようにスクリプトを直した。

### /tmp/claude-0/palette2.py（古い図解の色の変数と灰を置き換える）

```python
"""古い図解の色の変数と灰色を、決まりの色に置き換える（2026-11-08・2回目のまとまり用）"""
import re, sys, pathlib
MAP = [(r'#e8e8e3|#e8e6df|#e4e4dc', '#ececec'), (r'#c9c9c3|#cfcfc9|#cdc9bf|#d6d6d0|#dcdcd6', '#e6e6e6'),
       (r'#999999\b|#999\b|#aaa\b|#8a8a84|#8a8a86|#8c8c86', '#888'), (r'#444444\b|#444\b|#6f6f6a|#6a6a65|#6a635a|#666\b|#777\b', '#555'),
       (r'#1a1a1a', '#242424')]
INKV = r'var\(--(?:black|ink|fg|blk|bk)\)'
for name in open(sys.argv[1]).read().split():
    p = pathlib.Path('public/embeds') / name; h = p.read_text(encoding='utf-8'); o = h
    for a, b in MAP: h = re.sub(a, b, h, flags=re.I)
    # 枠の 1.5px の黒 → 1px（中の箱は後で面にする）
    h = re.sub(r'border(-\w+)?\s*:\s*1\.5px\s+solid\s+(' + INKV + r'|#242424)', r'border\1:1px solid \2', h)
    if h != o: p.write_text(h, encoding='utf-8')
```

見本が通ったあと、スクリプトで共通の所を入れ、266本を9つのまとまり（29〜30本ずつ）に分けて、Claude の作業を同時に9つ動かした。それぞれが決まりの文（round2_spec.md）と見本3本を読み、1本ずつ直しては画像にして確かめた。

| まとまり | 本数 | かかった時間 | 使ったトークン |
|---|---|---|---|
| 0 | 30 | 18分25秒 | 562,001 |
| 1 | 30 | 17分27秒 | 479,392 |
| 2 | 30 | 16分27秒 | 438,530 |
| 3 | 30 | 18分11秒 | 522,663 |
| 4 | 30 | 16分53秒 | 419,373 |
| 5 | 29 | 14分57秒 | 485,777 |
| 6 | 29 | 15分08秒 | 500,020 |
| 7 | 29 | 16分36秒 | 439,285 |
| 8 | 29 | 17分02秒 | 502,626 |
| 計 | 266 | 同時に動かしたので約18分 | 4,349,667 |

9つのまとまりは、同じ作業用のフォルダを使っていた。あるまとまりが置いた置き換えのスクリプトを、別のまとまりが同じ名前で上書きする、ということが起きた。どのまとまりも途中で気づいて、自分の道具を別のフォルダに分けていた。

### /tmp/claude-0/round2_spec.md（2回目の、1本ずつ仕上げる作業の決まり）

```markdown
# 図解の仕上げ（2回目のまとまり・作業の決まり）

対象：/home/claude/p2m-site/public/embeds/ の、渡された図解 HTML だけ。ブランチ embeds-style-round2 の作業ツリーにある。
すでにスクリプトで「字（Inter＋Noto Sans JP）・下地の色（#242424 / #f7f7f7）・外側の白いカード・一部の箱の灰の面」が入っている。
スクリプトでは直しきれない所を、1本ずつ中身を読んで仕上げるのが仕事。直す前の元のファイルは /tmp/claude-0/before2/<同じ名前> にある（読むだけ）。

## まず読む見本（この3本と同じ密度・同じ見た目にそろえる）
- /home/claude/p2m-site/public/embeds/mecharm-ssh-overhead-40x-speedup-7Uxkyf0a.html（棒：待ち＝赤、改善後＝青。結果の黒い箱は黒のまま）
- /home/claude/p2m-site/public/embeds/motion-ux-three-phase-model-VWZggSw_.html（枠の箱→灰の面、札は白地＋#e6e6e6 の枠・角丸4px、比べる2つのうち主役を薄い青の面）
- /home/claude/p2m-site/public/embeds/j0-sweep-reachability-map-e8VFvq2d.html（測った点＝青、基準＝黒、補助の枠＝細い灰の点線）
画像で見たいときは `python3 /tmp/claude-0/render_one.py <ファイル>` → /tmp/claude-0/rc_png/<名前>.png を Read で開く。

## 形
1. 外側：白いカード（background:#fff・border-radius:8px・padding 28px 24px 前後）が1枚だけ。body は背景 #f7f7f7・padding:8px 0。
   - スクリプトが、幅の入れ物（max-width の要素）を白いカードにし、そのすぐ中の「枠のある大きな箱」（.frame など）を灰の面にしてしまっていることがある。外側の枠がカードの役なら、その中の箱は背景なし・枠なし・padding 0 にして、白いカードの上に直接のせる（白の中に灰の大箱が1枚だけ、を作らない）
   - 外側の黒い枠線は使わない
2. 中の箱（説明の欄・数字の欄・カード）：黒い枠をやめて #f7f7f7 の面・角丸 8px・枠なし。黒く塗られた箱（強調・結果・選んでいる物）は黒のまま角丸 8px（灰にしない）
3. ボタン・タブ・札（タグ）：角丸 4px。押していない物は白地＋1px #e6e6e6 の枠、選んでいる物は黒塗り＋白字。札に 8px の灰の面が付いていたら 4px の白＋#e6e6e6 枠に直す
4. データ（棒・帯・割合・面・線・軸・目盛り・地図の形）は角丸にしない
5. 区切りの線：段落や欄を分けるだけの線は #e6e6e6 の 1px。表の見出しの下・グラフの軸は黒（#242424）のままでよい
6. 模様：縦じま・太い斜線・横じまは使わない。比べる相手は細かい斜線 `repeating-linear-gradient(-45deg,色 0 2px,transparent 2px 4px)`
7. 灰色：#888（目立たせない字・小さいラベル）、#555（補足の字）、#e6e6e6（細い区切り）、#ececec（帯の地・トラック）。これ以外の灰を新しく足さない。opacity で字を薄くしている所は、そのままでもよい

## 色（役割で決める。これ以外の色を足さない）
- 主役（その図で一番見てほしいもの・今・後・選んだ物・自分の案・測った値）：青 #2f6fdf。面に薄く敷くときは #e8f0ff
- 比べる相手（前・基準・試算・もう一方）：主役と同じ色の細かい斜線、または灰 #9a9a9a
- 悪い（詰まり・待ち・止まり・失敗・エラー・危険・増えて困るもの）：赤 #d85a30（薄い面 #fbe6de）
- 良い（合格・成功・完了・安全・減って良くなったもの）：緑 #1d9e75（薄い面 #dff3ec）
- 記録だけ・取りやめ・試していない・関係ない物：灰 #9a9a9a
- 1つの図の中で青に2つの意味を持たせない。凡例があれば凡例の色と言葉も合わせる（「黒が〜」→「赤が〜」など、色を説明している言葉だけは直してよい）
- 主役が決められない図（手順・一覧・文章・用語の図）は、無理に色を入れず黒と灰のままでよい
- 元から色が付いている図（赤・緑・青・黄・紫など）は、上の役割の色に置き換える。意味が合う色がなければ、黒・灰に落とす。黄色 #e8c520 系はお客さんの色なので使わない（警告の黄色は赤に）
- ロボットの関節や軸（X=赤・Y=緑・Z=青のような、決まった色の約束を説明している図）は、その約束どおりの色を残してよい（記事の中身なので）。その場合も色の値だけ近い決まりの色（#d85a30・#1d9e75・#2f6fdf）にそろえる

## 線の強さ
- 目盛り・区切り ＜ 軸 ≦ データの線 ＜ 選んだ物の囲み・強調。この順番が逆になっている所だけ直す。太さの数字をそろえることはしない（細い線のほうがよい所は細いままでよい）
- 線グラフの線は stroke-linecap:butt・stroke-linejoin:round

## 変えてはいけないもの
- 数字・データ・文の中身・作り・動き（JS の動作）・最後にある iframe の高さを送る script（p2mHeight を postMessage するもの）
- 渡されたファイル以外は触らない。git の add・commit・push はしない（まとめ役がする）
- YouTube や Figma の iframe を中に持つ図は、外側の形だけ直し、iframe には触らない

## 確かめ方
- 1本直すごとに render_one.py で画像にして Read で見る。字の重なり・はみ出し・空の箱（白字が白い面に乗って消えている、など）・色の意味のずれがないかを見る。動く図・押して出る図は最初の画面で見えていればよい
- 壊れている所（CSS の書き間違いで崩れている・高さの script が無い・画面の外にはみ出す）を見つけたら、直してよい。直したら返事に書く
- 終わったら、ファイルごとに1行で「主役に何を青にしたか／赤・緑・灰を何に使ったか／色を入れなかった理由／直した壊れ」を返す。最後に、迷ったもの・自信のないものを別に挙げる
```

途中で、トップページの部品（top-video と top-banner）も直す対象に入っていたことに気づいた。

> バナーを修正しろという指示はしていません。関係ないページまで修正しないように注意してください。

記事の図解として使われていない部品（top-video・top-banner・mecharm_data_blueprint・series4-2）は、公開前に元に戻した。公開後、本番の top-video と top-banner が、作業の前とバイト単位で同じことを確かめた。

最後に、283本すべてを PC の幅（760px）とスマホの幅（390px）で画面に出した。高さを親のページに伝える script は283本すべてに1つずつ残っていた。スマホの幅で横にはみ出したのは4本（day112・day150・day151・day92）で、どれも直す前からはみ出していた。day92 だけは直す前より広がっていたので直した。

![2回目に直した図解のうち35本を並べたもの。どれも白いカードで、主役が青、悪い所が赤、比べる相手が灰か斜線になっている](https://pixelstomotors.com/media/LOG469_08_embeds_after_overview.png)

### /tmp/claude-0/render_one.py（1本を画像にして確かめる）

```python
# 使い方: python3 /tmp/claude-0/render_one.py <図解のHTMLのパス>...  → /tmp/claude-0/rc_png/<名前>.png（幅760・2倍）
import re, sys, pathlib
from playwright.sync_api import sync_playwright
T='/mnt/user-data/outputs/tools/fonts/'
R="unicode-range:U+0000-00D6,U+00D8-024F,U+2000-206F"
FACES=("@font-face{font-family:'Inter';font-weight:400 500;src:url(file://%sInter-Regular.ttf);%s}"
 "@font-face{font-family:'Inter';font-weight:600 900;src:url(file://%sInter-Bold.ttf);%s}"
 "@font-face{font-family:'Noto Sans JP';font-weight:100 500;src:local('Noto Sans CJK JP')}"
 "@font-face{font-family:'Noto Sans JP';font-weight:600 900;src:local('Noto Sans CJK JP Bold')}") % (T,R,T,R)
with sync_playwright() as p:
    b=p.chromium.launch(); pg=b.new_page(viewport={'width':760,'height':900},device_scale_factor=2)
    for src in sys.argv[1:]:
        src=str(pathlib.Path(src).resolve())
        h=open(src,encoding='utf-8').read()
        h=re.sub(r'<link[^>]+fonts\.googleapis[^>]+>','',h).replace('<head>','<head><style>'+FACES+'</style>',1)
        t=pathlib.Path('/tmp/claude-0/rc_png/_tmp_'+pathlib.Path(src).name); t.write_text(h,encoding='utf-8')
        pg.set_viewport_size({'width':760,'height':900}); pg.goto('file://'+str(t)); pg.wait_for_timeout(3000)
        hg=min(pg.evaluate('document.documentElement.scrollHeight'),1800); pg.set_viewport_size({'width':760,'height':hg})
        out='/tmp/claude-0/rc_png/'+pathlib.Path(src).stem+'.png'; pg.screenshot(path=out); t.unlink(); print(out)
    b.close()
```

### /tmp/claude-0/render_many.py（全部を同時に画像にし、はみ出しを数える）

```python
import re, sys, pathlib
from concurrent.futures import ThreadPoolExecutor
from playwright.sync_api import sync_playwright
T='/mnt/user-data/outputs/tools/fonts/'
R="unicode-range:U+0000-00D6,U+00D8-024F,U+2000-206F"
FACES=("@font-face{font-family:'Inter';font-weight:400 500;src:url(file://%sInter-Regular.ttf);%s}"
 "@font-face{font-family:'Inter';font-weight:600 900;src:url(file://%sInter-Bold.ttf);%s}"
 "@font-face{font-family:'Noto Sans JP';font-weight:100 500;src:local('Noto Sans CJK JP')}"
 "@font-face{font-family:'Noto Sans JP';font-weight:600 900;src:local('Noto Sans CJK JP Bold')}") % (T,R,T,R)
names=[l.strip() for l in open(sys.argv[1]) if l.strip()]; W=int(sys.argv[2]); out=pathlib.Path(sys.argv[3]); out.mkdir(exist_ok=True)
def run(chunk):
    with sync_playwright() as p:
        b=p.chromium.launch(); pg=b.new_page(viewport={'width':W,'height':900},device_scale_factor=1)
        for n in chunk:
            src=pathlib.Path('/home/claude/p2m-site/public/embeds')/n
            h=src.read_text(encoding='utf-8')
            h=re.sub(r'<link[^>]+fonts\.googleapis[^>]+>','',h); h=re.sub(r"@import url\('https://fonts[^)]*\);",'',h)
            h=('<style>'+FACES+'</style>')+h
            t=out/('_t_'+n); t.write_text(h,encoding='utf-8')
            try:
                pg.set_viewport_size({'width':W,'height':900}); pg.goto('file://'+str(t),timeout=15000); pg.wait_for_timeout(1500)
                hg=min(pg.evaluate('document.documentElement.scrollHeight'),2400); sw=pg.evaluate('document.documentElement.scrollWidth')
                pg.set_viewport_size({'width':W,'height':max(hg,100)}); pg.screenshot(path=str(out/(n[:-5]+'.png')))
                if sw>W+2: print('OVERFLOW',n,sw,flush=True)
            except Exception as e: print('ERR',n,e,flush=True)
            t.unlink()
        b.close()
k=6; chunks=[names[i::k] for i in range(k)]
with ThreadPoolExecutor(k) as ex: list(ex.map(run,chunks))
print('done')
```

### スマホの幅（390px）で横にはみ出した図解

```
OVERFLOW day112-W9yz_lYl.html 408
OVERFLOW day150-N7MVWVrc.html 402
OVERFLOW day151-Q4aHCIRt.html 640
OVERFLOW day92-JQEFn0GS.html 455
4
```

```
直す前の同じ4本
OVERFLOW _b_day150-N7MVWVrc.html 400
OVERFLOW _b_day112-W9yz_lYl.html 405
OVERFLOW _b_day151-Q4aHCIRt.html 640
OVERFLOW _b_day92-JQEFn0GS.html 402
```

図の中の字の中身に手が入った所が2つある。measuring-the-stuck-joint は、軸の向きの字（浅い・深い）が点の並びと左右逆だった。JS のコメントには「-40（右/浅い）」と書いてあるが、位置を出す式 `pct = v => ((A_HI - v) / (A_HI - A_LO)) * 100` では -40 が 0%（左）になる。字のほうを、実際の並びに合わせて入れ替えた。day123 は、Cloudflare のメールの伏せ字で「[email protected]」になっていた所を、元の `ssh ubuntu@10.42.0.1` に戻した。家の中のネットワークの番号で、外からは届かない。

### 9つのまとまりの報告（全量）

#### まとまり0

```text
r2_b_00 の30本を見終わりました。29本を直し、day39 は直す所がなかったのでそのままです。1本直すごとに画像にして確かめ、押して開く図は開いた状態の画像も見ました。p2mHeight の高さの script は30本とも残っています。git の操作はしていません。

**ファイルごと（主役の青／赤・緑・灰の使い方／色を入れなかった理由／直した壊れ）**

- **DesFKiCf-VFo5M_Nm**：青なし（3分野を並べる図で主役がない）。例外の区間を細かい赤の斜線、「!」と EXCEPTION の字を赤に。説明の文「斜線の区間」→「赤い斜線の区間」。外枠と左の太線をやめて灰の面に、＋ボタンと札を角丸4px。
- **avatar-gitai-pikari**：信号の四角を青。通信を切ったときの×を赤。操縦者の部屋と病棟の2つの枠を灰の面に。壊れ：床の CSS が書き間違いで模様が出ていなかったので、#ececec の床に直した。吹き出しがピカリくんの頭に重なって字が隠れていたので、幅を狭めて右に寄せた。
- **category-strategy**：陣取りの棒を青（測った値）。3層の箱を灰の面、札とフロー図の箱を白地＋#e6e6e6 の枠。壊れ：点線の箱が太さの指定漏れで3pxの太い点線になっていたので、1px の #888 の点線に直した。
- **digital-twin-place-surface**：爪先と、残した弧の点を青。面の点を灰 #9a9a9a。凡例の●も青に。壊れ：トグルのボタンが灰の上に灰で見えなかったので、白地＋枠に直した。
- **exposed-judgment**：青なし（三つの力を並べる図）。カード・札・区切りの線だけ直した。
- **four-layers-of-robot-softness**：トルクの棒を青、慣性の棒を赤（増えて困るもの）。白の中に灰の大箱が1枚だけになっていたのを外し、プリセットのボタンを白地＋枠に。
- **handover-step1-design**：青なし（手順の図）。8つのステップを4列×2段に並べ替えた。壊れ：RETREAT→HOME の字が隣の箱にはみ出していたのと、インターロックの点線が太すぎたのを直した。
- **japan-robot-leader**：日本の棒を青、他の国を灰 #9a9a9a。スキル不足を示す GAP の棒を赤。タブ・パイプライン・下の4つの欄を枠なしの面に。
- **mecharm-control-frequency**：Pi完結版の棒を青、Mac経由版を灰（JS の色指定と凡例も合わせた）。BUG の札を赤字に。壊れ：白いカードが無かったので body をカードにした。
- **mecharm270-joint-range**：実測の可動域の棒を青。軸方向だけ確かめた j4〜j6 の帯は、太い斜線を細かい灰の斜線に。凡例「黒バー」→「青バー」。
- **not-chasing-humanoids**：「置き換えたもの（FIXED INSTEAD）」の絵を青。タブの重なりと下の線の欠けを直した。
- **pi05-libero-benchmark**：使ったメモリを青、確保したが使っていない分を細かい青の斜線、シミュレータの描画を灰（縦じまをやめた）。時系列の線を青、使用率の棒を灰。凡例も同じ色に合わせた。
- **place-lookup-three-filters**：残った候補数の棒を青。「残差が跳ねる」の札を赤。
- **reachy-mini-motion-ux-timing**：「考え中」で埋めた状態を薄い青の面、何もしない「無反応」を薄い赤の面に（細い太線の斜線をやめた）。
- **robotics-ux-designer-en**：青なし（仕事の一覧）。枠を灰の面に、点線の区切りを #e6e6e6 の実線に。
- **slugwhere-the-bug**：最終の 06f の棒を青、06d と 06e を灰。壊れ：白いカードが無かったので body をカードにした。
- **the-wall-was-a-dropped-command**：OK を緑、MISS を赤（行も薄い赤）、取りこぼした数の棒を赤。
- **two-layers-of-motion**：滑らかに繋いだ線・整合させた軌道・手で教えた1本を青。継ぎ目のジャンプと平均した線を赤。増やした軌道を灰。判定の札を赤／緑。壊れ：「確定」「続きを整合」の字が線に重なっていたので位置をずらした。灰の上に灰になって見えなかった層の箱と説明欄も直した。
- **vlm-planning-layer**：青なし（③の黒が強調の役）。カード同士の -1.5px の重なりを 8px の間隔にした。
- **where-pixel-values-change**：色の見本は記事の中身なのでそのまま。見本の四角はデータとして角丸を外した。タブと区切りの線を直した。
- **day101**：完了の札と 12本の成功のマスを緑に。左の縦じまの帯をやめた。壊れ：幅が 780px だったのを 720px に揃えた。
- **day112**：まだ誰も定義していない「?」の欄と、その行を薄い青の面に。米国の列の模様を細かい斜線に、凡例も合わせた。壊れ：min-height 100vh と縦方向の中央寄せで上に大きな空きができていたのを外した。札が横いっぱいに伸びていたのも直した。
- **day125**：カメラ index の結果 OK を緑。表の黒い枠を行ごとの面に。
- **day134**：完了の✓を緑、表の AFTER の札を青。
- **day144**：旧設計の、Pi 側でもう一度変換していた箱を薄い赤＋赤い点線に（問題の場所）。
- **day152**：青なし（「掴む」の黒が強調の役）。枠と区切りの線だけ直した。
- **day160**：「嘘になる」「効かない」の一文を赤。タブの点々の模様をやめた。壊れ：「H行列計算」の字が矢印の線に重なっていたのを上にずらした。
- **day169**：19mm の誤差の表の誤差のマスを赤の面、確定した角度の表の強調のマスを青の面に。壊れ：min-height 100vh を外した。
- **day39**：直す所なし（すでに決まりどおり）。
- **day94**：NEW の札を青。外枠と区切りの線をやめて、タブ・欄・フローの箱をそれぞれ独立した形にした。

**迷ったもの・自信のないもの**

- **DesFKiCf**：「解けている区間」を黒のまま残しました（説明の文が「黒く埋まった」と言っているため）。開いた状態では、この区間が白で出ます（元からそうでした）。
- **reachy-mini**：決まりでは「比べる相手は細かい斜線」ですが、斜線の上に字が乗って読めなくなるので、斜線をやめて薄い面の色で分けました。
- **day112**：中国と米国はどちらも主役ではないので、無地と、#ececec の細かい斜線で分けました。斜線がかなり薄いので、凡例と合わせて見分けにくいかもしれません。
- **day101**：開いているフェーズの背景を、他のフェーズと同じ #f7f7f7 にしました。開いた行は詳細が出ることでしか分かりません。
- **handover-step1-design**：ステップの並びを1行から4列×2段に変えました（HTML の中身はそのまま）。
- **two-layers-of-motion**：左の図の「障害」の字に×が重なっているのは、元の描き方どおりに残しました。
- **day160**：MAT_CORNERS の黒い箱がマットの四角の右の辺に重なっているのは、元のままです。
- **pi05**：最初の画面は自動で切り替えが動くため、撮った瞬間の状態が一定しません。「確保したが使っていない分」の青い斜線の帯は凡例でしか見ておらず、横長の帯に出ているところは画像で確かめていません。

**ほかの作業者と共有しているもの**

作業用のフォルダ /tmp/claude-0/-home-claude/1459a415-214b-5016-bb5e-5c2b56ecdccb/scratchpad/ を、ほかの作業者と共有しているようです。そこにある rep.py は別の人のものだったので、自分の道具は /tmp/claude-0/-home-claude/1459a415-214b-5016-bb5e-5c2b56ecdccb/scratchpad/b00/ に分けて置きました。
```

#### まとまり1

```text
（このまとまりの係は英語で返事をしたので、まとめ役の Claude が日本語に直して載せる。中身は変えていない）

30本すべて仕上げた。高さを送る script（p2mHeight）は全ファイルに残っていて、git の操作はしていない。

## ファイルごと
- **ai-visual-inspection-lighting**：色は入れていない（手順の説明のため）。見出しと3つの段の余白が二重になっていたのを直した。番号の札を白地＋#e6e6e6 の枠・角丸4px に。画像の枠を #e6e6e6 に。
- **beyond-the-arc-taught-grasp**：届いた＝緑、j2 の限界の近く＝緑の細かい斜線、再生できない＝赤の点線の枠、弧の芯の帯＝灰の細かい斜線。壊れ：「空白帯」の字が目盛りの200に重なっていた。seq5 と seq4 の名前が重なっていた（seq4 を1段下へ）。
- **claude-instructions-hit-a-wall**：青＝今の断面とその点、灰＝前の版。つながった黒い枠のタブを、離した角丸4px のボタンに。見出しと図の欄を灰の面に。
- **does-a-delivery-robot-help-nurses**：赤＝倒れた柱と「倒れた」の進み具合の帯。立っている柱は #ececec。飾りのしまを消し、カードを角丸8px の灰の面にし、見出しと図の間を空けた。
- **figma-design-token-consolidation**：青＝後、青の細かい斜線＝前。色の見本は記事が扱うトークンの値なので残した。壊れ：棒が inline の span で幅が効かず、いつも空に見えていた。display:block を足した。
- **four-layers-of-robot-softness**：色なし（層の一覧）。カードの中に入れ子になっていた灰の箱を外した。壊れ：層のボタンにブラウザ既定の枠が出ていた。
- **hd-map-how-thin**：赤＝地図の手入れの費用（増えて困る）。緑＝ほかの3つ（代わりの手・ODD・走れる所）。スライダーのつまみを角丸4px に。
- **japan-robot-leader-physical-ai-gap-en**：青＝日本の棒、灰＝ほかの国、赤＝スキル不足の棒。タブ・パネル・流れ・仮説のカード・下のまとめを、つながった枠から離した面に。
- **mecharm-descend-phase-eliminated**：薄い青＝新しい設計のカード、赤の判定＝前の設計、緑の判定＝新しい設計、緑の点＝解けた、灰の白抜きの点＝解けていない／次の回。壊れ：年表の白抜きの点が中心からずれていた。
- **mecharm_data_blueprint**：青＝pose_id（元は赤）、赤＝変えない・未着手・未定、緑＝済み・解けた。カードがなかったので .sheet を白いカードにした。色の言葉を合わせた（「（赤）」→「（青）」など）。※このファイルは公開前にまとめ役が元に戻した（記事の図解ではなく単独のページのため）。
- **openpi-remote-inference-first-run**：青＝π0.5 の計算時間、赤＝ネットワークと受け渡し・コンパイルの止まり・待ち・1011 の切断、灰＝前処理・後処理。壊れ：タブ B に切り替えると、時間の線の再生位置が左端に張り付いていた。
- **pi05-mecharm-demo-to-inference**：青＝選んだ予測の線と使った一歩、灰＝ほかの予測、赤＝待ち時間。リードの文の「黒丸」を「青丸」に。
- **place-release-trial-log**：緑＝箱に入った、赤＝外れた。壊れ：短い棒（−7・+2・−4）の値が、明るい地に白字で消えていた。棒の外に黒字で置いた。
- **real-data-first-digital-twin**：青の白抜きの点＝実機で測った値、灰＝ツインの予測。
- **safety-controls-were-hiding-the-truth**：青＝アームの先、赤＝止まった読み取りの値。壊れ：「j1=…」の字がアームの線に重なっていた。
- **so101-imitation-learning-papers**：青＝成功率の点とばらつきの帯、方法ごとの印。
- **three-ai-content-pipeline**：色なし（流れの図）。決まりにない灰（#bbb・#ccc・#333 など）をそろえ、数字の欄を離した面に。
- **unity-fk-vs-real-machine-calibration**：緑＝OK の段、薄い赤＝FK CALIB、灰＝PICK（届いていない）、青の枠＝実機の高さ（見込み）、灰＝Unity の FK、赤＝約95mm の差。壊れ：マットの面の字と破線が列の名前に重なっていた。
- **vocabulary-of-modern-vision**：青＝すでに測った考え方につながる言葉（元は太い斜線。番号は薄い青で出る）。凡例と空のときの文の「斜線」を「青」に。
- **which-file-the-live-pick-reads**：青＝j0 を振った弧。作業の範囲を細い灰の点線の枠に。
- **day102**：色なし（手順）。カードがなかったので白いカードを足した。色の付いた丸の絵文字は CSS で灰色に見せた。
- **day116**：青＝ツカムくんの棒、青の細かい斜線＝ほかのアーム、緑＝480g、赤＝501g、灰＝500g の限界。壊れ：「LIMIT 500g」の字が折り返して線に重なっていた。
- **day126**：青＝R2R2R の棒、灰＝ほか。合成データは縦じまをやめて細かい斜線に。凡例の文も合わせた。
- **day135**：青＝ロボットの UI、灰＝ゲームの UI（元は太い斜線）。
- **day145**：赤＝遅れの影響の年表の棒。
- **day153**：色なし（落とし穴の一覧）。カードがなかったので .container を白いカードにした。
- **day161**：青＝「認知の早さ」とそこまでの線、灰の白抜きの点＝「技術の早さ」。壊れ：印が帯より上に浮いていた。
- **day17・day4**：決まりどおりだったので変えていない。
- **day95**：赤＝Target モードの実効の硬さ・「見せかけ」の判・範囲を超えた関節、緑＝Force モード、青＝範囲内の関節。凡例「ハッチング = 可動域超過」を「赤 = 可動域超過」に。

## 迷ったもの
- **figma-design-token-consolidation**：5つの色の見本（#1565C0 など）は記事のデータなので、そのまま残した。
- **day95**：範囲内の関節を青にするか緑にするかで迷い、青にした。
- **day135**：Game と Robot を1本の帯に積んでいて、最後の行は合計が100%を超える（92+55）。データはそのままにしたが、読み違えやすい図。
- **unity-fk-vs-real-machine-calibration**：高さの図で −55mm がマットの線より上に描かれる。線を動かしただけで、目盛りは変えていない。
- **mecharm_data_blueprint**：元の 1160px の幅のまま（ほかの図より広い）。
- **does-a-delivery-robot-help-nurses**：「柱が倒れる」を悪い（赤）と読んだ。
- **day102**：絵文字（🔵🟢🟣）は文字なので、CSS で灰にしただけで置き換えていない。
```

#### まとまり2

```text
渡された30本をすべて直しました。1本直すごとに画像にして見ており、押して出る図は押した後の画面も確かめています。git の操作はしていません。どのファイルにも高さを送る script が残っています。

**ファイルごと**
- ai-writes-code-not-motions-OIeOlSIq.html：青＝各工程まで成功している確率の箱と、下の2本の棒。赤・緑・灰は使っていない。壊れ：中の％の字が混色（mix-blend）で崩れていたので、白い札に変えた。凡例の文「箱の黒い部分」を「青い部分」に直した。
- box-collision-misdiagnosed-three-times-SVIWOrWA.html：青＝正解の j1 関節。赤＝「外れ」の札と、当たっている箱の線。緑＝「当たり」の札。
- code-skeleton-agent-judgment-lMyJu6FS.html：青＝最後まで残る「振る舞いの設計」の棒。灰＝誰でも手に入るようになる層の棒と細かい斜線（太い斜線から直した）。
- dont-trust-logs-trust-the-arm-zMxJJf6Q.html：青＝REAL の札と、実機側の比較の欄。緑＝✓、赤＝✗ と「PICK姿勢」の札、灰＝—。
- fill-the-hole-not-loosen-the-guard-ohcznTlz.html：青＝穴を埋めた新しい点。赤＝閾値を緩めたときのずれ（21.9mm）。
- from-cleanup-to-traceability-2kytkeCz.html：青＝つなぐ鍵の pose_id。赤・緑・灰は使っていない。
- hospital-operations-digital-twin-system-design-bwIWPhEz.html：青＝ハコブくんの棒。灰＝看護師の棒。緑＝減った量（−24 など）。棒の地にあった縦じまは消した。
- joint-guard-that-stopped-normal-motion-mxLeCm8A.html：青＝測った関節の差の棒。赤＝「送角せず」。緑＝「到達」。壊れ：閾値の「10°」の字が上で切れていたので、上に余白を足した。
- mecharm-descend-tip-shift-two-stage-tracking-2roJrLSN.html：色を入れていない（リンクの帯なので）。壊れ：body の地と余白が無かったので足した。黒い帯は角丸8px、VIEW は角丸4px。
- microfactory-fairino-fr3-4Miaw9zv.html：青＝人が直せる度合いの棒。黒い箱（人が中身を確かめられない部分）は黒のまま。
- parallax-and-pose-constrained-motion-eVq9Dkhb.html：青＝平行スライドの TIP。赤＝リーチ伸縮の TIP（今のやり方で高さが崩れる方）。壊れ：body の余白が無かったので足した。
- pick-success-place-stuck-YMMDfQ8Q.html：薄い赤＝未解決の箱。灰の面＝方針転換の箱。成功の箱は黒のまま。壊れ：点線・破線の箱が白い地に白で、見えなくなっていた。
- place-servo-never-fired-import-bug-vmwVgldk.html：青＝ROOT CAUSE の札（CONFIRMED の字と薄い青の面）。赤＝REJECTED。
- revamping-the-observe-view-pE0JP6py.html：青＝● 参照・適用。赤＝◇ 補正を破棄。灰＝未参照。
- sam3-mirror-reflection-grasp-problem-cYEPE4i8.html：青＝実体のマスク。赤＝映り込みに伸びたマスクと「no mass」。
- speak-to-relearn-robotics-O7uyYP36.html：青＝一致の棒。赤＝文字にならなかった所（赤地）と、カードにない文字（赤い点線）。凡例の文「黒地」を「赤地」に直した。
- three-ai-content-pipeline-en-2EQ2F1Ek.html：色を入れていない（手順の図なので）。壊れ：字が IBM Plex Sans のままだったので Inter と Noto Sans JP に直した。並んだ2つの箱が、開いていない方まで同じ高さに伸びていたので、伸びないようにした。
- visual-article-builder-UjyrSni8.html：色を入れていない（リンクの帯なので）。壊れ：上と同じく body の地と余白が無かった。
- wB1BUDZj-69l7FVdb.html：青＝成立度の線と点（ずれた例は青の点線）。赤＝見放しと肩代わりの帯（薄い赤の面）。外側の大きな灰の箱と、四隅の黒い四角は外した。
- who-is-responsible-robot-identity-design-liAQHeKc.html：赤＝「人＝読めない／？」の札。灰＝「未設計」の点線の枠。青は使っていない。
- day104-xTqyGQKj.html：色を入れていない（仕組みの図なので）。カード左にあった縦じまの線は消した。
- day117-xN_wto19.html：色を入れていない（キャラクターの考え方の図なので）。背景の網点、上の太い黒線、選んだ物の網点を消した。
- day127-88sj5oYF.html：色を入れていない（制作の手順の記録なので）。壊れ：外側の白いカードが無かったので、包む div を足した。
- day136-2UFJuGwY.html：色を入れていない（比べる表と手順なので）。
- day146-tScis2wW.html：緑＝✓ 正常の状態の字。赤＝⚠ IK不安定の字と「なぜIKが不安定なのか」の欄（点線の枠から薄い赤の面に変えた）。
- day154-5nNwpHD9.html：赤＝誤検出の枠と札（#cc3333 から直した）。緑＝score 0.85 の検出の枠と札。場面の色（#e8e8e0 など）は決まりの灰に直した。
- day162-V7v2rhYz.html：青＝グリッパー爪（実際に掴む点。凡例も青に直した）。赤＝固定補正の線と字。
- day170-6YuR9_Zk.html：赤＝✕ と「否定」の欄。緑＝✓。「確定」の黒い欄は黒のまま。
- day5-Ti5Y0a1o.html：変更なし（表で、すでに決まりどおりだった）。
- day96-0vYH4MvQ.html：青＝jointPosition の線（実際の関節位置）。赤＝乖離の線と面、トルクの警告、「✗ 発散」の札。緑＝「✓ 動く」の札。囲みの線はすべて灰の面に変えた。

**迷ったもの・自信のないもの**
- revamping-the-observe-view：◇「補正を破棄」は、決まりでは「取りやめ＝灰」にも読めます。ただ灰だと「未参照」の点と区別がつかず、不具合の根につながる所なので赤にしました。
- who-is-responsible：「未設計」の点線の枠を灰にしました。「デザイナーが埋める空白＝主役」と見るなら青もあり得ます。
- place-servo-never-fired：開いた黒いカードの上で、CONFIRMED の青い字がやや読みにくいです。
- pick-success-place-stuck：6「FAILED」の箱は、凡例の分け方（方針転換）に合わせて灰の面にしました。失敗として赤にする考え方もあります。
- day127：外側のカードを作るために、包む div を1つ足しました。中身の構造には触っていません。
- 少しだけ触らずに残したもの：
  - リンクの帯2本で、カーソルを乗せたときの #2a2a2a（他の担当者が仕上げた同じ形の帯も同じ値なので、そろえて残した）
  - day162 の canvas の薄い黒（rgba）の目盛りと字（opacity で薄くしているのと同じ扱い）
  - day136 の ✅ 絵文字（中身の文なので）
```

#### まとまり3

```text
30本すべて直しました。どれも render_one.py で画像にして確かめています。押して切り替わる図のうち、色が状態で変わるものは、押したあとの画面も見ました。最後の高さを送る script は30本とも直す前と同じで、git の操作もしていません。

**ファイルごと**
- **akita-data-center-robotics-perspective**：青＝雇用の折れ線・カーソル・％の大きな数字。基準の4施設の破線は黒のまま。凡例を「青い折れ線」に直した。外側の黒枠をやめ、施設タイプのボタンを4pxの札に、施設アイコンを灰の面に。
- **build-in-public-double-structure**：赤＝解くべき課題（hot）の点。太い斜線の警告テープは消した。カードは白・角丸8pxで枠なし、CTAボタンは4px。〔壊れ〕`@import` が `html{}` より後ろにあり、字が読み込まれていなかった。順を入れ替えた。
- **correction-blocked-by-table-gap**：青＝高い姿勢の点、灰＝低い姿勢の点、ABORT＝赤、OK＝緑。凡例もそろえた。タブを分け、札を4pxに。
- **drag-teach-v2-replay-and-position-binding**：青＝JSONの中の位置（Mac検出で埋める枠）と凡例。灰の上に灰が重なって見えなかったステップの箱と JSON を白の面に、札を4pxに。
- **find-tasks-where-nobody-comes**：青＝通る作業の印・残り数の点、灰＝外れた作業の印と斜線、赤＝注意点の三角と凡例。スイッチを4pxの札に、外側の黒い区切りをやめた。
- **from-drawing-to-solid**：青＝押し出した塊（細かい斜線）と、交差して残った立体。工程の4つを4pxのタブにし、外枠の黒線と進み具合の帯をやめた。
- **how-as-metadata-vs-subject**：赤＝✗の経路の印、緑＝○の経路の印。層の箱と経路の箱を灰の面に、区切りを #e6e6e6 に。
- **making-explainer-video-with-ai**：赤＝TROUBLE の見出し、緑＝FIX の見出し。タブ・札を4pxに、段階を示す帯は #ececec の地に黒。
- **mecharm-descend-tip-shift-two-stage-tracking**：青＝TIP の印、赤＝DESCEND のずれの矢印と「+61.4mm」。タブを分け、パネルの黒い見出し帯は文字だけにした。〔壊れ〕「MAT (Z PLANE)」と「BASE」の字が重なっていた。BASE を下へずらした。
- **microfactory-loop-model-fix-the-variance**：色なし（一覧の図のため）。項目を4pxの札に、詳しい欄を灰の面にした。
- **parallax-and-pose-constrained-motion**：色なし（バナー）。黒いバナーを角丸8px、VIEW を白い4pxのボタンに。〔壊れ〕白いカードが画面の外にはみ出していた（box-sizing がなかった）。字の読み込みと body の背景も無かったので足した。
- **pipeline-timeline-visualization**：赤＝古いままの JSON・「問題」の欄・質問の矢印。〔壊れ〕白いカードそのものが無かった。入れ物の div を足した。太い斜線をやめ、タブを分けた。
- **place-test-not-moving**：赤＝P2 ABORT の欄・✕の印、緑＝✓の印。黒い見出し帯は小さな灰の見出しに、各欄を灰の面にした。
- **rgb-hsv-threshold-lighting-failure**：赤ブロック・青ブロックは記事の中身なので色を残し、決まりの赤・青の値にそろえた。重なった区間は黒の細かい斜線、誤検出の棒は赤、それ以外は灰。〔壊れ〕`@import` の順を直した。
- **same-place-different-object**：青＝正しい対応の線・実際に正しかった数の棒・同一変換からの距離の輪、比べる段階は青の斜線、全件は灰。赤＝誤った対応の線、失敗のときの読み出し（元は黒）、NEAR IDENTITY の判定。
- **stage2-not-moving-isolation**：緑＝「動く」「問題なし」の札、赤＝「動かない」の札。箱の間を空け、番号と札を4pxに。
- **tilted-plane-to-flat**：赤＝4点目のずれ（帯・数字・図の中の印）。基準の許容の破線は黒。外枠の黒線をやめ、パネルを灰の面に、4つのボタンを4pxに。
- **visual-article-builder**：色なし（変遷の図）。段階のボタンを4px、パネルを灰の面、中の欄を白に。body に余白を足した。
- **wB1BUDZj**：「人を消す」を押したとき、効率の場所の棒を緑、主体性の場所の棒を赤にした（CSS の :has で行い、JS は触っていない）。外の灰の大きな箱と角のトンボ印は消した。〔壊れ〕`@import` の順を直した。
- **why-arm-stopped-grasping-root-cause-apick**：食い違いのときは降下の線・爪・MISS・結果の欄を赤、一致のときは緑（:has を使用）。数値の欄を灰の面に。
- **day106**：色なし（一覧の図）。〔壊れ〕body に高さを画面いっぱいにして上下中央に置く指定（min-height 100vh）があった。iframe の高さと食い違うので外した。幅が760pxで端に付いていたのを720pxに。
- **day118**：青＝R2R2R（自分の案）の数値の箱・利点・表の見出し・表の札。表の札は白い4pxに、パネルを灰の面に。〔壊れ〕表の外枠だけが全幅で、中身は左に寄っていた。広い画面では普通の表に戻した。
- **day128**：色なし（構造の図）。〔壊れ〕body の背景が透明で、min-width:620px のためスマホで横にはみ出していた。両方直した。
- **day137**：色なし（手順の図）。ステップの間を空け、破線の区切りを #e6e6e6 の線に。
- **day147**：青＝「修正後」の箱と、固有名詞ありの数字の箱。〔壊れ〕min-height 100vh を外した。タブを4pxに。
- **day154**：赤＝誤検出の枠と札、緑＝score 0.85 の枠と札。決まりの外のベージュ系の灰を #ececec・#f7f7f7・白に置き換えた。
- **day163**：青＝検出した四隅の点と、mm に直した値。暗いカメラ画面は写真に近いので黒のまま。座標の箱を白の面に。
- **day171**：緑＝DRAWN の札。PENDING は白い札。カードと数字の欄を灰の面にし、間を空けた。
- **day55**：色なし（表）。色の列が1字ずつ折り返していたので、最小の幅を付けた。
- **day97**：赤＝発振している目標角度の線、青＝5次多項式・CSV の軌道、灰＝追従している実際の角度。凡例も各パネルで合わせた。〔壊れ〕幅が760pxだったのを720pxに。

**迷ったもの・自信のないもの**
- 「黒く塗った箱は黒のまま」の決まりと合わないところがあります。same-place の失敗時の読み出しと判定の帯は、元が黒でも意味が「失敗」なので赤にしました。
- 自分の案や「後」の結果が黒塗りだった所も青に変えました（day118 の R2R2R の箱・札、day147 の「修正後」と固有名詞ありの数字）。黒のままにするか決めてください。
- stage2-not-moving と day171 の札は、「白＋#e6e6e6」の決まりではなく、緑・赤の薄い面にしました。結果の色を見せるためです。
- wB1BUDZj と why-arm-stopped は、押したあとの色を CSS の :has で切り替えています。古いブラウザでは色が付かず、黒のまま表示されます。
- 黒い見出し帯を残した図があります（same-place / tilted-plane / day118 のパネル見出し、day128 の表の見出し、day163 のカメラ画面）。
- 画面の中の線と字の重なり（day97 ③の注記、correction-blocked の「選ばれた低い点」）は、中身の位置に関わるので触っていません。
- tilted-plane の 4.97mm は許容 10mm の内側ですが、「ずれ」なので赤にしています。
- day154 では、机の余白（#f7f7f7）のほうがシーンの地（白）より少し暗く見えます。
- 作業のための置き換えスクリプトを、ほかの係と同じ名前で scratchpad に置いたところ、上書きされました。今は `/tmp/claude-0/-home-claude/1459a415-214b-5016-bb5e-5c2b56ecdccb/scratchpad/b03/` に分けています。私のファイルは、壊れたスクリプトで置き換えが始まる前に止まったので、影響はありません。
```

#### まとまり4

```text
30本すべて仕上げました。直したのは /home/claude/p2m-site/public/embeds/ の、一覧にあるファイルだけです。git の操作はしていません。1本直すごとに画像にして見ました。押して出る画面も、開いた状態で確かめています。30本とも、高さを送る script は1つずつ残っていて、div の開きと閉じの数も合っています。

**ファイルごと**
- approach-move-passes-bFY6JmuB.html：「原因1・2」の札を赤にし、「結果」の箱を薄い緑の面にして札を緑にしました。新しく足されていたホバーの灰を #ececec に直しました。直した壊れ：`</style>` が2つ重なっていた。
- build-in-public-double-structure-p87fRgQY.html：課題の一覧の図なので色は入れていません。札を角丸4px に、区切りの線を #e6e6e6 にし、ホバーの灰を #ececec に直しました。
- dashboard-reads-the-failure-xzdf8P6i.html：タブを1つずつのボタン（角丸4px）に分けました。札を白地＋#e6e6e6 の枠にし、「place: unknown」を赤にしました。結論の上の線は #e6e6e6 です。
- drag-teaching-records-and-replays-7mJ0BFPI.html：実測の帯を青、仮置きを灰の破線にしました。直した壊れ：仮置きの破線が実測の帯の下に隠れて見えなかったので、帯の上へずらしました。
- first-grasp-by-loosening-z-threshold-eh30dxk2.html：数字の欄と壁のタブの黒い枠をやめて、灰の箱に分けました。前後を比べる欄の黒い枠も外しました。結果の黒い箱の点滅を緑にしました。スマホ用の黒い枠の指定も消しました。
- grip-hold-position-vs-pressure-cpxRPuh6.html：押し込む矢印を青、ガタの隙間を赤にしました。切り替えはボタン2つ（角丸4px）に分けました。
- hue-holds-color-breaks-Ch0ZmhlA.html：色のある面の棒を青、色味のない面を白地＋灰 #9a9a9a の枠にしました。札は角丸4px です。直した壊れ：「155.025°」の数字がカードの外にはみ出していた。「丸め誤差 0.33°」の字が1行目の名前と重なっていた。
- measure-holding-needs-ledger-wYx_wl8t.html：空欄のセルを赤（太い斜線をやめて薄い赤の面）にしました。held を緑、not_reached と interrupted を赤、保留の grasped を灰の破線にしました。灰の上に灰が重なっていた欄は白にしました。
- mecharm-dynamic-tracking-stable-grasp-kpsedXlU.html：収束の線と終わりの点を青にしました。タブとパネルの黒い枠もやめました。直した壊れ：body の下地と字の読み込みが無かった。目盛りの「100/50」が軸に重なっていた。「STABLE+CONVERGED」が矢印に重なっていた。
- morphology-kernel-fits-or-vanishes-EntwV6XE.html：「変化した場所」を青の枠にし、凡例も合わせました。空のマスは白にして、灰の下地の上で見えるようにしました。
- persistent-loop-interrupt-auto-home-Jid7z0Ns.html：手順の図なので色は入れていません。周の箱を灰の面に、札を白地＋#e6e6e6 の枠（角丸4px）にしました。直した壊れ：開いた周（黒地）で札の字が消えていた。
- pipeline-timeline-visualization-oF8CDy59.html：失敗のセルを赤（斜線をやめて塗り）、DESCEND の上の線を赤、Next Design の上の線を青にしました。黒い枠をすべて灰の面か白地のボタンに直しました。直した壊れ：白いカードが無かったので、外側の入れ物を足しました。
- precision-is-not-learning-so101-AmCM22WX.html：選ぶ物は今までどおり黒です。2つの軸と手順の黒い枠をやめて、灰の箱に分けました。
- road-marking-job-post-and-physical-ai-oL1eVMI_.html：新しく塗った線を青、ズレの寸法とズレの棒を赤にしました。色を説明している言葉を「黒→青」に直しました（aria-label と本文）。区切りの黒い線をやめて灰の箱にし、札を角丸4px にしました。
- series4-2.html：緊5深5（最重要の課題）の点を赤にしました。太い斜線の警告テープは消しました。直した壊れ：body の下地が無く、外側が黒い枠だったので、白いカードにしました。
- stop-designing-to-stop-x9B9OztD.html：実測を青、ズレの線と j4 の Δ を赤にしました。白の中に灰の大箱が1枚だけになっていたのを解きました。
- tip-block-offset-measurement-BndnaH_q.html：青シールの輪を青、差分の線を赤にしました。黒い枠のパネルは灰の面にしました。直した壊れ：「爪先 43px」の字がブロックに重なっていた。
- visual-servo-debug-tip-detection-ey9Eph6s.html：「DAY184 再設計・追加」の札を青、「不変」を灰にしました。直した壊れ：白いカードと字の読み込みが無かった。行の作りを灰の箱に作り直しました。
- waypoint-replay-eight-takes-0pnbDVHu.html：✕ を赤、○ を緑、△ は黒のままです。take の箱どうしに間をあけました。
- why-robot-arms-stop-at-one-WTpHew8e.html：費用の内訳の図で主役が決められないので、黒・細かい斜線・灰で分けました。関門と枠を灰の面にし、出口は灰の破線にしました。直した壊れ：斜線の CSS が書き間違いで効いていなかった（括弧が余っていた）。
- day107-T68MYE21.html：フィジカルAIの欄を薄い青の面、数字の棒を青、障壁の棒を赤（斜線をやめた）にしました。グラフはロボットのコストを青、人件費を赤の破線にしました。直した壊れ：タブのボタンが太い灰の枠で出ていた。2つのカードに 3px の黒い破線が出ていた。
- day120-hGYn1iBW.html：OK の札を緑の枠、PENDING を灰にしました。パネルの黒い枠もやめました。
- day129-FtW_FPY9.html：結果の箱を薄い緑の面、字と棒を緑にしました。手順の黒い枠もやめました。
- day138-d4NjruKe.html：「動作確認済み」を緑にしました。手順のあいだをあけ、札を角丸4px、結果の箱の黒い枠を灰の面にしました。
- day147-YVKXxxyB.html：LOG126 の棒を青、ほかの記事を灰、IMPROVED の数字を緑、✕ を赤にしました。🔴🟠🟡 は ●（赤・#555・灰）に替えました（黄色を使わないため）。表と欄の黒い枠を灰の箱に直しました。
- day155-RWq3Iv2q.html：危険の欄を薄い赤、「UNSAFE」の札を赤、「SECURE」を緑にしました。フローの箱は白地＋#e6e6e6、暗号化済みの側は黒のままです。
- day164-VRiK3Wq2.html：系統誤差の棒を赤にしました。X軸＝赤・Z軸＝青の決まりは残し、色の値だけ #d85a30 と #2f6fdf にそろえました。直した壊れ：コードの改行が潰れていた（元のファイルから）。
- day172-_bvRMyT0.html：実測の線・点・進み具合の棒を青、目標は黒の破線にしました。直した壊れ：body の下地と字の読み込みが無かった。「TARGET」の字が外にはみ出していた。「105°」の字が図の端で切れていた。
- day6-pBNtWWqI.html：表だけの図で、もう決まりの見た目になっていたので、何も変えていません。
- day99-6_NsfiFO.html：今のステートを薄い青の面と青い点、進み具合を青、済んだステートを緑の点（斜線をやめた）にしました。直した壊れ：白いカードが無かったので、外側の入れ物を足しました。

**迷ったもの・自信のないもの**
- 作りを少し変えた所：外側の入れ物を足した3本（pipeline-timeline-visualization-oF8CDy59・day99-6_NsfiFO・visual-servo-debug-tip-detection-ey9Eph6s）と、JS のテンプレートに class を1つ足した waypoint-replay-eight-takes-0pnbDVHu。動きは変えていません。
- 図の中身に関わる見た目の変更：drag-teaching-records-and-replays-7mJ0BFPI の仮置きの帯を上へずらしたこと、series4-2 の警告テープを消したこと。
- 色の意味の取り方：hue-holds-color-breaks-Ch0ZmhlA は「赤い面」の棒も青にしました（測った値という意味）。tip-block-offset-measurement-BndnaH_q は青シールを青にしたので、青が「シールの色」と「主役」を兼ねています。
- 主役を決めて色を入れなかった図：why-robot-arms-stop-at-one-WTpHew8e は黒・斜線・灰で分けました。
- 字の読み込みを足した図：day172-_bvRMyT0、mecharm-dynamic-tracking-stable-grasp-kpsedXlU、visual-servo-debug-tip-detection-ey9Eph6s の3本に、Google Fonts の link を足しました。
- そのままにした色：day138-d4NjruKe のコードの中にある #e8ff00（黄色）は記事の中身なので残しました。
- 横に長い表：pipeline-timeline-visualization-oF8CDy59 の表は最小幅 780px のままです。狭い画面では表の中だけ横にスクロールします。
```

#### まとまり5

```text
29本すべて見直しました。うち27本を直し、day3・day9 の2本は直す所がなかったので手を入れていません。直した27本は直すたびに render_one.py で画像にして見ました。押して出る画面も、開いた状態の写しを作業用フォルダで画像にして確かめています。どのファイルも高さを送る script は1本ずつ残っていて、git の操作はしていません。

**ファイルごと**
- arcs-without-area：主役は FK の点で、青にしました。赤・緑・灰は使っていません。外の枠と canvas の黒い枠を外し、目盛りは #e6e6e6 にしています。段の札（ALL/PICK…）は角丸4pxで、白地＋枠、選んでいる物は黒です。
- build-robot-arm-telemetry-dashboard：主役は DASHBOARD の欄で、薄い青の面にして見出しも青にしました。赤は STALE の札に使っています。2つの欄は黒い枠をやめて灰の面にし、#bbb・#ccc は決まりの灰に置き換えました。
- data-map-before-real-run：手順の図なので色は入れていません。live・real・twin の箱は灰の面、ARM は黒のまま角丸にしました。切り替えボタンは間を空けた2つのタブにし、見出しの下の線は #e6e6e6 です。
- dual-camera-detection-design：バナーなので色は入れていません。黒いバナーを角丸8pxにし、VIEW の札は角丸4pxです。
  - 直した壊れ：幅が box-sizing の指定なしで右へはみ出していたので、指定を足しました。
  - body に下地 #f7f7f7 を入れ、字の読み込みの link も足しました。
- first-video-end-to-end：主役は BGM の線で、青にしました（線の端は butt）。dB の補助線は灰の点線です。4つのボタンは間を空けたタブにし、区切りの線は #e6e6e6 にしました。
- handover-control-code-complete：主役は新方針の結末で、緑にしています（HOME 着、帰還）。赤は RELEASE 失敗の段と、流れの札の RELEASE ✕ に使いました。旧方針の「その場で固まる」結末は、薄い赤の面に赤字です。
  - 黒い枠と左の黒い帯はすべてやめ、灰の面にしました。札とタブは角丸4pxです。
  - 直した壊れ：人の確認の段の両端に、太い黒の破線が出ていました。dashed の書き方が原因だったので直しています。
- identify-camera-by-name-not-index：主役は「固有名」で、見出しの帯、新しく見つかった1台の行、「当てにする」の欄を青にしました。緑は [OK] の印、灰は取り消し線の index = 0 です。
  - 決まり以外の灰（#3a3a36・#75746e・#b9b8b1・#ecebe4）は、#555・#888 などに置き換えました。
  - ステップの帯はタブにし、画面の黒い枠と下の欄の黒い仕切りはやめて、灰の面と薄い青の面にしています。
- measure-the-hole-before-filling-it：主役は点の集まりで、青にしました。赤は穴の輪と「穴 195/195」の字、凡例です。中の大きな灰の箱は外し、図の欄は黒い枠をやめて灰の面にしました。目盛りは #e6e6e6 です。
- mecharm-not-done-yet：主役は PHASE 2 の NEW 札で、青にしました。空の欄と開いて出る欄は、黒い枠や黒い破線をやめて灰の面にしています。
- motion-uncanny-valley：主役は谷の線で、青にしました。赤は谷の底の点 B です。図の欄は灰の面、A/B/C のボタンはタブにし、区切りの線を #e6e6e6 にしました。
- physical-ai-beyond-industrial-robots：主役は人1人が受け持つロボットで、絵を青にしました。太い区切り線はすべてやめています。4つの欄は灰の面、段のボタンはタブ、手間の棒の地は #ececec です。
- pivot-to-probe-control-and-external-review：主役は外部のレビュー役（codex / gemini）と AFTER の説明欄で、薄い青の面にしました。赤は BEFORE の見落とし欄と、バグ番号の札です。枠はすべて灰の面に変えています。
  - 直した壊れ：「→ → →」が左に寄っていました。指定の書き方が当たっていなかったので直しています。
- precision-lives-in-the-input-not-the-model：主役は訂正した箇所で、青にしました。凡例の「反転 =」も「青 =」に変えています。推定の欄は灰の細い破線です。中の大きな灰の箱は外し、ボタンはタブにしました。
- robot-car-kit-autonomy-breakdown：緑は安全層（止める距離の線と、働いたときの欄）、赤は障害物、灰は AI が止まる位置の線と、出番のない層です。この図は良い・悪いで色を分け、青は使っていません。
  - 直した壊れ：地面の線の CSS が書き間違いで効いていませんでした。障害物の横じまも消しています。
  - 直した壊れ：入力欄の囲みに、ブラウザが付ける灰の枠が出ていたので消しました。
  - 直した壊れ：2本の線の名前が重なっていたので、上下にずらしました。
- setting-up-the-side-camera：主役は、その軸を受け持つカメラです。図の中の箱と下の欄を青にしました。
  - 直した壊れ：外側に白いカードがなく、body の指定もなかったので、カードと body の指定を足しました。字の読み込みの link も足しています。
  - 黒い枠と破線の枠はやめ、灰の面にしました。
- symmetric-reach-mirror：主役は点の集まりで、青にしました。目盛りは #e6e6e6 です。3つの数字の欄は黒い仕切りをやめて白い面にし、#333 は #555 に替えました。下の欄は上の欄とくっついていたので、間を空けています。
- top-banner：課題の多さを表す丸のうち、塗りつぶしの丸は赤、半分の濃さの丸は灰 #9a9a9a にしました。上の太い斜線のテープは外しています。黒い枠をやめて白いカード・角丸8pxにし、body の下地を足しました。
- visual-servo-design：主役は補正後の棒で、青の塗りにしました。補正前は青の細かい斜線、計測できなかった試行は灰の細い斜線です。棒の地の黒い枠はやめました。LOOP の囲みは灰の細い破線・角丸です。
- what-circularity-actually-measures：主役は minAreaRect で、線・名前・棒を青にしました。赤は膨らむ boundingRect（破線と斜線、名前、棒）です。数字の欄は黒い枠をやめて灰の面にし、形のボタンは角丸4pxにしました。
- why-z-correction-failed-twin-poses：赤は Z の残差の線と名前で、青は使っていません。2つのボタンはタブ、図の欄は灰の面、札は角丸4pxにしました。
- day108：緑は 12/12 SUCCESS の札（図の中も含む）です。2本の腕は同じ立場なので、青は使っていません。
  - 直した壊れ：外側に白いカードがなかったので、全体を包むカードを足しました。
  - 下線式のタブは角丸4pxのタブに変えました。カード上の太い帯と、中の黒い枠（3か所）はやめています。
- day121：流れの図なので色は入れていません。札は白地＋枠・角丸4pxにし、点線の欄は白い面に変えました。
- day130：緑は 4a SUCCESS の欄、赤は 4b FAIL の欄です。データ項目のカードは黒い枠をやめて灰の面にしました。流れの欄はくっついていたので、間を空けています。
- day139：主役は集めた点で、青の丸にしました。基準の四角は黒のまま、補助の枠は灰の細い点線です。段の欄はくっついていたので間を空け、結果の欄は白い面にしました。
- day148：赤はリスクの札とBEFOREの点、緑は AFTER の点です。決まり以外の灰（#e8e8e2・#ddd・#ccc・#333）を置き換えました。表の行の区切りは #e6e6e6、BEFORE/AFTER の欄は角丸の面で間を空けています。
- day156：主役は「Human only」の欄で、薄い青の面にしました。赤は AI が外した3回、緑は人が拾った3回です。2つの欄の黒い枠はやめ、データの黒い箱は角丸にしました。
- day165：主役は採用した方式 03 で、薄い青の面に、数字を青、SUCCESS の行を緑にしました。灰は取りやめた 01・02 の数字です。#ebebde は決まりの灰に置き換え、札は角丸4pxにしました。
- day3・day9：文章の表なので色は入れていません。すでに決まりの見た目になっていたので、変えていません。

**迷ったもの・自信のないもの**
- precision-lives-in-the-input：凡例が「実線 = 映像で確認」なので、ふつうの欄に #e6e6e6 の細い実線を残しました。薄くて実線と読めないかもしれません。
- visual-servo-design：補正前（青の斜線）と計測不能（灰の細い斜線）が、どちらも斜線で少し似ています。
- robot-car-kit：この図の主役を「安全層」と見て緑にし、青は使っていません。
- top-banner：塗りつぶしの丸を赤（急ぎ・深い課題）と読みましたが、元の意味の取り方に自信はありません。サイト上部のバナーで、幅は 1216px のままにしています。
- handover：最初の段（RELEASE 失敗）を、決まりの「黒のまま」ではなく赤にしました。押すと黒と赤が入れ替わる作りはそのままです。
- what-circularity：minAreaRect の青い線は、塗った図形の下に描かれていてほとんど見えません。描く順番は元のままにしています。
- day108・day121 は、元から左右2つの対等な比べ方、または流れの図なので、主役の青を入れていません。
```

#### まとまり6

```text
担当の29本は、すべて決まりの見た目にそろえました。1本直すごとに画像にして目で確かめ、押して出る画面もクリックした状態で確かめています。高さを送る script は29本とも1つずつ残っています。決まりにない色は、top-video のサムネイルの裏地 #141414 だけです（画像の下に敷く色なので残しました）。git の操作はしていません。

**ファイルごと**
- **arm-posture-vs-payload**：青＝測ったブロック位置の点と弧（選んでいる点は濃い青）。マットの外枠は細い灰の点線にし、読み取りの欄は灰の面3つに分けた。
- **building-a-physical-ai-lab-at-home**：青＝中心の CONFIG ノード（薄い青の面）。赤・緑は使わず、ノードは灰の面・角丸にした。直した壊れ：矢印の線に重なっていた札（MEASURE・DEPLOY・EXPORT USD）の字が読めなかったので、白いふちを付けた。
- **decisions-api-grasp-check**：緑＝掴めた写真の丸、赤＝失敗した写真の丸。説明文の「黒丸／白丸」は「緑の丸／赤の丸」に直した。タブは1つずつ分かれたボタンにした。直した壊れ：％の数字が丸に重なっていた。
- **dual-camera-detection-design**：青と緑は実物のシールの色（青シール・緑シール）で、記事の中身として残した。除外の札は灰にした。直した壊れ：html・body の枠がなく、ページの周りが白かったので背景を入れた。字を読み込む行がなかったので足した。「TIP = green中点」の字が爪の線に重なっていたので、ふちを付けた。
- **fk-place-one-fix-reveals-next**：赤＝ずれた爪、問題の関節、角度、stalled。比べる相手（ghost）は灰。外の大きな灰の箱は外した。
- **handover-lost-the-incremental-loop**：黒い段（実機に触れる段）は強調として黒のまま。それ以外は灰で、主役を決める図ではないので色は入れなかった。直した壊れ：黒い段の「real machine」の札が白地に白字で消えていた。
- **image-recognition-object-detection-overview**：青＝モデルの出力（ラベル・枠・マスク・キーポイント）。机の上の絵は黒のまま。タブの重なりを直し、ばらばらに分けた。
- **measure-what-the-gripper-missed**：主役を決める図ではないので、黒と灰のまま（結論の黒い箱は黒のまま）。直した壊れ：つなぐ線が破線のように途切れて描かれていた（線を描くアニメの長さの計算が、図の伸び縮みに合っていなかった。画面上の長さで計算するように JS を直した）。
- **mecharm-onto-ros2-moveit**：対応表なので主役は決めず、黒と灰のまま。行は灰の面・角丸にした。
- **motion-ux-three-phase-model-en**：日本語版の見本と同じにした（青＝Motion UX 側の比べる欄、札は白地に細い枠）。
- **physical-tracking-limit-vs-software-speed**：青＝測った完了時間の棒、灰＝外れ値の棒、赤の薄い面＝見つかった欠陥1〜3。直した壊れ：白いカードがなかったので、外側にカードを足した。
- **pixels-as-perception-data**：青＝YOLO の検出枠、赤＝HSV で間違って拾った所の点線の丸。図の中の枠は灰の面・角丸にした。直した壊れ：字を読み込む行がなかったので足した。
- **precision-lives-in-the-input-not-the-model**：青＝爪先の揺れの線と点、灰＝手の揺れ（比べる相手）。外の大きな灰の箱は外した。
- **robot-handoff-design**：手順の図なので黒と灰のまま。直した壊れ：灰の帯の中のノードが灰の面で見えなかったので、白にした。
- **setting-up-the-side-camera**：バナーなので色は入れず、黒い帯を角丸にした。直した壊れ：body の背景がなく、外側の箱が右へはみ出していた。字を読み込む行もなかったので足した。
- **telemetry-dashboard-sidebar-goes-live**：UI の説明なので色は入れなかった。オフのトグルは白地に細い枠にした。直した壊れ：上下の余白が二重になっていた。
- **top-video**：トップページ用の部品で、背景はサイト側の枠に任せる作りなのでカードは足さなかった。決まりにない灰（#EDF0F0・#D6DADA）を決まりの灰に置き換え、札を角丸4px にした。
- **visual-servo-y-axis-design**：青＝測った dx・dz・dy の棒。j4（選んだ関節）の黒い箱は黒のまま。タブは分け、外枠を外した。
- **what-to-hand-to-robots**：青＝機械へ渡せる分、青の細かい斜線＝人に残る分（太い縦じまをやめた）。行の印も同じ色にそろえた。
- **youtube-channel-identity**：青＝見える範囲の枠、灰の細かい斜線＝隠れる部分（太い斜線をやめた）。
- **day11 / day3**：すでに決まりどおりだったので変えていない。
- **day122**：緑＝最後の「完了」の欄。黒が4つ並んでいた流れの欄は灰にした。
- **day131**：緑＝AFTER の ✓、灰＝取り消し線の BEFORE。直した壊れ：つなぐ線が幅0で見えていなかった。灰の帯の中のノードが見えなかった（白にした）。BEFORE の字がほぼ見えなかった。黒地のカードに #bbb が使われていた。
- **day140**：青＝自分の案の VLM-as-Reward（評価ループの欄、札、ループの1段）。説明文の「点線枠」は「青の点線枠」に直した。直した壊れ：その欄を選ぶと、薄い面に白字で読めなくなっていた。
- **day149**：赤＝3つのアプローチの FAIL の字。座標の表は灰の面に、黒い式の箱は黒のまま。
- **day157**：赤＝BEFORE の 69.23mm、緑＝AFTER の 0.11mm（黒い箱は黒のまま）。直した壊れ：字を読み込む行がなかったので足した。
- **day166**：問いを並べた手順の図なので、黒と灰のまま。直した壊れ：字を読み込む行がなかったので足した。
- **day90**：青＝処理中の関節と腕の区間（凡例も青に）、緑＝収束した先端と「収束」の欄。直した壊れ：白いカードがなかったので足した。説明の欄の左に付いた太線が、角丸に沿って曲がって崩れていた。

**迷ったもの・自信のないもの**
- **day166・pixels-as-perception-data**：何も選んでいないときの欄や見出しの帯が、大きな黒い箱のまま残っています。「黒い箱は黒のまま」に従いましたが、選ぶ前から真っ黒なのは重く感じます。
- **day157・day90**：結果の黒い箱の中の数字だけを緑や赤にしました。黒地の上の緑なので、はっきり見えるかは自信がありません。
- **top-video**：カードを足さず、色の置き換えだけにしています。これで合っているか見てください。
- **dual-camera-detection-design**：青と緑をシールの色として使いました。決まりの「関節や軸の色の約束」と同じ扱いにしてよいかは判断が分かれます。
- **decisions-api-grasp-check**：ok と ng を緑と赤にしましたが、「主役＝青」として扱う考え方もあります。
- **day90**：収束したときに「EE ✓」「dist ≤ 0.05m」「Target」の字が互いに近く、ふちを付けても少し窮屈です。字の位置は動かしていません。
- **fk-place-one-fix-reveals-next・image-recognition-object-detection-overview**：腕や爪の線の端の丸み（round）は、グラフではなく絵なのでそのままにしました。
```

#### まとまり7

```text
29本の作業が終わりました。git の操作はしていません。直したあと、1本ずつ `render_one.py` で画像にして確かめました。動く図は、押したあとの画面も写して確かめています。day10 と day3 はもう決まりどおりだったので、手を入れていません。

## ファイルごと

- **autodesk-fusion-mcp-setup**：主役がない図なので青は入れていません（つながりの順番を示す図のため）。切れた所の「×」印を赤にしました。黒い枠を、灰の面・白い札・角丸に替えています。
- **calibration-arm-not-moving**：青なし（比べる2列のどちらも主役ではないため）。「動く」の札を緑、「止まる」の札と占有している行を赤にしました。黒い枠の比べる2列は、隙間を空けた灰の面にしました。
  - 壊れ：外枠の余白が 40px で背景が透明だったので、背景 #f7f7f7・余白 8px 0 に直しました。中の欄の余白が二重になっていたのも直しました。
- **descend-passed-but-placement-drifts**：落ちた位置のブロックを青にしました。ずれの線は赤、結果の帯は「入った」を緑、「入らなかった」を赤、マットの枠は細い灰の点線です。
  - 壊れ：「ARM BASE」の字が丸に重なっていたので、横へずらしました。
- **dynamic-tracking-z-axis-sign-inversion**：比べる欄のうち「AFTER」と「LOG#7」を薄い青の面にしました。赤・緑はなし。
  - 壊れ：このファイルは html / body のない断片で、字の読み込みもありませんでした。body の背景・余白と、Google Fonts のリンクを足しました。
- **fk-vs-real-machine-camera-loop**：「NEXT 採用予定」の札を青にしました。「ABANDONED」と「PARTIAL」の札は灰の字です。選んでいる物と選んでいない物の黒・白が逆になっていたので、選んでいる物が黒になるよう直しました。
- **handover-present-pose-from-probe-scan**：青なし（テストの照合の仕方を示す図のため）。見つかった違いの番号は、1・2 を赤、3（記録だけ）を灰にしました。空席は灰の細かい斜線です。
  - 壊れ：空席の斜線が欄の高さいっぱいに広がっていなかったので直しました。
- **j0-position-table-place-approach**：測った弧の点を青にしました（JS の中の色の値だけを変えています）。基準の線は黒のままです。
- **measuring-the-stuck-joint**：届いた点を青、届かなかった点を赤の細かい斜線、壁の線を赤にしました。指令は白い丸のままです。
  - 壊れ：軸の下の向きの字が、図と左右逆でした（左が -40°なのに「← 深い（-100°）」と書いてあった）。「← 浅い（-40°）／深い（-100°）→」に入れ替えました。前の版（before2）から既に逆でした。
- **mecharm-reach-control-ssh-overhead-analysis**：積み上げた効果の棒を青にし、比べる相手の「現状」は青の斜線にしました。内訳では time.sleep の待ちを赤、ほかの項目を灰にしています。
  - 壊れ：白いカードがありませんでした。また、細い棒に乗っている白い字が、うしろの灰の地に消えていました。どちらも直しました。
- **no-such-thing-as-free-ai**：青なし（使い方の段階を並べた図のため）。SAFE を緑、CAUTION と DANGER を赤、危ない例の箱を薄い赤にしました。
  - 壊れ：断片で、白いカード・body の決まり・字の読み込みがありませんでした。足しました。
- **pi-brain-not-muscle-local-first**：青なし（層の構造を示す図のため）。白の中に灰の大箱が1枚だけある形をやめ、層を直接置きました。凡例の意味（点線・灰・黒）に合わせて見本の色も替えています。
- **place-descend-and-camera-stability**：青なし。「次の課題」の札を赤、「その日に解決」の札を緑にしました。
- **predicted-vs-actual-correction-loop**：青なし（2つの考え方を説明する図のため）。まだ空の ACTUAL 列を灰の細かい斜線にしました。タブ・数字の欄・ループの節を白や角丸に替えています。
- **robot-ideas-from-pain-not-robots**：「物理介入が必要」を青い点、「混合」を青の斜線、「情報系」を灰の白抜きにしました。凡例の見本もそろえています。
  - 壊れ：白いカードがなかったので足しました。
- **shoot-one-frame-before-building-detection**：手を青、壁を青の斜線、物体・機材を灰にしました。
  - 壊れ：斜線の CSS に余分な括弧があり、壁の斜線と凡例が表示されていませんでした。直しました。
- **telemetry-idle-stage-history-place-verdict**：青なし（画面の状態を示す図のため）。灰の面・角丸の札にし、点線の区切りは #e6e6e6 の実線にしました。
- **toward-dual-arm-coffee-en**：青なし（段階を示す図のため）。タブは白地＋#e6e6e6 の枠、パネルは灰の面、流れの箱は白い札にしました。
- **visualizing-correction-memory**：改善中を緑、不安定を赤、横ばいを灰にしました（状態の色・凡例とも JS の中の値です）。
- **when-robot-motion-is-learned**：NN が担う範囲の棒と凡例を青にしました。
  - 壊れ：body に 8px の余白がなかったので足しました。
- **day10**：変更なし（決まりどおり、一覧の表のため）。
- **day3**：変更なし（決まりどおり、一覧の表のため）。
- **day110**：表の最終行「DAYログ」の名前と「積み上がる」を青にしました。
  - 壊れ：白いカードがなく、幅が 820px でした。720px のカードにしています。黒い表の見出し・黒い枠の段の選びと流れの箱は、見出しの下だけ黒い線にし、ほかは灰の面にしました。
- **day123**：補正の棒と「-30° 水平確定」の札を青、各関節の「✓」を緑にしました。選んでいる物と選んでいない物の黒・白が逆になっていた流れの段も直しました。
  - 壊れ：元のファイルから、メインの script が途中（`fill.style.left =`）で切れていて閉じていませんでした。そのため押す動きがすべて止まり、高さを送る script も一緒に止まっていました。左右へ棒を伸ばす処理を補い、script を閉じました。押して動くことも画像で確かめています。
  - 壊れ：Cloudflare のメールの伏せ字で「[email protected]」になっていた所がありました。元のデータから `ssh ubuntu@10.42.0.1` に戻し、cdn-cgi の script を消しました。
- **day132**：実測を青、指令を青の斜線にしました。実測の棒を細くして、うしろの指令の斜線が見えるようにしています。
- **day142**：修正方針の欄を薄い青にしました。今の設計（矛盾あり）の欄は薄い赤、failure を赤、success を緑です。
  - 壊れ：段の並びに隙間がなく、最後だけ黒い枠が残っていたので直しました。判定の欄と次の見出しがくっついていたのも直しました。
- **day150**：取れた点を青、正解の段の「✓」と差分の札を緑、失敗した案の「✗」を赤にしました。補助の枠は細い灰の点線です。
- **day158**：確定した命名（チップ・大きな札・拡張例）を青にしました。
  - 壊れ：白いカードがなかったので足しました（html を灰、body を白いカードにしました）。
  - 壊れ：`.final` という名前が最終ルールの欄と札の両方に当たっていて、札が大きく崩れていました。欄の側を `div.final` に絞って直しました。
- **day167**：青なし（3つの層の用語の図のため）。黒い枠と点線の例の箱を、角丸の面に替えました。
- **day92**：つかんだカップを青にしました（canvas の色の値だけを変えています）。決まりにない灰（#c8c8c3・#e0e0db・#ccc）は、#9a9a9a・#e6e6e6 に替えました。
  - 壊れ：白いカードがなかったので足しました（html を灰、body を白いカードにしました）。

## 迷ったもの・自信のないもの

- **day123**：補った script のうち、0°より大きい側の棒の描き方（左端 50%・幅 pct%）は、元のコードが途中で切れていたため推測です。
- **day123**：`ssh ubuntu@10.42.0.1` を戻したことで、IP アドレスが記事に出ます。元の記事の意図どおりかは、確かめてもらう必要があります。
- **day158**・**day92**：body に `margin:8px auto` を付けて白いカードにしました。iframe の高さの計算で、下の 8px が欠けないかは、実際のサイトでの確認をおすすめします。
- **measuring-the-stuck-joint**：向きの字を入れ替えたのは中身の字の変更です。図（-40 が左）に合わせましたが、記事の本文と食い違わないか確かめてください。
- **day150**：j0 の軸の字が、補助の枠の下の点とわずかに重なっています。元からなので、そのままにしました。
- **no-such-thing-as-free-ai**：層3の「CAUTION」は、元の作りで DANGER と同じ赤になっています。注意の段階なので、別の扱いにすべきか迷いました。
- **dynamic-tracking-z-axis-sign-inversion**：表で強調している行（連動 D）は黒のまま残しました。青にしなかったのは、1つの図で青に2つの意味を持たせないためです。
- **作業中の気づき**：まとめ役の scratchpad を他の作業者と共有していたようで、`rep.py` が書き換えられていました。自分の道具は `/tmp/claude-0/-home-claude/1459a415-214b-5016-bb5e-5c2b56ecdccb/scratchpad/b07/` に分けて置いています。
```

#### まとまり8

```text
担当の29本を直しました（day38 は手を入れていません）。全ファイルを画像にして見て、押して出る画面もタブ・パネルを開いた状態で確かめています。高さを送る script は29本とも残っています。git は触っていません。

## ファイルごとの結果

- **avatar-gitai**：選んだ候補の五角形を青（薄い青の面）にしました。比べる相手の馬・車の点線は灰、「満たした／見込み」は緑、「弱い」は赤の×にし、凡例も同じ色にそろえました。太い斜線の模様をやめ、黒い枠線は #e6e6e6 にしました。
- **category-strategy-en**：検索の量の棒を青にしました。重なった黒枠の3層は灰の面に、札は白地＋枠・角丸4px にし、入口のノードは細い灰の点線にしました。
- **design-implementation-split**：進行中を青、検証済みを緑、未着手を灰の帯にしました（縦じま・斜線はやめました）。黒枠を積み重ねていたフェーズは灰の面にしています。
- **explainer-video-character-narration**：口・目・リングの線を青、元になる声の線を灰にしました。つながったタブは、離した角丸4pxのボタンにしています。
- **flying-car-naming**：把持の線を青、比べる相手の飛行を灰にし、凡例も合わせました。余地の棒は青です。セルの黒枠と、グラフの外の黒枠は外しました。
- **handover-series-concept**：判定の札と線（JUDGE）を青にしました。外の黒枠と、つながったタブ・柱をすべて面とボタンに直しています。
- **j1-command-rejection-found**：測った点を青にし、捨てられた指令は赤、到達OKは緑にしました。外の枠は細い灰の点線です。二重になっていた灰の大箱はやめました。
- **mecharm-continuous-recording-pipeline**：記録されたコマ・パケットを青、停止の札を赤、本線が稼働中の印を緑にしました。黒枠の箱・ボタン列は面と角丸4pxにしています。
- **mecharm-static-to-dynamic**：係数の棒を青、CONVERGED を緑、ABORT・SKIP を赤にしました。4つのタブそれぞれの黒枠をすべて面に直しています。
- **no-such-thing-as-free-ai-en**：SAFE を緑、CAUTION・DANGER を赤にしました。
  - **直した壊れ**：断片の HTML で白いカードも字の読み込みもなかったので、カードとフォントの読み込みを足しました。
- **pi-brain-not-muscle**：主役が決められないので色は入れていません。
  - **直した壊れ**：白いカードがなかったので足しました。押して反転する動きはそのまま残っています。
- **place-design-and-first-move**：OK を緑にしました。黒枠の表は面に、札は白地＋枠にしています。
- **push-hold-stops-the-drop**：押し込みの矢印を青、隙間（ガタ）の印を赤にしました。外の黒枠と、つながったトグルを直しています。
- **robotics-ux-designer-as-a-profession**：一覧なので色は入れていません。黒枠を面に直しました。
- **side-camera-watches-descend**：経緯の図なので色は入れていません。
  - **直した壊れ**：白いカードがなかったので足しました。黒枠の柱・パネル・表は面にしています。
- **template-match-center-was-the-corner**：正しい中心を出す輪郭を青、ずれるテンプレートの枠を赤にしました。灰の大箱は1枚だけだったのでやめています。
  - **直した壊れ**：SVG の文字が大きすぎて枠と重なっていました。字を小さくして縁取りを付け、ラベルを十字から少し離しました（JS の位置の数字を +9 から +14 に変えています）。
- **toward-dual-arm-coffee**：段階の図なので色は入れていません。黒枠のパネルを面にしました。
- **vlm-planning-layer**：失敗の札を赤、成功の札を緑、未着手の計画層を灰の点線にしました。凡例の言葉を「灰の面／灰の破線」に直しています。
- **where-learning-data-lives**：色は入れていません。マウスを乗せたときの色を #ececec にしただけです。
- **day100**：色は入れていません。
  - **直した壊れ**：カードがなかったので、body を白いカードにしました。つながったタブ・表・フェーズの黒枠も直しています。
- **day111**：色は入れていません。
  - **直した壊れ**：画面の高さいっぱい（100vh）で中央に寄せる指定があり、iframe の高さが伸び続けるおそれがあったので外しました。ボタンにブラウザ既定の太い枠が出ていたのも直しています。
- **day124**：手順の図なので色は入れていません。黒い番号の列と黒枠を面に直しました。
- **day133**：補間の秒数の棒を青にしました。タイムラインの黒枠は灰の帯に変えています。
- **day143**：色は入れていません。白いカードの中に灰の大きなパネルがあり、その中の箱が見えなくなっていたので、パネルを白に戻しました。タブ・表の黒枠も直しています。
- **day151**：補間後の欄を薄い青の面に、補間前の角度差の棒を灰にしました。黒枠のパイプライン・比較・グラフは面に直しています。
- **day159**：色は入れていません。くっついていた層の間をあけ、マウスを乗せると黒になるのを薄い灰にしました。
- **day168**：色は入れていません。黒枠のパネルと計測表を面＋細い区切り線にしました。
- **day38**：すでに決まりどおりだったので変えていません。
- **day93**：現行の IsKinematic を薄い青の面に、廃止した FixedJoint の ✗ と点線を赤にしました。黒枠の柱・パネル・状態カード・フローを面に直しています。

## 迷ったもの・自信のないもの

- **avatar-gitai**：選んだ候補の形を青、印を緑・赤にしたので、1つの図に3色が出ます。
- **day151**：上の比較で「補間後」を青にしたので、下の角度差の棒は青にせず灰にしました。棒は補間前の値です。
- **push-hold**：ブロックは記事の中で実物の赤いブロックですが、「悪い」と読まれないよう黒のままにしました。
- **vlm-planning-layer**：凡例の「実線／破線」を「灰の面／灰の破線」に言い換えました。見た目を説明する言葉の変更として扱っています。
- **template-match**：ラベルの位置をずらすために JS の数字を1か所変えました。見た目だけの変更ですが、「動きは変えない」の決まりに触れるかもしれません。
- **mecharm-static-to-dynamic**：ABORT（失敗）と SKIP（lookup 失敗）はどちらも同じ status-ng の分類から決まるので、両方とも赤になっています。
- **day111**：表情の画像は手元の描画では読み込めず、表示を確かめられていません。
- **side-camera と day143**：黒い見出しの列（flow-key など）は黒のまま残しました。
```


## 作業ログ ― p2m-site に入れた commit の一覧

#### p2m-site（GitHub PixelstoMotors/p2m-site）

| commit | 時刻 | 内容 | 変えたファイル |
|---|---|---|---|
| 7fa47c0 | 08:33 | 図解130本の字と色をサイトにそろえる（Inter＋Noto Sans JP・#242424/#f7f7f7） | 130 files changed, 1911 insertions(+), 1911 deletions(-) |
| 0deaa06 | 09:17 | 図解130本に共通の形を入れる（白いカード・角丸は入れ物だけ・細かい斜線） | 130 files changed, 1143 insertions(+), 1125 deletions(-) |
| e6afbab | 09:36 | 図解127本に役割の色を入れる（青＝主役・斜線＝比べる相手・赤＝悪い・緑＝良い） | 127 files changed, 1387 insertions(+), 1337 deletions(-) |
| c2c69cf | 09:58 | LOG468 の図解も新しい決まりにそろえる（白いカード・青＝戻るロボット・赤＝止まりと×） | 1 file changed, 33 insertions(+), 33 deletions(-) |
| efe895f | 10:04 | 図解の枠に2pxの余裕を足す（高さが小数で1px足りず、枠の中にスクロールバーが出ていた） | 1 file changed, 2 insertions(+), 1 deletion(-) |
| 1e27f29 | 10:06 | LOG455 の図解：紙吹雪が見えなかったのを直す（画面の外に出していた） | 1 file changed, 3 insertions(+), 1 deletion(-) |
| 047fb2a | 10:34 | 消えたスプレッドシートの埋め込み13か所を、サイトの中の表に置き換える | 15 files changed, 830 insertions(+), 111 deletions(-) |
| 34444ee | 10:43 | LOG468 の図解：「スタッフの手すき」の棒を灰にする（悪い値ではないため） | 1 file changed, 1 insertion(+) |
| 62e3579 | 10:47 | 図解9本の線の強さを直す（強く見せたい線が弱かった所だけ） | 9 files changed, 11 insertions(+), 11 deletions(-) |
| b5dee02 | 11:13 | 導入シミュレーションの記事のグラフ7枚を、決まりの色と字で描き直した版に差し替える | 3 files changed, 7 insertions(+), 7 deletions(-) |
| 8c3184e | 11:31 | 公開中の Google の埋め込み15本を、サイトの中の表と本文に置き換える | 15 files changed, 1066 insertions(+), 120 deletions(-) |
| d98644e | 12:02 | 記事の図解283本を新しい決まり（字・色・形）にそろえる | 283 files changed, 10114 insertions(+), 12870 deletions(-) |

#### r2r2r-content（記事の作り方の決まり）

| commit | 時刻 | 内容 |
|---|---|---|
| 2affc2e | 08:33 | article_rules: 図解の字と色をサイトにそろえる（Inter＋Noto Sans JP・#242424/#f7f7f7） |
| b77ebad | 09:11 | article_rules: 図解の形（白いカード・角丸は入れ物だけ・細かい斜線）を足す |
| df36093 | 10:16 | article_rules: 図解の線の太さは強さの順番で決める |


決まりは、記事の作り方の決まり（article_rules.md）の「HTML図解」の節と、KIOXIA の STYLE.md に書いた。次に新しい図解やグラフを作るときは、この2つを読んでから作る。残っているのは、店の平面図の部品の色をどうするかと、サムネイル（今は前のまま）の2つ。
