diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog for `hgg-svg`
+
+## 0.1.0.0 — 2026-07-18
+
+First public release on Hackage.
+
+- SVG backend (pure Haskell): `saveSVG` / `saveSVGBound` and friends.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, Toshiaki Honda
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from this
+   software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/examples/CustomMarkDemo.hs b/examples/CustomMarkDemo.hs
new file mode 100644
--- /dev/null
+++ b/examples/CustomMarkDemo.hs
@@ -0,0 +1,69 @@
+-- | Phase 51: custom mark (拡張可能な描画語彙) の最小作例。
+--
+-- core (@MarkKind@ の閉列挙) を **一切触らず**、@customMark id drawFn@ + @encX@/@encY@ で
+-- 新しいプロット型 (ここでは簡易 dendrogram) を定義し、 @dendrogram "leaf" "height"@ と
+-- 組み込み mark (@scatter x y@) と同じ書き味で使えるようにする。 draw 関数は @"leaf"@/@"height"@
+-- 列を @rcResolver@ で読み、 'Primitive' 列を返すだけ。
+--
+-- @
+-- cabal run custom-mark-demo
+-- @
+-- → カレントディレクトリに @custom-mark-demo.svg@ を生成。
+--
+-- 詳細な拡張ガイドは @docs/api-guide/10-custom-marks.md@。 canvas parity が欲しい時は同じ id で
+-- PS registry (@Graphics.Hgg.Custom.registerMark@) に draw 関数を手登録する。
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVGWith)
+import           Graphics.Hgg.Easy
+import           Graphics.Hgg.Primitive   (Primitive (..), Point (..),
+                                           TextStyle (..), TextAnchor (..), solid)
+import           Graphics.Hgg.Spec        (RenderCtx (..), ColRef, ColData (..),
+                                           customMark, encX, encY, resolveNum)
+import           Graphics.Hgg.Unit        (px, (*~))
+import qualified Data.Vector              as V
+
+-- | ① draw: @"leaf"@ (葉の x 位置)・@"height"@ (節の高さ) 列を 'rcResolver' で読み、 併合を
+--   「Π 字」の elbow で描く (簡単のため 4 葉固定トポロジ)。 座標は data 空間で作り 'rcProjectXY'
+--   で px 化するので、 軸 scale/coord に自動追従する。
+dendroDraw :: RenderCtx -> [Primitive]
+dendroDraw ctx =
+  let num nm = maybe [] V.toList (resolveNum (rcResolver ctx) nm)  -- 列を読む
+      p x y  = uncurry Point (rcProjectXY ctx x y)                 -- data→px
+      ls     = solid (rcColor ctx) 1.5
+      -- 子を高さ hy で併合する Π 字 (子の基準高さ by1/by2 から立てる)
+      elbow cx1 by1 cx2 by2 hy =
+        [ PLine (p cx1 by1) (p cx1 hy) ls
+        , PLine (p cx2 by2) (p cx2 hy) ls
+        , PLine (p cx1 hy)  (p cx2 hy) ls ]
+      ts = TextStyle (rcTextColor ctx) 10 "sans-serif" AnchorMiddle 0 "normal" False
+  in case (num "leaf", num "height") of
+       ([x0, x1, x2, x3], [_, h1, h2]) ->            -- leaf=[0,1,2,3], height=[0,1,2]
+         let m01 = (x0 + x1) / 2
+             m23 = (x2 + x3) / 2
+         in concat
+              [ elbow x0 0 x1 0 h1                    -- 葉0,1 → 高さ h1
+              , elbow x2 0 x3 0 h1                    -- 葉2,3 → 高さ h1
+              , elbow m01 h1 m23 h1 h2                -- (0,1) と (2,3) → 高さ h2 = root
+              , [ PText (p x (-0.12)) nm ts
+                | (x, nm) <- zip [x0, x1, x2, x3] ["A", "B", "C", "D"] ] ]
+       _ -> []
+
+-- | ② 名前付き combinator = 普通の mark 化。 x/y 列を束ねて軸 range を自動化する。
+dendrogram :: ColRef -> ColRef -> Layer
+dendrogram x y = customMark "dendrogram" dendroDraw <> encX x <> encY y
+
+main :: IO ()
+main = do
+  -- ③ 使う側 = scatter x y と同じ。 列は resolver (dat) / df |>> で供給。
+  let dat "leaf"   = Just (NumData (V.fromList [0, 1, 2, 3]))  -- 葉の x 位置
+      dat "height" = Just (NumData (V.fromList [0, 1, 2]))     -- 節の高さ (葉基準0 / 1段 / root)
+      dat _        = Nothing
+      spec = purePlot
+        <> layer (dendrogram "leaf" "height")   -- ★ core 無改造・組み込み mark と同じ書き味
+        <> title  "Custom mark demo (簡易 dendrogram)"
+        <> xLabel "leaf" <> yLabel "merge height"
+        <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  saveSVGWith "custom-mark-demo.svg" dat spec
+  putStrLn "wrote custom-mark-demo.svg (custom mark = 簡易 dendrogram・core 無改造)"
diff --git a/examples/DagComparisonDemo.hs b/examples/DagComparisonDemo.hs
new file mode 100644
--- /dev/null
+++ b/examples/DagComparisonDemo.hs
@@ -0,0 +1,373 @@
+-- | Phase 1 A2-A4 完了レビュー用 比較 demo。
+--
+-- @
+-- cabal run dag-comparison-demo
+-- @
+-- → design/dag-parity/ に 6 SVG を書き出す。
+--
+--   * k33-before.svg / k33-after.svg
+--     ─ K3,3 風 reverse pattern (= A→Z, B→Y, C→X)。
+--       v0.1 は ID alphabetical で 3 crossing、 A3+A4 後は 0 crossing。
+--   * hbm-before.svg / hbm-after.svg
+--     ─ 中規模 HBM ModelGraph (= 9 node、 latent 群 → tau 群 → y)。
+--       v0.1 は alphabetical 等間隔、 A3+A4 後は median 整列 + 親 anchor。
+--   * chains-before.svg / chains-after.svg (= A4 効果が明瞭)
+--     ─ 2 平行 chain (a1→a2→a3、 b1→b2→b3)。
+--       v0.1 は a/b が各 rank 内 alphabetical の左右 (= 直線 vertical)。
+--       A4 後も a と b が垂直 chain として保たれる (= 親 anchor が効く)。
+--     ─ 加えて cross-link (= a2→b3) を追加して、 A4 の TD+BU median 効果
+--       (= 親 + 子の中庸位置) を視覚化する。
+--
+-- "before" は 'Graphics.Hgg.Spec.LayoutManual' + 手計算 alphabetical 位置で
+-- v0.1 layout を再現 (= 旧 code を取り出さず、 既存の Manual layout 経路で simulate)。
+-- "after" は通常の 'Graphics.Hgg.DAG.dagPlot' (= LayoutHierarchical default、
+-- A2 network simplex + A3 median + transpose + A4 Brandes-Köpfe TD+BU median) を使う。
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG)
+import           Graphics.Hgg.Unit         (px, (*~))
+import qualified Graphics.Hgg.DAG
+import           Graphics.Hgg.DAG         ((~>))
+import           Graphics.Hgg.Easy
+import qualified Graphics.Hgg.Spec        as Spec
+import           Data.Text                (Text)
+
+main :: IO ()
+main = do
+  -- ===========================================================================
+  -- K3,3 風 reverse pattern: A→Z, B→Y, C→X
+  -- v0.1 (= alphabetical): top [A,B,C] / bottom [X,Y,Z] → 3 crossings
+  -- A3 後 (= median heuristic): top [A,B,C] / bottom [Z,Y,X] → 0 crossings
+  -- ===========================================================================
+  let mkN i lbl x y kind = Spec.dagNode i lbl kind x y
+      -- Before: alphabetical 等間隔 (= v0.1 layoutHierarchical 相当)
+      k33Before =
+        [ mkN "A" "A" 0.0  0.0  NodeLatent
+        , mkN "B" "B" 0.5  0.0  NodeLatent
+        , mkN "C" "C" 1.0  0.0  NodeLatent
+        , mkN "X" "X" 0.0  1.0  NodeObserved
+        , mkN "Y" "Y" 0.5  1.0  NodeObserved
+        , mkN "Z" "Z" 1.0  1.0  NodeObserved
+        ]
+      k33Edges =
+        [ Spec.dagEdge "A" "Z"
+        , Spec.dagEdge "B" "Y"
+        , Spec.dagEdge "C" "X"
+        ]
+      k33BeforeSpec = purePlot
+        <> layer (Spec.dagFromLists k33Before k33Edges Spec.LayoutManual
+                    <> size 22)
+        <> title  "K3,3 reverse: BEFORE (v0.1 alphabetical, 3 crossings)"
+        <> theme  ThemeLight
+        <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+      k33Graph = ("A" :: Text) ~> "Z"
+              <> ("B" :: Text) ~> "Y"
+              <> ("C" :: Text) ~> "X"
+      k33AfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlot k33Graph <> size 22)
+        <> title  "K3,3 reverse: AFTER (A2+A3, 0 crossings)"
+        <> theme  ThemeLight
+        <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+
+  saveSVG "design/dag-parity/k33-before.svg" k33BeforeSpec
+  saveSVG "design/dag-parity/k33-after.svg"  k33AfterSpec
+
+  -- ===========================================================================
+  -- 中規模 HBM 風: alpha/beta が gamma を経由して y に、 直接 y にも貢献
+  -- 接続を意図的に "alphabetical で reorder したくなる" 順にして比較
+  -- ===========================================================================
+  let hbmBefore =
+        [ mkN "alpha"   "α"     0.00 0.0 NodeLatent
+        , mkN "beta"    "β"     0.25 0.0 NodeLatent
+        , mkN "gamma"   "γ"     0.50 0.0 NodeLatent
+        , mkN "delta"   "δ"     0.75 0.0 NodeLatent
+        , mkN "epsilon" "ε"     1.00 0.0 NodeLatent
+        , mkN "tau1"    "τ₁"    0.00 0.5 NodeLatent
+        , mkN "tau2"    "τ₂"    0.50 0.5 NodeLatent
+        , mkN "tau3"    "τ₃"    1.00 0.5 NodeLatent
+        , mkN "y"       "y obs" 0.50 1.0 NodeObserved
+        ]
+      hbmEdges =
+        [ Spec.dagEdge "alpha"   "tau3"   -- ⇒ alphabetical で長距離 cross
+        , Spec.dagEdge "beta"    "tau1"
+        , Spec.dagEdge "gamma"   "tau2"
+        , Spec.dagEdge "delta"   "tau1"
+        , Spec.dagEdge "epsilon" "tau3"
+        , Spec.dagEdge "tau1"    "y"
+        , Spec.dagEdge "tau2"    "y"
+        , Spec.dagEdge "tau3"    "y"
+        ]
+      hbmBeforeSpec = purePlot
+        <> layer (Spec.dagFromLists hbmBefore hbmEdges Spec.LayoutManual
+                    <> size 22)
+        <> title  "HBM 9-node: BEFORE (v0.1 alphabetical)"
+        <> theme  ThemeLight
+        <> widthUnit (900 *~ px) <> heightUnit (600 *~ px)
+
+      hbmGraph =
+           ("alpha"   :: Text) ~> "tau3"
+        <> ("beta"    :: Text) ~> "tau1"
+        <> ("gamma"   :: Text) ~> "tau2"
+        <> ("delta"   :: Text) ~> "tau1"
+        <> ("epsilon" :: Text) ~> "tau3"
+        <> ("tau1"    :: Text) ~> "y"
+        <> ("tau2"    :: Text) ~> "y"
+        <> ("tau3"    :: Text) ~> "y"
+      hbmAfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlot hbmGraph <> size 22)
+        <> title  "HBM 9-node: AFTER (A2 rank + A3 median+transpose)"
+        <> theme  ThemeLight
+        <> widthUnit (900 *~ px) <> heightUnit (600 *~ px)
+
+  saveSVG "design/dag-parity/hbm-before.svg" hbmBeforeSpec
+  saveSVG "design/dag-parity/hbm-after.svg"  hbmAfterSpec
+
+  -- ===========================================================================
+  -- 2 平行 chain + 1 cross-link: A4 効果が一番分かるケース
+  -- v0.1: 各 rank 内 alphabetical (a1, b1) / (a2, b2) / (a3, b3) で等間隔 0/1
+  -- A4 後: TD+BU median で同 chain が垂直整列、 cross-link は緩やかに引かれる
+  -- ===========================================================================
+  let chainsBefore =
+        [ mkN "a1" "a1" 0.0 0.0 NodeLatent
+        , mkN "b1" "b1" 1.0 0.0 NodeLatent
+        , mkN "a2" "a2" 0.0 0.5 NodeLatent
+        , mkN "b2" "b2" 1.0 0.5 NodeLatent
+        , mkN "a3" "a3" 0.0 1.0 NodeObserved
+        , mkN "b3" "b3" 1.0 1.0 NodeObserved
+        ]
+      chainsEdges =
+        [ Spec.dagEdge "a1" "a2"
+        , Spec.dagEdge "a2" "a3"
+        , Spec.dagEdge "b1" "b2"
+        , Spec.dagEdge "b2" "b3"
+        , Spec.dagEdge "a2" "b3"   -- cross-link
+        ]
+      chainsBeforeSpec = purePlot
+        <> layer (Spec.dagFromLists chainsBefore chainsEdges Spec.LayoutManual
+                    <> size 22)
+        <> title  "Chains+cross: BEFORE (v0.1, evenly spaced)"
+        <> theme  ThemeLight
+        <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+      chainsGraph =
+           ("a1" :: Text) ~> "a2"
+        <> ("a2" :: Text) ~> "a3"
+        <> ("b1" :: Text) ~> "b2"
+        <> ("b2" :: Text) ~> "b3"
+        <> ("a2" :: Text) ~> "b3"
+      chainsAfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlot chainsGraph <> size 22)
+        <> title  "Chains+cross: AFTER (A2+A3+A4 TD+BU median anchor)"
+        <> theme  ThemeLight
+        <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+
+  saveSVG "design/dag-parity/chains-before.svg" chainsBeforeSpec
+  saveSVG "design/dag-parity/chains-after.svg"  chainsAfterSpec
+
+  -- ===========================================================================
+  -- Long edge (= rank 差 > 1) + spline routing: A5 効果が分かる
+  -- a → b → c → d (chain) + a → d (long skip edge、 rank 差 3)
+  -- v0.1: a→d は直線で b/c の塊を貫通する
+  -- A5 後: a→d は dummy 経由の Catmull-Rom spline で迂回する
+  -- ===========================================================================
+  let longBefore =
+        [ mkN "a" "a" 0.5 0.00 NodeLatent
+        , mkN "b" "b" 0.5 0.33 NodeLatent
+        , mkN "c" "c" 0.5 0.66 NodeLatent
+        , mkN "d" "d" 0.5 1.00 NodeObserved
+        ]
+      longEdges =
+        [ Spec.dagEdge "a" "b"
+        , Spec.dagEdge "b" "c"
+        , Spec.dagEdge "c" "d"
+        , Spec.dagEdge "a" "d"   -- long edge (= skip 2 ranks)
+        ]
+      longBeforeSpec = purePlot
+        <> layer (Spec.dagFromLists longBefore longEdges Spec.LayoutManual
+                    <> size 22)
+        <> title  "Long skip edge: BEFORE (v0.1, a→d 直線で b/c を貫通)"
+        <> theme  ThemeLight
+        <> widthUnit (600 *~ px) <> heightUnit (500 *~ px)
+      longGraph =
+           ("a" :: Text) ~> "b"
+        <> ("b" :: Text) ~> "c"
+        <> ("c" :: Text) ~> "d"
+        <> ("a" :: Text) ~> "d"   -- long
+      longAfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlot longGraph <> size 22)
+        <> title  "Long skip edge: AFTER (A5 dummy + Catmull-Rom で迂回)"
+        <> theme  ThemeLight
+        <> widthUnit (600 *~ px) <> heightUnit (500 *~ px)
+
+  saveSVG "design/dag-parity/long-before.svg" longBeforeSpec
+  saveSVG "design/dag-parity/long-after.svg"  longAfterSpec
+
+  -- ===========================================================================
+  -- A6: plate (= cluster) aware layout
+  -- 2 plate (= group A / group B) を持つ HBM 風モデル
+  -- v0.1: plate 制約無し → A/B メンバが交互に並ぶことあり、 plate box が node 群を斜めに覆う
+  -- A6 後: 同 plate メンバが rank 内で contiguous → plate box が綺麗な矩形
+  -- ===========================================================================
+  let plateNodes =
+        [ mkN "muA"  "μ_A"  0.20 0.0 NodeLatent
+        , mkN "muB"  "μ_B"  0.80 0.0 NodeLatent
+        , mkN "a1"   "a₁"   0.05 0.5 NodeLatent
+        , mkN "b1"   "b₁"   0.30 0.5 NodeLatent
+        , mkN "a2"   "a₂"   0.55 0.5 NodeLatent
+        , mkN "b2"   "b₂"   0.80 0.5 NodeLatent
+        , mkN "y"    "y"    0.50 1.0 NodeObserved
+        ]
+      plateEdges =
+        [ Spec.dagEdge "muA" "a1"
+        , Spec.dagEdge "muA" "a2"
+        , Spec.dagEdge "muB" "b1"
+        , Spec.dagEdge "muB" "b2"
+        , Spec.dagEdge "a1"  "y"
+        , Spec.dagEdge "a2"  "y"
+        , Spec.dagEdge "b1"  "y"
+        , Spec.dagEdge "b2"  "y"
+        ]
+      plateA = Spec.DAGPlate "plate A (n=2)" ["a1", "a2"]
+      plateB = Spec.DAGPlate "plate B (n=2)" ["b1", "b2"]
+      plateBeforeSpec = purePlot
+        <> layer (Spec.dagFromListsWithPlates plateNodes plateEdges
+                    Spec.LayoutManual [plateA, plateB]
+                    <> size 22)
+        <> title  "Plate-aware: BEFORE (v0.1 a/b 交互配置、 plate box が斜め)"
+        <> theme  ThemeLight
+        <> widthUnit (800 *~ px) <> heightUnit (500 *~ px)
+      plateGraph =
+           ("muA" :: Text) ~> "a1" <> ("muA" :: Text) ~> "a2"
+        <> ("muB" :: Text) ~> "b1" <> ("muB" :: Text) ~> "b2"
+        <> ("a1"  :: Text) ~> "y"  <> ("a2"  :: Text) ~> "y"
+        <> ("b1"  :: Text) ~> "y"  <> ("b2"  :: Text) ~> "y"
+      plateAfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlotWithPlates plateGraph [plateA, plateB]
+                    <> size 22)
+        <> title  "Plate-aware: AFTER (A6 plate メンバ contiguous、 box 矩形)"
+        <> theme  ThemeLight
+        <> widthUnit (800 *~ px) <> heightUnit (500 *~ px)
+
+  saveSVG "design/dag-parity/plate-before.svg" plateBeforeSpec
+  saveSVG "design/dag-parity/plate-after.svg"  plateAfterSpec
+
+  -- ===========================================================================
+  -- 並列 edge (= a→b を 3 本) の表現比較。 旧実装は完全に重なって 1 本にしか見えなかった。
+  -- 新: 各並列 edge を perpendicular にずらした 3 点 spline 化、 dot 同等の「並ぶ曲線」 に。
+  -- ===========================================================================
+  let parGraph = (("a" :: Text) ~> "b")
+              <> (("a" :: Text) ~> "b")
+              <> (("a" :: Text) ~> "b")
+              <> (("b" :: Text) ~> "c")
+              <> (("b" :: Text) ~> "c")
+      parAfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlot parGraph <> size 22)
+        <> title  "Parallel edges: AFTER (= perpendicular bend、 3 本 / 2 本)"
+        <> theme  ThemeLight
+        <> widthUnit (500 *~ px) <> heightUnit (500 *~ px)
+  saveSVG "design/dag-parity/parallel-after.svg" parAfterSpec
+
+  -- ===========================================================================
+  -- Phase 39 A2-8a: plate 跨ぎ skip edge。
+  -- mu→{t1,t2}→y, s→y, plate[t1,t2]、 さらに mu→y (= plate の rank を跨ぐ skip)。
+  -- 期待: mu→y は plate 箱を貫通せず、 箱の縦全域を外側で迂回する (graphviz cluster と同様)。
+  -- ===========================================================================
+  let pcPlate = Spec.DAGPlate "plate (n=2)" ["t1", "t2"]
+      pcGraph =
+           ("mu" :: Text) ~> "t1" <> ("mu" :: Text) ~> "t2"
+        <> ("t1" :: Text) ~> "y"  <> ("t2" :: Text) ~> "y"
+        <> ("s"  :: Text) ~> "y"
+        <> ("mu" :: Text) ~> "y"   -- plate 跨ぎ skip edge
+      pcAfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlotWithPlates pcGraph [pcPlate]
+                    <> size 22)
+        <> title  "Plate-crossing skip: mu->y は plate 箱を外迂回すべき"
+        <> theme  ThemeLight
+        <> widthUnit (700 *~ px) <> heightUnit (520 *~ px)
+  saveSVG "design/dag-parity/plate-cross-after.svg" pcAfterSpec
+
+  -- ===========================================================================
+  -- 難ケース: plate box が src→snk skip の **直線経路上**に来る配置。
+  -- src が plate 中央上、 snk が plate 中央下にあり、 src→snk を真っ直ぐ引くと
+  -- box を貫通する。 box を避けて迂回できるか (= obstacle routing の本検証) を見る。
+  -- 期待: src→snk は plate {p0,p1} を貫通せず外を迂回する。
+  -- ===========================================================================
+  let ptPlate = Spec.DAGPlate "plate (n=2)" ["p0", "p1"]
+      ptGraph =
+           ("src" :: Text) ~> "p0" <> ("src" :: Text) ~> "p1"
+        <> ("p0"  :: Text) ~> "snk" <> ("p1" :: Text) ~> "snk"
+        <> ("src" :: Text) ~> "snk"  -- box 直下を跨ぐ skip
+      ptAfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlotWithPlates ptGraph [ptPlate]
+                    <> size 22)
+        <> title  "Plate-through skip: src->snk は box を貫通せず迂回すべき"
+        <> theme  ThemeLight
+        <> widthUnit (640 *~ px) <> heightUnit (520 *~ px)
+  saveSVG "design/dag-parity/plate-through-after.svg" ptAfterSpec
+
+  -- ===========================================================================
+  -- A4 検証用: nested plate (= 外側 plate の中に兄弟 inner plate 2 つ)。
+  -- mu → {gA,gB}、 gA → {xa1,xa2}、 gB → {xb1,xb2}、 全 x → y。
+  -- 外側 plate "model" が gA,gB,xa* ,xb* を、 inner "A"/"B" が各 xa*/xb* を囲む。
+  -- 期待 (graphviz contain/separate_subclust): 外箱が内箱を完全内包し、 兄弟
+  -- inner A/B が x 方向で重ならない。 現状 (A4 前) の重なりを実測する。
+  -- ===========================================================================
+  let nestGraph =
+           ("mu" :: Text) ~> "gA" <> ("mu" :: Text) ~> "gB"
+        <> ("gA" :: Text) ~> "xa1" <> ("gA" :: Text) ~> "xa2"
+        <> ("gB" :: Text) ~> "xb1" <> ("gB" :: Text) ~> "xb2"
+        <> ("xa1" :: Text) ~> "y" <> ("xa2" :: Text) ~> "y"
+        <> ("xb1" :: Text) ~> "y" <> ("xb2" :: Text) ~> "y"
+      nestOuter = Spec.DAGPlate "model"  ["gA", "gB", "xa1", "xa2", "xb1", "xb2"]
+      nestA     = Spec.DAGPlate "A (n=2)" ["xa1", "xa2"]
+      nestB     = Spec.DAGPlate "B (n=2)" ["xb1", "xb2"]
+      nestAfterSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlotWithPlates nestGraph
+                    [nestOuter, nestA, nestB] <> size 22)
+        <> title  "Nested plate: 外箱が内箱を内包・兄弟 A/B は非重複であるべき"
+        <> theme  ThemeLight
+        <> widthUnit (760 *~ px) <> heightUnit (560 *~ px)
+  saveSVG "design/dag-parity/nested-after.svg" nestAfterSpec
+
+  -- ===========================================================================
+  -- A4 stress 1: 3 レベル深い nest (outer ⊃ mid ⊃ inner)。 margin 累積を実測。
+  -- 期待: 各境界が 1 段ぶん margin で離れる (= graphviz は各 level に CL_OFFSET)。
+  -- ===========================================================================
+  let deepGraph =
+           ("a" :: Text) ~> "b" <> ("b" :: Text) ~> "c" <> ("c" :: Text) ~> "d"
+      deepOuter = Spec.DAGPlate "L1" ["b", "c", "d"]
+      deepMid   = Spec.DAGPlate "L2" ["c", "d"]
+      deepInner = Spec.DAGPlate "L3" ["d"]
+      deepSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlotWithPlates deepGraph
+                    [deepOuter, deepMid, deepInner] <> size 22)
+        <> title  "Deep nest (L1 superset of L2 superset of L3): margin 累積"
+        <> theme  ThemeLight
+        <> widthUnit (560 *~ px) <> heightUnit (640 *~ px)
+  saveSVG "design/dag-parity/nested-deep-after.svg" deepSpec
+
+  -- ===========================================================================
+  -- A4 stress 2: 同 rank に 3 兄弟 inner plate。 兄弟分離 (= A1 keepout で
+  -- 兄弟 member は互いに非member ゆえ排除される) を実測。 box 重なりが無いこと。
+  -- ===========================================================================
+  let triGraph =
+           ("m" :: Text) ~> "p" <> ("m" :: Text) ~> "q" <> ("m" :: Text) ~> "r"
+        <> ("p" :: Text) ~> "z" <> ("q" :: Text) ~> "z" <> ("r" :: Text) ~> "z"
+      triOuter = Spec.DAGPlate "all" ["p", "q", "r"]
+      triP = Spec.DAGPlate "P" ["p"]
+      triQ = Spec.DAGPlate "Q" ["q"]
+      triR = Spec.DAGPlate "R" ["r"]
+      triSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlotWithPlates triGraph
+                    [triOuter, triP, triQ, triR] <> size 22)
+        <> title  "3 兄弟 inner plate: box 重なり無しであるべき"
+        <> theme  ThemeLight
+        <> widthUnit (720 *~ px) <> heightUnit (520 *~ px)
+  saveSVG "design/dag-parity/nested-tri-after.svg" triSpec
+
+  putStrLn "Wrote 15 SVGs to design/dag-parity/"
+  putStrLn "  k33-before.svg / k33-after.svg       (= reverse pattern、 3 crossings -> 0)"
+  putStrLn "  hbm-before.svg / hbm-after.svg       (= 9-node HBM、 alphabetical -> median+anchor)"
+  putStrLn "  chains-before.svg / chains-after.svg (= A4 TD+BU 親 anchor が見える)"
+  putStrLn "  long-before.svg / long-after.svg     (= A5 dummy 経由 spline で長 edge 迂回)"
+  putStrLn "  plate-before.svg / plate-after.svg   (= A6 plate メンバ contiguous で box 矩形)"
diff --git a/examples/DagParityBench.hs b/examples/DagParityBench.hs
new file mode 100644
--- /dev/null
+++ b/examples/DagParityBench.hs
@@ -0,0 +1,158 @@
+-- | Phase 1 A9 — graphviz CLI との parity check 用 bench。
+--
+-- @
+-- cabal run dag-parity-bench
+-- @
+-- → design/dag-parity/parity/{small,medium,large}/ に
+--   hgg.svg + input.dot + crossings.txt を出力。
+--
+-- 後段 'scripts/dag-parity-check.sh' が dot CLI で input.dot を SVG 化 (= graphviz.svg)、
+-- side-by-side HTML を生成する。 dot が無ければ hgg 出力のみ。
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG)
+import qualified Graphics.Hgg.DAG
+import           Graphics.Hgg.DAG         ((~>))
+import           Graphics.Hgg.DAG.Internal.Sugiyama
+                                          (assignOrderFull, assignRanks,
+                                           buildLayoutGraph, countCrossings)
+import           Graphics.Hgg.Easy
+import qualified Graphics.Hgg.Spec        as Spec
+import           Graphics.Hgg.Unit        (px, (*~))
+import           Data.Text                (Text)
+import qualified Data.Text                as T
+import qualified Data.Text.IO             as TIO
+import           System.Directory         (createDirectoryIfMissing)
+
+main :: IO ()
+main = do
+  -- canvas サイズは dot の見た目に合わせる: 各 case の rank 数 (= 縦) と
+  -- rank あたり最大幅 (= 横) を見て、 dot 既定の出力 aspect に近づける。
+  --   small (rank 5 程度、 幅 3-4): 400x500 (= 縦長気味)
+  --   medium (rank ~30、 幅 1-2):   400x1400 (= かなり縦長、 dot chain と同形)
+  --   large (rank ~12、 幅 5):     800x1100 (= dot の portrait に近い)
+  mapM_ runCase
+    [ ("small",  buildSmall,  "N=10、 単純な階層 HBM 様",
+        400,  500)
+    , ("medium", buildMedium, "N=30、 chain + skip edges",
+        400, 1400)
+    , ("large",  buildLarge,  "N=60、 5 chain 並列 + cross + long",
+        800, 1500)
+    , ("isolated", buildIsolated,
+        "孤立 4 node + chain 6 = 10、 孤立が上部に横並びになるか確認",
+        600, 400)
+    ]
+  putStrLn ""
+  putStrLn "Done. 次に scripts/dag-parity-check.sh を実行して dot 比較 + HTML 生成。"
+
+-- ===========================================================================
+-- Test case 1: small (N=10)
+-- ===========================================================================
+
+buildSmall :: ([Text], [(Text, Text)])
+buildSmall =
+  let ns = [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j" ]
+      es = [ ("a", "c"), ("b", "c"), ("c", "d"), ("c", "e")
+           , ("d", "f"), ("e", "f"), ("d", "g"), ("e", "h")
+           , ("f", "i"), ("g", "j"), ("h", "j"), ("i", "j")
+           , ("a", "j")  -- long skip edge
+           ]
+  in (ns, es)
+
+buildMedium :: ([Text], [(Text, Text)])
+buildMedium =
+  let ns = [ T.pack ("n" <> show k) | k <- [0 .. 29 :: Int] ]
+      -- chain n0 → n1 → n2 → ... → n29
+      chain = [ (ns !! k, ns !! (k + 1)) | k <- [0 .. 28] ]
+      -- 横方向 skip edges
+      skips = [ (ns !! 0, ns !! 5), (ns !! 3, ns !! 10)
+              , (ns !! 7, ns !! 15), (ns !! 12, ns !! 22)
+              , (ns !! 18, ns !! 28)
+              ]
+  in (ns, chain <> skips)
+
+-- | 孤立 node が含まれるケース。 isoX (= 4 個) は edge を持たず、 chain a-f (= 5 edge)
+-- とは独立。 dot は孤立 4 個を上部に横並びで配置し、 hgg も同じ挙動 (= 'mkGraph'
+-- が nodeIds 全てを 'vertex' で先に登録するため) を取る。
+buildIsolated :: ([Text], [(Text, Text)])
+buildIsolated =
+  let ns = [ "a", "b", "c", "d", "e", "f"
+           , "iso1", "iso2", "iso3", "iso4" ]
+      es = [ ("a","b"), ("b","c"), ("c","d"), ("d","e"), ("e","f") ]
+  in (ns, es)
+
+buildLarge :: ([Text], [(Text, Text)])
+buildLarge =
+  let ns = [ T.pack ("v" <> show k) | k <- [0 .. 59 :: Int] ]
+      -- 5 disjoint chain × 12 node = 60 node、 全 node が edge を持つ。
+      -- chain c は node [12c .. 12c+11] を使う (= 旧実装の indexing 重複 bug を修正、
+      -- 孤立 22 node が rank 0 に山積みになるのを防ぐ)。
+      chains = [ (ns !! (12 * c + k), ns !! (12 * c + k + 1))
+               | c <- [0 .. 4], k <- [0 .. 10] ]
+      -- 同 rank 間の cross-link (= chain c の rank k → chain c+1 の rank k)
+      cross  = [ (ns !! (12 * c + k), ns !! (12 * (c + 1) + k))
+               | c <- [0 .. 3], k <- [0, 4, 8] ]
+      -- chain 内の長 skip edge
+      longs  = [ (ns !! 0, ns !! 11), (ns !! 12, ns !! 22)
+               , (ns !! 24, ns !! 35) ]
+  in (ns, chains <> cross <> longs)
+
+-- ===========================================================================
+-- Case runner
+-- ===========================================================================
+
+runCase :: (FilePath, ([Text], [(Text, Text)]), String, Double, Double) -> IO ()
+runCase (name, (nodeIds, edges), desc, w, h) = do
+  let dir = "design/dag-parity/parity/" <> name
+  createDirectoryIfMissing True dir
+
+  -- hgg SVG 生成 (canvas は case ごとに調整、 dot の auto sizing に合わせる)
+  let g = mkGraph nodeIds edges
+      spec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlot g <> size 11)
+        <> title (T.pack (name <> " (" <> desc <> ")"))
+        <> theme  ThemeLight
+        <> widthUnit (w *~ px) <> heightUnit (h *~ px)
+  saveSVG (dir <> "/hgg.svg") spec
+
+  -- DOT 出力 (= graphviz CLI 用入力)
+  TIO.writeFile (dir <> "/input.dot") (toDot name nodeIds edges)
+
+  -- hgg 内部の crossings 数を計測 (= LayoutGraph 経由)
+  let lg0 = assignRanks (buildLayoutGraph nodeIds edges)
+      (lg1, om, _) = assignOrderFull lg0
+      crossings = countCrossings lg1 om
+  writeFile (dir <> "/crossings.txt")
+    ("hgg crossings: " <> show crossings <> "\n"
+       <> "N nodes: " <> show (length nodeIds) <> "\n"
+       <> "N edges: " <> show (length edges) <> "\n")
+
+  putStrLn $ "  " <> name <> ": " <> show (length nodeIds)
+          <> " nodes, " <> show (length edges) <> " edges, "
+          <> "hgg crossings = " <> show crossings
+
+-- ===========================================================================
+-- Helpers
+-- ===========================================================================
+
+-- | Text 列 + edge 列を Graph に。
+-- 全 nodeIds を 'vertex' で先に登録してから edge を overlay する。 これで edge に
+-- 含まれない孤立 node も Graph に残り、 dot の DOT (= 全 node 宣言) と挙動が揃う。
+mkGraph :: [Text] -> [(Text, Text)] -> Graphics.Hgg.DAG.Graph Text
+mkGraph nodeIds edges =
+  let vs = foldr (\v acc -> acc <> Graphics.Hgg.DAG.vertex v) mempty nodeIds
+      es = foldr (\(f, t) acc -> acc <> (f ~> t)) mempty edges
+  in vs <> es
+
+-- | DOT 文字列を生成。 graphviz CLI の `dot -Tsvg input.dot` で SVG 化可。
+toDot :: String -> [Text] -> [(Text, Text)] -> Text
+toDot name ns es = T.unlines $
+  [ T.pack ("digraph " <> name <> " {")
+  , "  rankdir=TB;"
+  , "  node [shape=ellipse, fontsize=10];"
+  ] <> [ "  " <> sanitize n <> ";" | n <- ns ]
+    <> [ "  " <> sanitize f <> " -> " <> sanitize t <> ";" | (f, t) <- es ]
+    <> [ "}" ]
+  where
+    sanitize = T.replace "-" "_"
diff --git a/examples/DfPlotDemo.hs b/examples/DfPlotDemo.hs
new file mode 100644
--- /dev/null
+++ b/examples/DfPlotDemo.hs
@@ -0,0 +1,85 @@
+-- | api-guide 05-dataframe (DataFrame 連携) 用の図生成デモ。
+--   ゼロ依存の Map ベース df を @(|>>)@ でバインドし、 列名だけで図を書く。
+--
+--   @cabal run df-plot-demo@ → @design/df-plot/*.svg@ を生成。
+{-# LANGUAGE OverloadedStrings #-}
+module Main (main) where
+
+import           Graphics.Hgg.Backend.SVG (saveSVGBound)
+import           Graphics.Hgg.Easy
+import           Graphics.Hgg.Frame       ((|>>), BoundPlot, bpDiagnostics)
+import           Data.Text                (Text)
+import qualified Data.Map.Strict          as M
+import qualified Data.Vector              as V
+import           System.Directory         (createDirectoryIfMissing)
+
+-- df の列値は ColData (NumData/TxtData)。 数値列・文字列列のヘルパ。
+num :: [Double] -> ColData
+num = NumData . V.fromList
+
+txt :: [Text] -> ColData
+txt = TxtData . V.fromList
+
+out :: FilePath -> BoundPlot -> IO ()
+out name = saveSVGBound ("design/df-plot/" <> name <> ".svg")
+
+main :: IO ()
+main = do
+  createDirectoryIfMissing True "design/df-plot"
+
+  -- 1 枚の df を用意 (Map Text ColData。 列名で参照する)
+  let df = M.fromList
+        [ ("x",     num [1,2,3,4,5,6,7,8,9,10])
+        , ("y",     num [2.1,3.9,6.0,7.7,10.2,11.8,14.1,15.9,18.2,20.0])
+        , ("size",  num [2,8,3,9,4,7,5,6,3,8])
+        , ("group", txt (take 10 (cycle ["A","B"]))) ] :: M.Map Text ColData
+
+  -- (a) 散布図 + group で色分け + size で大きさ (全部「列名」で指定)
+  out "01-scatter-color-size" $
+    df |>> ( layer (scatter "x" "y" <> colorBy "group" <> sizeBy "size" <> alpha 0.85)
+           <> title "df |>> scatter (color/size を列名で)" )
+
+  -- (b) 重畳: 散布図 + 折れ線 (同じ df の別列を別 layer に)
+  out "02-overlay" $
+    df |>> ( layer (scatter "x" "y" <> size 6)
+           <> layer (line "x" "y" <> color (fromHex "#d62728") <> stroke 1)
+           <> title "df |>> (scatter <> line)" )
+
+  -- (c) facet: group 列で小分け
+  out "03-facet" $
+    df |>> ( layer (scatter "x" "y" <> colorBy "group" <> size 6)
+           <> facet "group"
+           <> title "df |>> scatter <> facet \"group\"" )
+
+  -- (d) 棒グラフ: カテゴリ列 + 群 + position (dodge)
+  let dfB = M.fromList
+        [ ("cat", txt (concatMap (replicate 3) ["A","B","C"]))
+        , ("grp", txt (take 9 (cycle ["x","y","z"])))
+        , ("val", num [3,5,2, 4,1,6, 2,3,4]) ] :: M.Map Text ColData
+  out "04-bar-dodge" $
+    dfB |>> ( layer (bar "cat" "val" <> colorBy "grp" <> position PosDodge)
+            <> title "df |>> bar <> color <> position PosDodge" )
+
+  -- (e) Phase 26 A2: vector field (quiver)。 格子点 (x,y) に渦巻き風の場 (u,v) の矢印。
+  let gridPts = [ (gx, gy) | gx <- [-3,-2..3], gy <- [-3,-2..3::Double] ]
+      uOf x y = -y/3 - x/6        -- 回転 + 内向き
+      vOf x y =  x/3 - y/6
+      dfQ = M.fromList
+        [ ("x", num [ x | (x,_) <- gridPts ])
+        , ("y", num [ y | (_,y) <- gridPts ])
+        , ("u", num [ uOf x y | (x,y) <- gridPts ])
+        , ("v", num [ vOf x y | (x,y) <- gridPts ]) ] :: M.Map Text ColData
+  out "05-quiver" $
+    dfQ |>> ( layer (quiver "x" "y" "u" "v" <> color (fromHex "#1f77b4"))
+            <> title "df |>> quiver \"x\" \"y\" \"u\" \"v\" (vector field)" )
+
+  -- (f) Phase 26 A2: 同じ場を magnitude で連続色マップ (arrowColorByMagnitude)。
+  out "06-quiver-magnitude" $
+    dfQ |>> ( layer (quiver "x" "y" "u" "v" <> arrowColorByMagnitude <> arrowScale 1.2)
+            <> title "df |>> quiver <> arrowColorByMagnitude (|u,v| -> viridis)" )
+
+  -- バインド時の列名検証 (純関数・例外なし。 診断は値として載る)
+  let bad = df |>> layer (scatter "x" "wieght")   -- typo
+  putStrLn ("診断 (typo 列): " <> show (bpDiagnostics bad))
+
+  putStrLn "wrote design/df-plot/*.svg (6 examples)"
diff --git a/examples/DocFig/Common.hs b/examples/DocFig/Common.hs
new file mode 100644
--- /dev/null
+++ b/examples/DocFig/Common.hs
@@ -0,0 +1,73 @@
+-- | api-guide 図ジェネレータの共通基盤 (P3.1 = api-guide 再構成)。
+--   ページ別モジュール (DocFig.Quickstart / DocFig.Layers / DocFig.Decoration) が
+--   'Figure' のリストを公開し、 Main がまとめて 'renderFigure' する。各 'Figure' の
+--   ファイル名は静的リテラルなので、 orphan ゲート (md 参照 ↔ emit 突合) が build 無しで
+--   emit 名を列挙できる。
+{-# LANGUAGE OverloadedStrings #-}
+module DocFig.Common
+  ( Figure (..)
+  , outDir
+  , fig, figW, figR
+  , renderFigure
+  , linFit, lcg
+  , module Graphics.Hgg.Easy
+  ) where
+
+import           Graphics.Hgg.Easy
+import           Graphics.Hgg.Unit        (px, (*~))
+import           Graphics.Hgg.Backend.SVG (saveSVG, saveSVGWith)
+
+-- | 出力先 (repo root から)。
+outDir :: FilePath
+outDir = "docs/api-guide/images/"
+
+-- ===================================================================
+-- 図の宣言
+
+-- | 1 枚の図 = 出力ファイル名 + (任意) 'Resolver' + 最終 spec (サイズ適用済)。
+data Figure = Figure
+  { figFile     :: FilePath
+  , figResolver :: Maybe Resolver
+  , figSpec     :: VisualSpec
+  }
+
+-- | 既定サイズ (640×420 px) を付けた図。
+fig :: FilePath -> VisualSpec -> Figure
+fig name spec = Figure name Nothing
+  (spec <> widthUnit (640 *~ px) <> heightUnit (420 *~ px))
+
+-- | サイズ明示の図 (subplots / gallery 等で横長・大判)。
+figW :: FilePath -> Int -> Int -> VisualSpec -> Figure
+figW name w h spec = Figure name Nothing
+  (spec <> widthUnit (fromIntegral w *~ px) <> heightUnit (fromIntegral h *~ px))
+
+-- | 列名 ('ColByName') の解決に 'Resolver' が要る図用 (facet 等)。既定サイズ。
+figR :: FilePath -> Resolver -> VisualSpec -> Figure
+figR name r spec = Figure name (Just r)
+  (spec <> widthUnit (640 *~ px) <> heightUnit (420 *~ px))
+
+renderFigure :: Figure -> IO ()
+renderFigure (Figure name Nothing  spec) = saveSVG     (outDir ++ name) spec
+renderFigure (Figure name (Just r) spec) = saveSVGWith (outDir ++ name) r spec
+
+-- ===================================================================
+-- 図を安定させる小道具
+
+-- | 最小二乗 (傾き, 切片)。
+linFit :: [Double] -> [Double] -> (Double, Double)
+linFit xs ys =
+  let m  = fromIntegral (length xs)
+      sx = sum xs; sy = sum ys
+      sxx = sum (map (^ (2 :: Int)) xs)
+      sxy = sum (zipWith (*) xs ys)
+      a  = (m * sxy - sx * sy) / (m * sxx - sx * sx)
+      b  = (sy - a * sx) / m
+  in (a, b)
+
+-- | 0..1 の決定的擬似乱数列 (線形合同法、 図を安定させる)。
+lcg :: Int -> [Double]
+lcg seed =
+  let next s = (1103515245 * s + 12345) `mod` 2147483648
+      go s = let s' = next s
+             in fromIntegral s' / 2147483648 : go s'
+  in go seed
diff --git a/examples/DocFig/Decoration.hs b/examples/DocFig/Decoration.hs
new file mode 100644
--- /dev/null
+++ b/examples/DocFig/Decoration.hs
@@ -0,0 +1,239 @@
+-- | 03-decoration.md の図 (ラベル / scale / theme / facet / subplot / 座標 /
+--   参照線 / 注釈 / 重畳)。
+{-# LANGUAGE OverloadedStrings #-}
+module DocFig.Decoration (figures) where
+
+import           Data.Text     (Text)
+import qualified Data.Vector  as V
+import           DocFig.Common
+
+figures :: [Figure]
+figures =
+  -- Lesson 4: 重畳 (scatter + 回帰直線 overlay)
+  [ fig "lesson4-overlay.svg" $
+         purePlot
+      <> layer (scatter (inline oxs) (inline oys) <> alpha 0.85 <> size 6)
+      <> layer (line    (inline oxs) (inline ofit) <> color (fromHex "#dc2626") <> stroke 2)
+      <> title "Lesson 4: 重畳 (散布 + 回帰直線)" <> xLabel "x" <> yLabel "y"
+
+    -- 3c タイトル・ラベル: labs を一括
+  , fig "s3c-labs.svg" $
+         purePlot
+      <> layer (scatter xs ys <> size 6)
+      <> labs (emptyLabs
+           { labsTitle = Just "3c. title"
+           , labsSubtitle = Just "subtitle (副題)"
+           , labsCaption = Just "caption (脚注)"
+           , labsX = Just "x 軸", labsY = Just "y 軸" })
+
+    -- 3d scale: 連続色 gradient (scaleColorGradient2)
+  , fig "s3d-scale.svg" $
+         purePlot
+      <> layer (scatter xs ys <> colorContinuousBy cz <> size 9)
+      <> scaleColorGradient2 "#2166AC" "#F7F7F7" "#B2182B" 3.0
+      <> legend
+      <> title "3d. scaleColorGradient2 (連続色)"
+
+    -- 3e theme: ThemeDark
+  , fig "s3e-theme.svg" $
+         purePlot
+      <> layer (scatter xs ys <> color (fromHex "#38bdf8") <> size 6)
+      <> theme ThemeDark
+      <> title "3e. theme ThemeDark"
+
+    -- 3e theme gallery: 代表テーマ全 13 種を 1 枚に
+  , figW "s3e-theme-gallery.svg" 1280 1040 $
+         subplots (map themePanel allThemes) <> subplotCols 4
+      <> title "theme 一覧 (代表テーマ 13 種)"
+
+    -- 3e theme × subplots: 外側に theme を足すと全 panel に効く
+  , figW "s3e-theme-subplots.svg" 960 400 $
+         subplots [ layer (scatter xs ys <> color (fromHex "#38bdf8") <> size 5) <> title "散布"
+                  , layer (bar cats vals) <> title "棒" ]
+      <> subplotCols 2 <> theme ThemeDark
+      <> title "subplots <> theme ThemeDark (外側 theme が全 panel に伝播)"
+
+    -- 3f facet: facetWrap で群ごとに小分け (facet 列は名前解決が要るため Resolver 版)
+  , figR "s3f-facet.svg" rFacet $
+         purePlot
+      <> layer (scatter "x" "y" <> colorBy "g" <> size 6)
+      <> facetWrap "g" 2
+      <> title "3f. facetWrap"
+
+    -- 3f-2 subplot: 独立図の並置 (subplots) ─ geom も軸も別でよい
+  , fig "s3f2-subplot.svg" $
+         subplots [ layer (scatter (inline [1,2,3,4,5]) (inline [2,4,3,5,4])) <> title "散布"
+                  , layer (line    (inline [1,2,3,4,5]) (inline [1,3,2,4,3])) <> title "折れ線"
+                  , layer (bar cats vals)                                     <> title "棒" ]
+      <> subplotCols 3
+      <> title "3f-2. subplots ─ 独立図の並置"
+
+    -- 3f-2 selectPanels (Phase 18 A1): repeatFields で量産した panel から名前で選択
+  , fig "s3f2-select-panels.svg" $
+         repeatFields ["a", "b", "c", "d"] selPanelOf
+      <> selectPanels ["c", "a"]
+      <> subplotCols 2
+      <> title "3f-2. selectPanels ─ panel の名前選択 (c, a の順)"
+
+    -- 3d discrete limits (Phase 18 A2): forest の行を選択 + 列挙順に並べ替え
+  , fig "s3d-discrete-limits.svg" $
+         layer (forest (inlineCat (["b0", "b1", "b2", "b3", "sigma"] :: [Text]))
+                       (inline [1.0, 2.0, 0.1, -0.45, 0.3])
+                       (inline [0.2, 0.4, 0.3, 0.1, 0.05])
+                <> forestNull 0)
+      <> scaleYDiscreteLimits ["b1", "b0", "sigma"]
+      <> title "3d. scaleYDiscreteLimits ─ forest の行選択 + 並べ替え"
+
+    -- 3f-2 nested: 入れ子 subplots (B1) ─ 主図 + 周辺分布を入れ子グリッドで
+  , fig "s3f2-nested.svg" $
+         subplots [ layer (scatter nx ny) <> title "主図 (x vs y)"
+                  , subplots [ layer (histogram nx) <> title "x 分布"
+                             , layer (histogram ny) <> title "y 分布" ]
+                      <> subplotCols 1 <> title "周辺分布 (入れ子)" ]
+      <> subplotCols 2
+      <> title "3f-2. nested subplots ─ 主図 + 入れ子グリッド"
+
+    -- 3f-2 concat: 演算子 <-> / <:> での非対称合成 ─ (a <-> b <-> c) <:> d
+  , fig "concat.svg" $
+         ((cA <-> cB <-> cC) <:> cD)
+      <> title "3f-2. (a <-> b <-> c) <:> d (横3列 + 全幅)"
+
+    -- 3g coord: coordFlip で横棒
+  , fig "s3g-coord.svg" $
+         purePlot <> layer (bar cats vals)
+      <> coordFlip
+      <> title "3g. coordFlip (横棒)" <> xLabel "群" <> yLabel "値"
+
+    -- 3h 補助: 参照線 + 凡例
+  , fig "s3h-guides.svg" $
+         purePlot
+      <> layer (scatter xs ys <> colorBy gs <> size 6)
+      <> refHorizontal 2.5
+      <> refVertical 2.5
+      <> legend
+      <> title "3h. refHorizontal / refVertical + legend"
+
+    -- 03 scale: 既製パレット okabeIto
+  , fig "s3d-palette-okabe.svg" $
+         purePlot <> layer (scatter xs ys <> colorBy gs <> size 7) <> palette okabeIto <> legend
+      <> title "palette okabeIto (色覚多様性対応)"
+
+    -- 03 theme: 要素単位の部分上書き
+  , fig "s3e-theme-override.svg" $
+         purePlot <> layer (scatter xs ys <> size 6)
+      <> theme ThemeMinimal
+      <> titleFont (fontSize 18 <> fontWeight "bold")
+      <> tickFont  (fontSize 10 <> fontColor "#64748b")
+      <> panelBorder True <> themeGrid False <> themeAxisTextAngle 45
+      <> title "theme 要素の部分上書き"
+
+    -- 03 theme × facet strip
+  , figR "s3e-theme-strip.svg" rFacet $
+         purePlot <> layer (scatter "x" "y" <> colorBy "g") <> facet "g"
+      <> themeStrip True <> stripFill "#eef2ff"
+      <> title "facet strip 背景 (stripFill)"
+
+    -- 03 guides: 軸の細かい制御 (log + 範囲 + 回転)
+  , fig "s3h-axis.svg" $
+         purePlot <> layer (scatter axx axy <> size 6)
+      <> xAxis (logAxis <> axisRange 1 1000 <> axisRotate 45)
+      <> yAxis (axisRange 0 100)
+      <> title "xAxis (logAxis <> axisRange <> axisRotate)"
+
+    -- 03 guides: 第 2 軸 (右 y)
+  , figR "s3h-second-axis.svg" rSec $
+         purePlot
+      <> layer (line "t" "price" <> color (fromHex "#1f77b4") <> stroke 2)
+      <> layer (line "t" "volume" <> toRightY <> color (fromHex "#d62728") <> stroke 2)
+      <> yAxisRight (axisRange 0 1000000)
+      <> title "第 2 軸 (price=左 / volume=右)"
+
+    -- 03 guides: 注釈
+  , fig "s3h-annotate.svg" $
+         purePlot <> layer (scatter xs ys <> size 6)
+      <> annotText 2.0 3.6 "注釈"
+      <> annotArrow 1.4 3.0 1.95 3.5
+      <> annotRect 3.0 1.0 4.0 2.0 "領域"
+      <> marginalX
+      <> insetAt 0.72 0.1 0.25 0.25 (layer (histogram xs))
+      <> title "annotText / annotArrow / annotRect / marginalX / insetAt"
+
+    -- 高度な図: 多数の設定を一枚に
+  , fig "advanced.svg" $
+         purePlot
+      <> layer ( scatter (inline axs) (inline ays)
+                 <> colorContinuousBy (inline azs)
+                 <> sizeBy (inline asz)
+                 <> alpha 0.85 )
+      <> layer ( line (inline axs) (inline afit)
+                 <> color (fromHex "#ef4444") <> stroke 2 )
+      <> scaleSize 4 16
+      <> refHorizontal 1.0
+      <> theme ThemeMinimal
+      <> legend
+      <> labs (emptyLabs
+           { labsTitle    = Just "高度な図: 連続色 + サイズ + 回帰 + 参照線"
+           , labsSubtitle = Just "colorContinuousBy / sizeBy / line overlay / refHorizontal / theme"
+           , labsCaption  = Just "hgg ─ <> で設定を積層"
+           , labsX        = Just "x"
+           , labsY        = Just "y" })
+  ]
+  where
+    xs = inline    [1,2,3,4, 1,2,3,4]
+    ys = inline    [2,3,1,4, 3,1,4,2]
+    gs = inlineCat (concatMap (replicate 4) (["alpha","beta"] :: [Text]))
+    cats = inlineCat (["A","B","C"] :: [Text])
+    vals = inline [3.0, 7.0, 5.0]
+    cz = inline [1.0, 2.5, 4.0, 1.5, 3.0, 4.5, 2.0, 3.5]
+
+    oxs = [1,2,3,4,5,6,7,8,9,10] :: [Double]
+    oys = [1.2, 1.9, 3.4, 3.1, 5.2, 5.0, 6.8, 7.3, 8.1, 9.4]
+    (oa, ob) = linFit oxs oys
+    ofit   = [ oa * x + ob | x <- oxs ]
+
+    themePanel (nm, th) =
+      layer (scatter xs ys <> color (fromHex "#3b82f6") <> size 5) <> theme th <> title nm
+    allThemes :: [(Text, ThemeName)]
+    allThemes =
+      [ ("Default", ThemeDefault), ("Minimal", ThemeMinimal), ("Dark", ThemeDark)
+      , ("Light", ThemeLight), ("Grey", ThemeGrey), ("BW", ThemeBW)
+      , ("Classic", ThemeClassic), ("Void", ThemeVoid), ("Linedraw", ThemeLinedraw)
+      , ("Noir", ThemeNoir), ("Lumen", ThemeLumen)
+      , ("Parchment", ThemeParchment), ("ParchmentDark", ThemeParchmentDark) ]
+
+    selPanelOf f = layer (scatter (inline [1,2,3,4,5 :: Double])
+                                  (inline [2,4,3,5,4 :: Double]))
+                   <> title f
+
+    nx = inline [ sin (0.5 * fromIntegral i) + 0.15 * fromIntegral (i `mod` 7)
+                | i <- [0 .. 60 :: Int] ]
+    ny = inline [ cos (0.4 * fromIntegral i) | i <- [0 .. 60 :: Int] ]
+
+    cA = layer (scatter (inline [1,2,3,4,5]) (inline [1,4,9,16,25]))            <> title "x²"
+    cB = layer (line    (inline [1,2,3,4,5]) (inline [2,4,8,16,32]))            <> title "2^x"
+    cC = layer (bar (inlineCat (["a","b","c","d"] :: [Text])) (inline [3,7,5,9])) <> title "bar"
+    cD = layer (scatter (inline [0,1,2,3,4]) (inline [0,3,1,4,2]))              <> title "full-width row"
+
+    axx = inline ([1,3,10,30,100,300,1000] :: [Double])
+    axy = inline ([3,7,12,20,33,52,78]     :: [Double])
+
+    n      = 60 :: Int
+    axs    = [ fromIntegral i * 0.18 | i <- [0 .. n - 1] ] :: [Double]
+    noise  = take n (lcg 12345)
+    ays    = [ 0.55 * x + 1.0 + (e - 0.5) * 2.2 | (x, e) <- zip axs noise ]
+    azs    = ays                       -- 連続色は y 値で
+    asz    = [ 0.5 + e | e <- noise ]  -- サイズは別の擬似量
+    (fa, fb) = linFit axs ays
+    afit     = [ fa * x + fb | x <- axs ]
+
+    rFacet :: Resolver
+    rFacet "x" = Just (NumData (V.fromList [1,2,3,4, 1,2,3,4]))
+    rFacet "y" = Just (NumData (V.fromList [2,3,1,4, 3,1,4,2]))
+    rFacet "g" = Just (TxtData (V.fromList (concatMap (replicate 4) ["A","B"])))
+    rFacet _   = Nothing
+
+    rSec :: Resolver
+    rSec "t"      = Just (NumData (V.fromList [1,2,3,4,5,6,7,8,9,10]))
+    rSec "price"  = Just (NumData (V.fromList [10,11,13,12,15,16,15,18,17,20]))
+    rSec "volume" = Just (NumData (V.fromList [3e5,5e5,4e5,7e5,6e5,9e5,8e5,7e5,9e5,1e6]))
+    rSec _        = Nothing
diff --git a/examples/DocFig/EncodingScale.hs b/examples/DocFig/EncodingScale.hs
new file mode 100644
--- /dev/null
+++ b/examples/DocFig/EncodingScale.hs
@@ -0,0 +1,34 @@
+-- | 03-encoding-scale.md の図。channel (列→視覚属性の写像) を 1 枚に集約した
+--   カタログ図を所有する。scale / palette / 軸 (position scale) のデモ図は
+--   共有束縛の都合で当面 DocFig.Decoration が emit する (orphan ゲートは全 *.hs を
+--   横断するため所在は問わない)。
+{-# LANGUAGE OverloadedStrings #-}
+module DocFig.EncodingScale (figures) where
+
+import           Data.Text     (Text)
+import           DocFig.Common
+
+figures :: [Figure]
+figures =
+  -- §1 channel カタログ: 同じ散布を 4 つの channel で写し分ける
+  -- (colorBy / sizeBy / shapeBy / linetypeBy)。mark カタログ (02-layers) は
+  -- mark ごとの実例を持つので、ここは「列→視覚属性」の対比に専念する。
+  [ figW "encoding-channels.svg" 960 760 $
+         subplots
+           [ layer (scatter xs ys <> colorBy g  <> size 6)  <> title "colorBy (カテゴリ→色)"
+           , layer (scatter xs ys <> sizeBy sz)             <> title "sizeBy (数値→点サイズ)"
+           , layer (scatter xs ys <> shapeBy g <> size 6)   <> title "shapeBy (カテゴリ→形)"
+           , layer (line lx ly <> linetypeBy lg <> stroke 2) <> title "linetypeBy (カテゴリ→線種)" ]
+      <> subplotCols 2
+      <> legend
+      <> title "channel ─ 列を視覚属性へ写像する"
+  ]
+  where
+    xs = inline    [1,2,3,4, 1,2,3,4]
+    ys = inline    [2,3,1,4, 3,1,4,2]
+    g  = inlineCat (replicate 4 "a" ++ replicate 4 "b" :: [Text])
+    sz = inline    [1,2,3,4, 4,3,2,1]
+
+    lx = inline    [1,2,3,4,5, 1,2,3,4,5]
+    ly = inline    [1,2,3,4,5, 2,3,3,4,5]
+    lg = inlineCat (replicate 5 "p" ++ replicate 5 "q" :: [Text])
diff --git a/examples/DocFig/Layers.hs b/examples/DocFig/Layers.hs
new file mode 100644
--- /dev/null
+++ b/examples/DocFig/Layers.hs
@@ -0,0 +1,317 @@
+-- | 02-layers.md の図 (mark 索引 + 定型エントリ用サムネイル)。
+--   基本 / 分布 / 区間・エラー / 集計・統計 / 2D 場・行列 / ベクトル場 /
+--   MCMC・ベイズ診断 / DAG の各 mark を 1 枚ずつ。
+{-# LANGUAGE OverloadedStrings #-}
+module DocFig.Layers (figures) where
+
+import           Data.Text     (Text)
+import qualified Data.Vector  as V
+import           DocFig.Common
+
+figures :: [Figure]
+figures =
+  -- 3a geom: bar (棒グラフ)
+  [ fig "s3a-geom.svg" $
+         purePlot <> layer (bar cats vals)
+      <> title "3a. geom: bar" <> xLabel "群" <> yLabel "値"
+
+    -- 3b layer 見た目: position dodge で群分け bar
+  , fig "s3b-aes.svg" $
+         purePlot
+      <> layer (bar bx bv <> colorBy bg <> position PosDodge)
+      <> legend
+      <> title "3b. position PosDodge + color" <> xLabel "群" <> yLabel "値"
+
+    -- 02 encoding: jitterX (整数 x に重なる点を散らす) + densityFill
+  , figW "s2-jitter-density.svg" 960 380 $
+         subplots [ layer (scatter jxs jys <> jitterX 0.25 <> alpha 0.6 <> size 5) <> title "jitterX (重なり点を散らす)"
+                  , layer (density densVals <> densityFill True <> alpha 0.4) <> title "densityFill" ]
+      <> subplotCols 2
+
+    -- 基本: scatter
+  , fig "scatter.svg" $
+         purePlot <> layer (scatter (inline [1,2,3,4,5]) (inline [2,4,3,5,7]))
+      <> title "scatter" <> xLabel "x" <> yLabel "y"
+
+    -- 基本: scatterPoints / linePoints
+  , fig "scatterpoints.svg" $
+         purePlot <> layer (scatterPoints [Point2 1 2, Point2 2 3.5, Point2 3 3, Point2 4 4.2, Point2 5 4])
+      <> title "scatterPoints ([Point2])" <> xLabel "x" <> yLabel "y"
+
+    -- 基本: text / label (ラベルは点の少し上に置いて重なりを避ける・label は枠付き)
+  , fig "text.svg" $
+         purePlot <> layer (scatter txX txY <> size 7) <> layer (label txX txYlab txN)
+      <> title "text / label (点に注釈)" <> xLabel "x" <> yLabel "y"
+
+    -- 基本: stem (lollipop)。 ★ stem は数値 x を取る (categorical 非対応)。
+  , fig "stem.svg" $
+         purePlot <> layer (stem (inline [1,2,3,4,5,6]) (inline [3,7,5,6,4,8]))
+      <> title "stem (lollipop)" <> xLabel "x" <> yLabel "値"
+
+    -- 基本: ecdf
+  , fig "ecdf.svg" $
+         purePlot <> layer (ecdf (inline [3,1,4,1,5,9,2,6,5,3,5,8,9,7,9]))
+      <> title "ecdf (経験累積分布)" <> xLabel "x" <> yLabel "F(x)"
+
+    -- 分布: histogram
+  , fig "histogram.svg" $
+         purePlot <> layer (histogram (inline [1,2,2,3,3,3,4,4,5,2,3,4,3,5,4,3,2,4,3,5]) <> binCount 8)
+      <> title "histogram (binCount 8)" <> xLabel "x" <> yLabel "count"
+
+    -- 分布: boxplot (群色)
+  , fig "boxplot.svg" $
+         purePlot <> layer (boxplot dV <> colorBy dG) <> legend
+      <> title "boxplot (群ごと colorBy)" <> yLabel "value"
+
+    -- 分布: violin / strip / swarm
+  , figW "violin.svg" 1280 420 $
+         subplots [ layer (violin dV <> colorBy dG) <> title "violin"
+                  , layer (strip  dV <> colorBy dG <> jitterX 0.15) <> title "strip"
+                  , layer (swarm  dV <> colorBy dG) <> title "swarm" ]
+      <> subplotCols 3 <> legend
+      <> title "violin / strip / swarm"
+
+    -- 分布: raincloud
+  , fig "raincloud.svg" $
+         purePlot <> layer (raincloud dV <> colorBy dG) <> legend
+      <> title "raincloud (violin + box + strip)" <> yLabel "value"
+
+    -- 分布: ridge (joyplot)
+  , fig "ridge.svg" $
+         purePlot <> layer (ridge rV <> colorBy rG) <> legend
+      <> title "ridge (joyplot)" <> xLabel "value"
+
+    -- 分布: qq
+  , fig "qq.svg" $
+         purePlot <> layer (qq (inline [-1.2,-0.3,0.1,0.5,-0.8,1.4,0.2,-0.1,0.9,-0.5,0.3,-0.6,1.1,-0.2,0.7]))
+      <> title "qq (正規 QQ)" <> xLabel "理論分位点" <> yLabel "標本分位点"
+
+    -- 区間: band + line
+  , fig "band.svg" $
+         purePlot <> layer (band bX bLo bHi <> alpha 0.3) <> layer (line bX bY)
+      <> title "band (x, lo, hi) + line" <> xLabel "x" <> yLabel "y"
+
+    -- 区間: lineRange / pointRange / crossbar (= (x, y, err) 対称)
+  , figW "range.svg" 1280 420 $
+         subplots [ layer (lineRange  rgC rgY rgE) <> title "lineRange"
+                  , layer (pointRange rgC rgY rgE) <> title "pointRange"
+                  , layer (crossbar   rgC rgY rgE) <> title "crossbar" ]
+      <> subplotCols 3
+      <> title "lineRange / pointRange / crossbar (y ± err)"
+
+    -- 区間: forest (= (ラベル, 推定, err) 対称)
+  , fig "forest.svg" $
+         purePlot <> layer (forest (inlineCat (["b0","b1","b2","b3"] :: [Text]))
+                                   (inline [0.2,-0.1,0.4,0.05]) (inline [0.15,0.2,0.1,0.12])
+                            <> forestNull 0)
+      <> title "forest (推定 ± err, null=0)" <> xLabel "効果量"
+
+    -- 区間: funnel (= (effect, SE))
+  , fig "funnel.svg" $
+         purePlot <> layer (funnel (inline [0.1,0.2,-0.1,0.15,0.05,0.12,-0.05,0.18])
+                                   (inline [0.05,0.1,0.08,0.12,0.2,0.06,0.15,0.09]))
+      <> title "funnel (effect vs SE)" <> xLabel "効果量" <> yLabel "SE"
+
+    -- 集計: statFunction
+  , fig "statfunction.svg" $
+         purePlot <> layer (scatter (inline [1,3,5,7,9]) (inline [3,7,10,16,18]))
+                  <> layer (statFunction (\x -> 2 * x + 1) 0 10 100)
+      <> title "statFunction (2x+1)" <> xLabel "x" <> yLabel "y"
+
+    -- 集計: statMean (参照線)
+  , fig "statmean.svg" $
+         purePlot <> layer (histogram smX) <> layer (statMean smX <> linetype LtDashed)
+      <> title "statMean (参照線)" <> xLabel "x" <> yLabel "count"
+
+    -- 集計: countXY
+  , fig "countxy.svg" $
+         purePlot <> layer (countXY (inlineCat (["A","A","B","A","B","B","A","B","B"] :: [Text]))
+                                    (inlineCat (["x","y","x","x","y","y","y","x","y"] :: [Text])))
+      <> title "countXY (頻度)" <> xLabel "x" <> yLabel "y"
+
+    -- 集計: histogramWide
+  , fig "histogramwide.svg" $
+         purePlot <> histogramWide [ inline [1,2,2,3,3,2,3], inline [2,3,3,4,4,3,4], inline [3,4,4,5,5,4,5] ]
+      <> legend <> title "histogramWide (複数列重ね)" <> xLabel "x" <> yLabel "count"
+
+    -- 2D 場: heatmap
+  , fig "heatmap.svg" $
+         purePlot <> layer (heatmap (inlineCat (map fst hmPts)) (inlineCat (map snd hmPts))
+                                    (inline [1,0.3,0.1, 0.3,1,0.5, 0.1,0.5,1]))
+      <> title "heatmap (カテゴリ grid)" <> xLabel "x" <> yLabel "y"
+
+    -- 2D 場: pie
+  , fig "pie.svg" $
+         purePlot <> layer (pie (inlineCat (["A","B","C"] :: [Text])) (inline [30,50,20])) <> legend
+      <> title "pie (円グラフ)"
+
+    -- 2D 場: waterfall
+  , fig "waterfall.svg" $
+         purePlot <> layer (waterfall (inlineCat (["start","Q1","Q2","Q3"] :: [Text])) (inline [100,30,-20,15]))
+      <> title "waterfall (累積寄与)" <> xLabel "段" <> yLabel "累積"
+
+    -- 2D 場: parallelCoords (inline 列・軸名は無し)
+  , fig "parallelcoords.svg" $
+         purePlot <> layer (parallelCoords [ inline [1,2,3], inline [4,5,4], inline [2,1,3], inline [5,4,5] ]
+                                           <> colorBy (inlineCat (["a","b","a"] :: [Text]))) <> legend
+      <> title "parallelCoords (平行座標)"
+
+    -- 2D 場: pairs (SPLOM、 inline 列・軸名は無し)
+  , fig "pairs.svg" $
+         purePlot <> pairs [ inline [1,2,3,4,5], inline [2,1,4,3,5], inline [1,3,2,5,4] ]
+      <> title "pairs (散布図行列)"
+
+    -- 集計2: contour (= 等高線図、 marching squares)
+  , fig "contour.svg" $
+         purePlot
+      <> layer (contour ctX ctY ctZ)
+      <> title "contour (等高線、 2 つの山)" <> xLabel "x" <> yLabel "y"
+
+    -- 2D 場: bin2d (= binned heatmap、 contour の塗り版。 同データ)
+  , fig "bin2d.svg" $
+         purePlot
+      <> layer (bin2d ctX ctY ctZ)
+      <> title "bin2d (binned heatmap、 contour の塗り版)" <> xLabel "x" <> yLabel "y"
+
+    -- 2D 場: hexbin (= 六角ビニング。 散布過密を六角セル件数の連続色で。 geom_hex)
+  , fig "hexbin.svg" $
+         purePlot
+      <> layer (hexbin hxX hxY <> hexbinBins 12)
+      <> title "hexbin (六角ビニング、 件数→連続色)" <> xLabel "x" <> yLabel "y"
+
+    -- ベクトル場: quiver (= vector field、 Phase 26 A2)
+  , fig "quiver.svg" $
+         purePlot
+      <> layer (quiver qX qY qU qV <> arrowColorByMagnitude)
+      <> title "quiver (vector field、 magnitude 連続色)" <> xLabel "x" <> yLabel "y"
+
+    -- MCMC: trace / traceLines (自己完結・reader が再現できる式で生成)
+  , figW "trace.svg" 1080 420 $
+         subplots [ layer (trace (inline (map fromIntegral tcIs)) (inline v1)) <> title "trace (1 chain)"
+                  , layer (traceLines tcIter tcVal tcCh) <> title "traceLines (chain 別)" ]
+      <> subplotCols 2 <> legend
+      <> title "trace / traceLines"
+
+    -- MCMC: ess
+  , fig "ess.svg" $
+         purePlot <> layer (ess (inline [100,200,300,400,500,600])
+                                (inline [80,150,210,260,300,330]))
+      <> title "ess (有効サンプルサイズ)" <> xLabel "iter" <> yLabel "ESS"
+
+    -- MCMC: autocorr (AR(1) 風の減衰系列・自己完結式)
+  , fig "autocorr.svg" $
+         purePlot <> layer (autocorr (inline acSeries) <> autocorrMaxLag 40)
+      <> title "autocorr (自己相関)" <> xLabel "lag" <> yLabel "ACF"
+
+    -- 積層: stream (streamgraph)
+  , fig "stream.svg" $
+         purePlot <> layer (stream stX stY <> colorBy stS) <> legend
+      <> title "stream (streamgraph)" <> xLabel "x" <> yLabel "y"
+
+    -- DAG: y ~ Normal(a + b·x, s) の構造を手書き
+  , fig "dag-manual.svg" $
+         purePlot <> layer (dag dagNodes dagEdges <> size 22)
+      <> title "手書き DAG (a,b→mu→y, s→y)"
+
+    -- DAG: plate で観測ループ (mu/y) を囲む
+  , fig "dag-plate.svg" $
+         purePlot
+      <> layer (dagFromListsWithPlates dagNodes dagEdges LayoutManual
+                 [ DAGPlate { dpLabel = "obs (N)", dpNodeIds = ["mu", "y"] } ] <> size 22)
+      <> title "plate つき DAG (obs を N 個の枠で囲む)"
+
+    -- distCols: 別列を別 mark で 1 パネルに併置 (= <+> の list 版・列名スロット)
+  , figR "distcols.svg" rDist $
+         distCols [ boxplot "a", violin "c", boxplot "d" ]
+      <> title "distCols [box a, violin c, box d]" <> yLabel "value"
+
+    -- distCols × colorBy: レーン内で群ごとに dodge 分割
+  , figR "distcols-colorby.svg" rDist $
+         distCols [ boxplot "a" <> colorBy "g", boxplot "c" ]
+      <> legend
+      <> title "distCols [box a <> colorBy g, box c]" <> yLabel "value"
+  ]
+  where
+    cats = inlineCat (["A","B","C"] :: [Text])
+    vals = inline [3.0, 7.0, 5.0]
+    -- 群分け bar 用 (x category × group)
+    bx = inlineCat (concatMap (replicate 2) (["A","B","C"] :: [Text]))
+    bg = inlineCat (concat (replicate 3 (["g1","g2"] :: [Text])))
+    bv = inline [3.0, 2.0, 5.0, 4.0, 4.0, 6.0]
+
+    jxs = inline ([1,1,1,1,2,2,2,2,3,3,3,3,1,1,2,2,3,3,1,2,3,1,2,3] :: [Double])
+    jys = inline ([2.0,2.4,2.1,1.8,2.6,2.2,2.5,2.0,3.1,3.4,2.9,3.3
+                  ,2.2,2.7,2.4,2.1,3.0,3.2,1.9,2.5,3.1,2.3,2.6,3.4] :: [Double])
+    densVals = inline ([ 4.6,4.9,5.0,5.1,5.4,5.0,4.4,4.9,5.4,4.8,4.8,4.3,5.8,5.7,5.4
+                       , 5.1,5.7,5.1,5.4,5.1,4.6,5.1,4.8,5.0,5.0,5.2,5.2,4.7,4.8,5.4 ] :: [Double])
+
+    txX = inline [1,2,3]; txY = inline [2,3,2.5]
+    txYlab = inline [2.18,3.18,2.68]; txN = inlineCat (["P","Q","R"] :: [Text])
+
+    dV = inline [4,5,6,5,7, 8,9,7,10,9, 5,6,7,6,8]
+    dG = inlineCat (concatMap (replicate 5) (["a","b","c"] :: [Text]))
+
+    rV = inline [1,2,2,3, 3,4,4,5, 5,6,6,7]
+    rG = inlineCat (concatMap (replicate 4) (["a","b","c"] :: [Text]))
+
+    bX = inline [1,2,3,4,5]; bY = inline [2,3,2.5,4,3.5]
+    bLo = inline [1.5,2.4,2.0,3.4,3.0]; bHi = inline [2.5,3.6,3.0,4.6,4.0]
+
+    rgC = inlineCat (["A","B","C"] :: [Text]); rgY = inline [2,3,2.5]; rgE = inline [0.4,0.6,0.3]
+
+    smX = inline [1,2,2,3,3,3,4,4,5]
+
+    hmCs = ["A","B","C"] :: [Text]
+    hmPts = [ (a, b) | a <- hmCs, b <- hmCs ]
+
+    ctGrid = [ (xi, yi) | xi <- [0.0, 0.4 .. 6.0], yi <- [0.0, 0.4 .. 6.0] ]
+    ctX = inline (map fst ctGrid)
+    ctY = inline (map snd ctGrid)
+    ctZ = inline [ exp (-(((x - 3) ** 2) + ((y - 3) ** 2)) / 4)
+                 + 0.4 * exp (-(((x - 1.2) ** 2) + ((y - 4.5) ** 2)) / 1.5)
+                 | (x, y) <- ctGrid ]
+
+    -- hexbin 用の相関した点群 (決定的・対角バンド + 正弦ばらつきで件数差を出す)
+    hxPts = [ (x, y)
+            | i <- [0 .. 299 :: Int]
+            , let fi = fromIntegral i
+                  x  = fromIntegral (i `mod` 20) * 0.3 + sin fi * 0.18
+                  y  = fromIntegral (i `mod` 20) * 0.25 + cos (fi * 1.3) * 0.6
+                       + fromIntegral (i `div` 20) * 0.05 ]
+    hxX = inline (map fst hxPts)
+    hxY = inline (map snd hxPts)
+
+    qGrid = [ (gx, gy) | gx <- [-3, -2 .. 3], gy <- [-3, -2 .. 3 :: Double] ]
+    qX = inline (map fst qGrid)
+    qY = inline (map snd qGrid)
+    qU = inline [ -y / 3 - x / 6 | (x, y) <- qGrid ]
+    qV = inline [  x / 3 - y / 6 | (x, y) <- qGrid ]
+
+    tcN  = 120 :: Int
+    tcIs = [1 .. tcN]
+    v1   = [ 1.0 + 0.3 * sin (fromIntegral i / 9) + 0.1 * sin (fromIntegral i * 7.1) | i <- tcIs ]
+    v2   = [ 1.3 + 0.3 * sin (fromIntegral i / 9) + 0.1 * sin (fromIntegral i * 3.3) | i <- tcIs ]
+    tcIter = inline (map fromIntegral (tcIs ++ tcIs))
+    tcVal  = inline (v1 ++ v2)
+    tcCh   = inlineCat (replicate tcN "1" ++ replicate tcN "2" :: [Text])
+
+    rnd i  = let h = sin (fromIntegral i * 12.9898) * 43758.5453 in h - fromIntegral (floor h :: Int)
+    acSeries = scanl (\prev i -> 0.85 * prev + (rnd i - 0.5)) 0 [1 .. 300 :: Int]
+
+    stX = inline [1,2,3, 1,2,3, 1,2,3]
+    stY = inline [2,3,4, 1,2,1, 3,2,3]
+    stS = inlineCat (["a","a","a","b","b","b","c","c","c"] :: [Text])
+
+    dagNodes = [ dagNodeDist "a"  "a"  NodeLatent        "Normal(0,10)"  0.15 0.0
+               , dagNodeDist "b"  "b"  NodeLatent        "Normal(0,10)"  0.45 0.0
+               , dagNodeDist "s"  "s"  NodeLatent        "HalfNormal(1)" 0.85 0.0
+               , dagNode     "mu" "mu" NodeDeterministic                 0.30 0.5
+               , dagNodeDist "y"  "y"  NodeObserved      "Normal(mu,s)"  0.45 1.0 ]
+    dagEdges = [ dagEdge "a" "mu", dagEdge "b" "mu", dagEdge "mu" "y", dagEdge "s" "y" ]
+
+    rDist :: Resolver
+    rDist "a" = Just (NumData (V.fromList [3,4,4.5,5,5,5.2,5.5,6,6.5,7,8,4.8]))
+    rDist "c" = Just (NumData (V.fromList [6,6.5,7,7.2,7.5,8,8.3,9,9.5,7.8,6.9,8.1]))
+    rDist "d" = Just (NumData (V.fromList [2,2.5,3,3.2,3.5,4,4.3,5,2.8,3.1,3.9,2.2]))
+    rDist "g" = Just (TxtData (V.fromList (concatMap (replicate 6) ["x","y"])))
+    rDist _   = Nothing
diff --git a/examples/DocFig/Quickstart.hs b/examples/DocFig/Quickstart.hs
new file mode 100644
--- /dev/null
+++ b/examples/DocFig/Quickstart.hs
@@ -0,0 +1,26 @@
+-- | 01-quickstart.md の図 (Easy / 文法レイヤ)。
+{-# LANGUAGE OverloadedStrings #-}
+module DocFig.Quickstart (figures) where
+
+import           Data.Text     (Text)
+import           DocFig.Common
+
+figures :: [Figure]
+figures =
+  [ -- Lesson 1: Easy
+    fig "lesson1-easy.svg" $
+         overlay [ points [1,2,3,4,5] [1,4,9,16,25] ]
+      <> title "Lesson 1: y = x²" <> xLabel "x" <> yLabel "y"
+
+    -- Lesson 2: Grammar (色分け + scale_color_manual + legend)
+  , fig "lesson2-grammar.svg" $
+         purePlot
+      <> layer (scatter xs ys <> colorBy gs <> size 7)
+      <> scaleColorManual [("alpha","#1B9E77"), ("beta","#D95F02")]
+      <> legend
+      <> title "Lesson 2: scale_color_manual" <> xLabel "x" <> yLabel "y"
+  ]
+  where
+    xs = inline    [1,2,3,4, 1,2,3,4]
+    ys = inline    [2,3,1,4, 3,1,4,2]
+    gs = inlineCat (concatMap (replicate 4) (["alpha","beta"] :: [Text]))
diff --git a/examples/DocFigures.hs b/examples/DocFigures.hs
new file mode 100644
--- /dev/null
+++ b/examples/DocFigures.hs
@@ -0,0 +1,24 @@
+-- | docs/api-guide/ に埋め込む図を生成する (P3.1 = api-guide 再構成)。
+--   図はページ別モジュール (DocFig.Quickstart / Layers / Decoration) が 'Figure' の
+--   リストとして宣言し、 ここはそれらをまとめて出力するだけの薄い入口。
+--   @cabal run doc-figures@ (repo root から) で docs/api-guide/images/*.svg を生成。
+--   ※ 05 (df/) / 06 (analyze-integration/) / 07 (3d/) の図は別ジェネレータ
+--     (df-plot-demo / 各 bridge demo / plot3d-demo → design/* を api-guide へコピー) が担当。
+module Main (main) where
+
+import           System.Directory (createDirectoryIfMissing)
+
+import           DocFig.Common      (outDir, renderFigure)
+import qualified DocFig.Quickstart    as Quickstart
+import qualified DocFig.Layers        as Layers
+import qualified DocFig.EncodingScale as EncodingScale
+import qualified DocFig.Decoration    as Decoration
+
+main :: IO ()
+main = do
+  createDirectoryIfMissing True outDir
+  let figs = Quickstart.figures ++ Layers.figures
+             ++ EncodingScale.figures ++ Decoration.figures
+  mapM_ renderFigure figs
+  putStrLn ("doc figures written to " ++ outDir
+            ++ " (" ++ show (length figs) ++ " figures)")
diff --git a/examples/GalleryDemo.hs b/examples/GalleryDemo.hs
new file mode 100644
--- /dev/null
+++ b/examples/GalleryDemo.hs
@@ -0,0 +1,1104 @@
+-- | Gallery demo: hgg の機能カタログ用 SVG 出力 (= design/gallery/ へ吐く)。
+--
+-- @
+-- cabal run gallery-demo
+-- @
+--
+-- → design/gallery/{basic,distribution,statistical,decoration,axes,theme,doe}/ 配下に
+-- 各 chart 種別ごとに SVG を生成。 'design/gallery.md' でこれらを参照して表示。
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG, saveSVGWith)
+import           Graphics.Hgg.DAG         (layoutHierarchicalFull)
+import           Graphics.Hgg.Render.Special (bakeDAGRoutesInSpec)
+import           Graphics.Hgg.Unit         (px, (*~))
+import           Graphics.Hgg.Easy
+import qualified Data.Aeson
+import qualified Data.ByteString.Lazy
+import           Data.Text                (Text)
+import qualified Data.Text                as T
+import qualified Data.Vector              as V
+import qualified System.Directory
+import           System.Directory         (createDirectoryIfMissing)
+
+main :: IO ()
+main = do
+  let dirs = ["basic", "distribution", "statistical", "decoration",
+              "axes", "theme", "palette", "doe", "coord", "scale"]
+  mapM_ (\d -> createDirectoryIfMissing True ("design/gallery/" ++ d)) dirs
+
+  putStrLn "Generating gallery SVGs..."
+
+  -- ===========================================================================
+  -- BASIC (= 1 列 chart)
+  -- ===========================================================================
+  let xs   = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double]
+      ys   = map (\x -> x * x) xs
+      vals = [3.0, 4.0, 4.5, 5.0, 5.0, 5.2, 5.5, 6.0, 6.5, 7.0, 8.0, 12.0] :: [Double]
+
+  -- scatter
+  emit "basic/scatter.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline ys) <> alpha 0.85 <> size 5)
+    <> title  "scatter: y = x²"
+    <> xLabel "x" <> yLabel "y"
+
+  -- line
+  let xs2 = [0, 0.5 .. 6.28] :: [Double]
+      ys2 = map sin xs2
+  emit "basic/line.svg" $
+       purePlot
+    <> layer (line (inline xs2) (inline ys2) <> color (fromHex "#2563eb") <> stroke 2)
+    <> title "line: y = sin(x)"
+
+  -- bar
+  emit "basic/bar.svg" $
+       purePlot
+    <> layer (bar (inlineCat (["A", "B", "C", "D", "E"] :: [Text]))
+                   (inline ([8.0, 5.0, 12.0, 6.5, 3.0] :: [Double])))
+    <> title "bar (categorical)"
+
+  -- Phase 9 B: position adjustment (dodge / stack / fill)。 long-form データ
+  --   (= 各 row が (cat, group, value)) を color で群分けし、 position で並べ方を選ぶ。
+  let posCats  = inlineCat (concatMap (replicate 3) (["A","B","C"] :: [Text]))
+      posGrp   = inlineCat (concat (replicate 3 (["x","y","z"] :: [Text])))
+      posVals  = inline ([3.0,5.0,2.0, 4.0,1.0,6.0, 2.0,3.0,4.0] :: [Double])
+  emit "basic/bar-dodge.svg" $
+       purePlot
+    <> layer (bar posCats posVals <> colorBy posGrp <> position PosDodge)
+    <> title "bar (dodge = 横並び)"
+  emit "basic/bar-stack.svg" $
+       purePlot
+    <> layer (bar posCats posVals <> colorBy posGrp <> position PosStack)
+    <> title "bar (stack = 積み上げ)"
+  emit "basic/bar-fill.svg" $
+       purePlot
+    <> layer (bar posCats posVals <> colorBy posGrp <> position PosFill)
+    <> title "bar (fill = 100% 積み上げ)"
+
+  -- Phase 10: coord_flip (= 横棒)。 軸ラベルは水平のまま・bar は横に伸びる。
+  emit "coord/bar-flip.svg" $
+       purePlot
+    <> layer (bar (inlineCat (["A", "B", "C", "D", "E"] :: [Text]))
+                   (inline ([8.0, 5.0, 12.0, 6.5, 3.0] :: [Double])))
+    <> coordFlip
+    <> title "bar + coord_flip (横棒)"
+
+  -- Phase 11 A7-a: coord_cartesian(xlim,ylim) = データを落とさない zoom。
+  --   全 11 点 (x=0..10, y=x^2) のうち x∈[2,6] の窓に絞り、 窓外の点は panel に clip
+  --   される (= データは残るが描画は枠内のみ)。 axisRange と違いデータを切らない。
+  emit "coord/cartesian-zoom.svg" $
+       purePlot
+    <> layer (scatter (inline ([0,1,2,3,4,5,6,7,8,9,10] :: [Double]))
+                      (inline ([0,1,4,9,16,25,36,49,64,81,100] :: [Double])))
+    <> coordCartesian 2 6 0 40
+    <> title "coord_cartesian (zoom x∈[2,6], y∈[0,40])"
+
+  -- Phase 11 A7-c: coord_polar (theta="x")。 x を角度・y を半径に写す (= radar / rose)。
+  --   12 方位の値を閉じた折線で結ぶ (先頭を末尾に追加して 1 周)。
+  emit "coord/polar-line.svg" $
+       purePlot
+    <> layer (line (inline ([0,1,2,3,4,5,6,7,8,9,10,11,12] :: [Double]))
+                   (inline ([6,8,5,9,7,10,6,8,5,9,7,10,6] :: [Double]))
+              <> stroke 2)
+    <> layer (scatter (inline ([0,1,2,3,4,5,6,7,8,9,10,11] :: [Double]))
+                      (inline ([6,8,5,9,7,10,6,8,5,9,7,10] :: [Double])) <> size 4)
+    <> coordPolar
+    <> title "coord_polar (theta=x、 radar)"
+
+  -- Phase 11 A7-c: coord_polar + bar = rose / 円形棒グラフ (扇形 wedge)。
+  emit "coord/polar-bar.svg" $
+       purePlot
+    <> layer (bar (inlineCat (["Mon","Tue","Wed","Thu","Fri","Sat","Sun"] :: [Text]))
+                  (inline ([4,7,5,9,6,11,8] :: [Double])))
+    <> coordPolar
+    <> title "coord_polar + bar (rose / 円形棒)"
+
+  -- Phase 11 A4-a: scale_x_reverse / scale_y_reverse (軸反転 = range 入替)。
+  --   tick/grid/glyph は scaleApply 経由で自動追従 (renderer 無変更)。
+  emit "scale/reverse-x.svg" $
+       purePlot
+    <> layer (scatter (inline ([1.0, 2.0, 3.0, 4.0, 5.0] :: [Double]))
+                       (inline ([1.0, 4.0, 9.0, 16.0, 25.0] :: [Double])))
+    <> reverseX
+    <> title "scale_x_reverse (X 軸反転)"
+  emit "scale/reverse-y.svg" $
+       purePlot
+    <> layer (scatter (inline ([1.0, 2.0, 3.0, 4.0, 5.0] :: [Double]))
+                       (inline ([1.0, 4.0, 9.0, 16.0, 25.0] :: [Double])))
+    <> reverseY
+    <> title "scale_y_reverse (Y 軸反転)"
+
+  -- Phase 11 A4-b: linetype (固定) と linetypeBy (categorical 群分け = 巡回 dash)。
+  --   line 系 mark の dash は LineStyle.lsDash 経由 (SVG stroke-dasharray / Canvas setLineDash)。
+  emit "scale/linetype.svg" $
+       purePlot
+    <> layer (line (inline ([1.0, 2.0, 3.0, 4.0, 5.0] :: [Double]))
+                   (inline ([1.0, 3.0, 2.0, 5.0, 4.0] :: [Double]))
+              <> stroke 2 <> linetype LtDashed)
+    <> title "linetype = dashed (固定)"
+  let ltX = inline (concat (replicate 2 ([1.0, 2.0, 3.0, 4.0, 5.0] :: [Double])))
+      ltY = inline ([1.0, 3.0, 2.0, 5.0, 4.0, 2.0, 1.0, 4.0, 3.0, 6.0] :: [Double])
+      ltG = inlineCat (concatMap (replicate 5) (["A", "B"] :: [Text]))
+  emit "scale/linetype-by.svg" $
+       purePlot
+    <> layer (line ltX ltY <> stroke 2 <> linetypeBy ltG)
+    <> title "linetype = factor(g) (A=solid, B=dashed)"
+
+  -- Phase 11 A4-d: 明示 breaks / labels (= ggplot scale_*_continuous(breaks=,labels=))。
+  --   X は break+label 対 (0→low, 50→mid, 100→high)、 Y は break のみ (自動 format)。
+  emit "scale/breaks-labels.svg" $
+       purePlot
+    <> layer (scatter (inline ([0.0, 25.0, 50.0, 75.0, 100.0] :: [Double]))
+                      (inline ([10.0, 40.0, 55.0, 70.0, 90.0] :: [Double])))
+    <> xAxis (axisBreaksLabeled [(0, "low"), (50, "mid"), (100, "high")])
+    <> yAxis (axisBreaksAt [0, 25, 50, 75, 100])
+    <> title "明示 breaks/labels (X=ラベル, Y=値刻み)"
+
+  -- Phase 11 A4-e: 色/サイズ scale 拡充 (manual 辞書 / 発散 gradient2 / size range)。
+  let aeX = inline (concat (replicate 2 ([1.0, 2.0, 3.0, 4.0] :: [Double])))
+      aeY = inline ([2.0, 3.0, 1.0, 4.0, 3.0, 1.0, 4.0, 2.0] :: [Double])
+      aeG = inlineCat (concatMap (replicate 4) (["alpha", "beta"] :: [Text]))
+  emit "scale/color-manual.svg" $
+       purePlot
+    <> layer (scatter aeX aeY <> colorBy aeG <> size 6)
+    <> scaleColorManual [("alpha", "#1B9E77"), ("beta", "#D95F02")]
+    <> legend
+    <> title "scale_color_manual (alpha→緑, beta→橙)"
+  -- gradient2: z = -3..3、 midpoint 0 を白に (発散)。
+  let gz = inline ([-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0] :: [Double])
+      gx = inline ([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] :: [Double])
+  emit "scale/color-gradient2.svg" $
+       purePlot
+    <> layer (scatter gx gx <> colorContinuousBy gz <> size 8)
+    <> scaleColorGradient2 "#2166AC" "#F7F7F7" "#B2182B" 0.0
+    <> legend
+    <> title "scale_color_gradient2 (midpoint 0 = 白)"
+  -- size: 値に応じた半径 (range 4..20)。
+  let sx = inline ([1.0, 2.0, 3.0, 4.0, 5.0] :: [Double])
+      ssz = inline ([1.0, 5.0, 10.0, 20.0, 40.0] :: [Double])
+  emit "scale/size.svg" $
+       purePlot
+    <> layer (scatter sx sx <> sizeBy ssz)
+    <> scaleSize 4 20
+    <> title "scale_size (range 4..20)"
+  -- Phase 30 A8: alphaBy = 連続 alpha encoding (= ggplot scale_alpha)。
+  -- 列値 min..max を alpha [0.1, 1.0] に map (薄→濃)。size 固定で alpha のみ変化。
+  emit "scale/alpha.svg" $
+       purePlot
+    <> layer (scatter sx sx <> alphaBy ssz <> size 12)
+    <> title "scale_alpha (alphaBy・薄→濃)"
+
+  -- Phase 11 A6: geom_text / geom_label (データ駆動ラベル)。
+  let gtX = inline ([1.0, 2.0, 3.0, 4.0, 5.0] :: [Double])
+      gtY = inline ([2.0, 4.0, 3.0, 5.0, 4.5] :: [Double])
+      gtL = inlineCat (["alpha", "beta", "gamma", "delta", "epsilon"] :: [Text])
+  emit "basic/geom-text.svg" $
+       purePlot
+    <> layer (scatter gtX gtY <> size 4)
+    <> layer (text gtX gtY gtL <> size 12)
+    <> title "geom_text (各点にラベル)"
+    <> xLabel "x" <> yLabel "y"
+  emit "basic/geom-label.svg" $
+       purePlot
+    <> layer (label gtX gtY gtL <> size 12)
+    <> title "geom_label (背景付きラベル)"
+    <> xLabel "x" <> yLabel "y"
+
+  -- Phase 11 A6-2: Q-Q plot (= ggplot geom_qq)。 正規サンプル N=120 を
+  -- 理論正規分位点に対してプロット (= 正規性の視覚診断、 直線なら正規)。
+  let qqVals = gaussian 7 0.0 1.0 120 :: [Double]
+  emit "basic/qq.svg" $
+       purePlot
+    <> layer (qq (inline qqVals) <> size 5)
+    <> title "Q-Q plot (正規分位点 vs サンプル)"
+    <> xLabel "理論分位点" <> yLabel "標本分位点"
+
+  -- Phase 11 A6-3: heatmap (= ggplot geom_tile)。 x/y カテゴリ grid を value の
+  -- 連続色 (Viridis) で塗る。 long-form (各 row = (x, y, value))。
+  let hmX = inlineCat (concatMap (replicate 3) (["A", "B", "C"] :: [Text]))
+      hmY = inlineCat (concat (replicate 3 (["P", "Q", "R"] :: [Text])))
+      hmV = inline ([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] :: [Double])
+  emit "basic/heatmap.svg" $
+       purePlot
+    <> layer (heatmap hmX hmY hmV)
+    <> title "heatmap (3×3 grid、 Viridis)"
+    <> xLabel "x" <> yLabel "y"
+
+  -- contour (= 等高線図、 marching squares)。 連続 x/y/z を正則格子に再標本化し
+  -- z を等分した各レベルの等値線を描く。 z = exp(-((x-3)²+(y-3)²)/4) 的な釣鐘 (中心が高い)。
+  let ctGrid = [ (xi, yi) | xi <- [0.0, 0.5 .. 6.0], yi <- [0.0, 0.5 .. 6.0] ]
+      ctX = inline (map fst ctGrid)
+      ctY = inline (map snd ctGrid)
+      ctZ = inline [ exp (-(((x - 3) ** 2) + ((y - 3) ** 2)) / 4) | (x, y) <- ctGrid ]
+  emit "basic/contour.svg" $
+       purePlot
+    <> layer (contour ctX ctY ctZ)
+    <> title "contour (等高線、 中心が高い釣鐘)"
+    <> xLabel "x" <> yLabel "y"
+
+  -- bin2d (= ggplot geom_bin2d、 binned heatmap)。 contour と同データを塗りで。
+  emit "basic/bin2d.svg" $
+       purePlot
+    <> layer (bin2d ctX ctY ctZ)
+    <> title "bin2d (binned heatmap、 中心が高い釣鐘)"
+    <> xLabel "x" <> yLabel "y"
+
+  -- Phase 11 A6-4: ECDF (= ggplot stat_ecdf)。 正規サンプルの経験累積分布 (階段)。
+  let ecdfVals = gaussian 13 0.0 1.0 80 :: [Double]
+  emit "basic/ecdf.svg" $
+       purePlot
+    <> layer (ecdf (inline ecdfVals))
+    <> title "ECDF (経験累積分布、 N=80)"
+    <> xLabel "x" <> yLabel "F(x)"
+
+  -- Phase 11 A6-4b: 区間 geom (linerange / pointrange / crossbar)。 x/y/err。
+  let rbX = inline ([1.0, 2.0, 3.0, 4.0, 5.0] :: [Double])
+      rbY = inline ([3.0, 4.5, 4.0, 5.5, 5.0] :: [Double])
+      rbE = inline ([0.5, 0.8, 0.4, 0.6, 0.7] :: [Double])
+  emit "basic/linerange.svg" $
+       purePlot
+    <> layer (lineRange rbX rbY rbE)
+    <> title "linerange (y±err の縦線)" <> xLabel "x" <> yLabel "y"
+  emit "basic/pointrange.svg" $
+       purePlot
+    <> layer (pointRange rbX rbY rbE)
+    <> title "pointrange (縦線 + 中心点)" <> xLabel "x" <> yLabel "y"
+  emit "basic/crossbar.svg" $
+       purePlot
+    <> layer (crossbar rbX rbY rbE)
+    <> title "crossbar (幅付き箱 + 中央線)" <> xLabel "x" <> yLabel "y"
+
+  -- Phase 11 A6-4c: stat_function (= ggplot stat_function)。 関数を構成時にサンプルして
+  -- inline line に焼き込む。 ここでは sin を [0, 2π] で 100 点。
+  emit "basic/stat-function.svg" $
+       purePlot
+    <> layer (statFunction sin 0.0 (2 * pi) 100 <> color (fromHex "#2563eb") <> stroke 2)
+    <> title "stat_function (sin、 [0,2π] を 100 点)"
+    <> xLabel "x" <> yLabel "sin x"
+
+  -- histogram
+  -- Phase 8 B8: N=200 正規分布、 bin 数は既定 (= ggplot と同じ 30)
+  let histVals = gaussian 41 5.0 1.5 200 :: [Double]
+  emit "basic/histogram.svg" $
+       purePlot
+    <> layer (histogram (inline histVals) <> alpha 0.8)
+    <> title "histogram (N=200、 既定 30 bins)"
+
+  -- box
+  -- 単一群 box
+  emit "basic/box.svg" $
+       purePlot
+    <> layer (boxplot (inline vals))
+    <> title "box plot (単一群、 Q1/median/Q3 + whisker)"
+
+  -- density
+  let denseVals = take 200 $ cycle
+        [3.0, 4.0, 4.5, 5.0, 5.0, 5.2, 5.5, 6.0, 6.5, 7.0, 8.0]
+  emit "basic/density.svg" $
+       purePlot
+    <> layer (density (inline denseVals) <> color (fromHex "#16a34a"))
+    <> title "density (KDE、 Silverman bw)"
+
+  -- pie
+  emit "basic/pie.svg" $
+       purePlot
+    <> layer (pie (inlineCat (["Eng", "Bio", "Math", "CS"] :: [Text]))
+                   (inline ([35.0, 22.0, 18.0, 25.0] :: [Double])))
+    <> title "pie (proportion)"
+
+  -- waterfall
+  emit "basic/waterfall.svg" $
+       purePlot
+    <> layer (waterfall (inlineCat (["Start", "+A", "+B", "-C", "End"] :: [Text]))
+                         (inline ([100.0, 30.0, 20.0, -15.0, 0.0] :: [Double])))
+    <> title "waterfall"
+
+  -- step
+  emit "basic/step.svg" $
+       purePlot
+    <> layer (step (inline xs) (inline ys) <> color (fromHex "#dc2626"))
+    <> title "step plot"
+
+  -- histogram wide-form (Phase 6 A10、 P1 解消) — 3 列を半透明で重ね
+  -- Phase 8 B7: 各列 N=70 の正規分布 (平均をずらした 3 群)、 bin 境界は全列共通
+  let h1 = gaussian 71 5.0 1.2 70 :: [Double]
+      h2 = gaussian 83 6.5 1.0 70
+      h3 = gaussian 97 4.0 1.4 70
+  emit "basic/histogram-wide.svg" $
+       histogramWide [inline h1, inline h2, inline h3]
+    <> title "histogramWide [c1, c2, c3] (= 全列共通 bin、 N=70 ×3)"
+
+  -- stem
+  let stemX = [1, 2, 3, 4, 5, 6, 7, 8] :: [Double]
+      stemY = [2.0, 4.5, 1.0, 5.5, 3.0, 6.0, 2.5, 4.0]
+  emit "basic/stem.svg" $
+       purePlot
+    <> layer (stem (inline stemX) (inline stemY) <> color (fromHex "#7c3aed"))
+    <> title "stem / lollipop"
+
+  -- ===========================================================================
+  -- DISTRIBUTION
+  -- ===========================================================================
+
+  let groups   = inlineCat (["A", "A", "A", "B", "B", "B", "C", "C", "C"] :: [Text])
+      gvalues  = inline ([1.0, 2.5, 3.0, 4.0, 4.5, 5.5, 6.0, 7.5, 8.0] :: [Double])
+      -- Phase 8 B2: 分布系の共用群データ (4 群 × N=90、 正規分布)
+      (grpLabels, grpVals) = groupedDemo
+      grpCat = inlineCat grpLabels
+      grpNum = inline grpVals
+
+  -- Phase 8 B3: violin も共用群データ (4 群 × N=90) で
+  emit "distribution/violin.svg" $
+       purePlot
+    <> layer (violin grpNum <> groupBy grpCat)
+    <> title "violin (4 群 × N=90)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  -- Phase 8 B4: strip も共用群データ (4 群 × N=90)、 横 jitter で散らす
+  emit "distribution/strip.svg" $
+       purePlot
+    <> layer (strip grpNum <> groupBy grpCat <> alpha 0.6 <> size 4)
+    <> title "strip plot (4 群 × N=90、 jitter)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  -- Phase 8 B5: swarm も共用群データ (4 群 × N=90)、 beeswarm 衝突回避
+  emit "distribution/swarm.svg" $
+       purePlot
+    <> layer (swarm grpNum <> groupBy grpCat <> alpha 0.85 <> size 5)
+    <> title "swarm (4 群 × N=90、 beeswarm)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  -- Phase 8 B2: raincloud は群ごと N=90 の正規分布データで (= 群内分布を見せる図)
+  emit "distribution/raincloud.svg" $
+       purePlot
+    <> layer (raincloud grpNum <> groupBy grpCat)
+    <> title "raincloud (4 群 × N=90、 jitter + box + half-violin)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  -- Phase 8 B6: ridge は (値, 群) の順。 群ごとに分布形が違うデータ (= joyplot は
+  -- 群間の分布形状比較が主眼) で 4 群 × N=90。 引数順は ridge valCol groupCol。
+  emit "distribution/ridge.svg" $
+       purePlot
+    <> layer (ridge grpNum <> groupBy grpCat)
+    <> title "ridge / joyplot (4 群 × N=90)"
+    <> xLabel "weight gain (g/d)"
+
+  -- Phase 8 B9: 複数群 box (= boxplot valsCol <> groupBy groupCol)、 共用群データ 4 群 × N=90
+  emit "distribution/box-grouped.svg" $
+       purePlot
+    <> layer (boxplot grpNum <> groupBy grpCat)
+    <> title "box plot (4 群、 群別 Tukey box)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  -- Phase 10: box + coord_flip (= 横向き box)。 群が縦に並び value が横に伸びる。
+  emit "coord/box-flip.svg" $
+       purePlot
+    <> layer (boxplot grpNum <> groupBy grpCat)
+    <> coordFlip
+    <> title "box plot + coord_flip (横向き)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  -- Phase 10 A5 ②: distribution 族の flip (violin/strip/swarm)。 自前軸 (cat ラベル/値 tick) が
+  -- flip 配置に追従するか確認 (box-flip と同パターン)。
+  emit "coord/violin-flip.svg" $
+       purePlot
+    <> layer (violin grpNum <> groupBy grpCat)
+    <> coordFlip
+    <> title "violin + coord_flip (横向き)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  emit "coord/strip-flip.svg" $
+       purePlot
+    <> layer (strip grpNum <> groupBy grpCat <> alpha 0.6 <> size 4)
+    <> coordFlip
+    <> title "strip + coord_flip (横向き)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  emit "coord/swarm-flip.svg" $
+       purePlot
+    <> layer (swarm grpNum <> groupBy grpCat <> alpha 0.85 <> size 5)
+    <> coordFlip
+    <> title "swarm + coord_flip (横向き)"
+    <> xLabel "genotype" <> yLabel "weight gain (g/d)"
+
+  -- Phase 10 A5 ②: waterfall の flip (standalone・自前 baseline/y tick/cat ラベル)。
+  emit "coord/waterfall-flip.svg" $
+       purePlot
+    <> layer (waterfall (inlineCat (["Start", "+A", "+B", "-C", "End"] :: [Text]))
+                         (inline ([100.0, 30.0, 20.0, -15.0, 0.0] :: [Double])))
+    <> coordFlip
+    <> title "waterfall + coord_flip (横向き)"
+
+  -- ===========================================================================
+  -- STATISTICAL
+  -- ===========================================================================
+
+  -- regression line + CI: plot 側では fit しないので OLS 当てはめ値 yhat を作り
+  -- band(信頼帯) + line で重ねる (自動 fit は analyze の statLm)。
+  let rxs = [0, 1 .. 9] :: [Double]
+      rys = map (\x -> 2 * x + 1 + sin x) rxs
+      rN   = fromIntegral (length rxs)
+      rA   = (rN * sum (zipWith (*) rxs rys) - sum rxs * sum rys)
+               / (rN * sum (map (^ (2 :: Int)) rxs) - sum rxs ^ (2 :: Int))
+      rB   = (sum rys - rA * sum rxs) / rN
+      yhat = [ rA * x + rB | x <- rxs ]
+      rlo  = map (subtract 0.6) yhat
+      rhi  = map (+ 0.6) yhat
+  emit "statistical/regression-ci.svg" $
+       purePlot
+    <> layer (scatter (inline rxs) (inline rys) <> alpha 0.85 <> size 5)
+    <> layer (band (inline rxs) (inline rlo) (inline rhi) <> color (fromHex "#dc2626") <> alpha 0.2)
+    <> layer (line (inline rxs) (inline yhat) <> color (fromHex "#dc2626"))
+    <> title "regression line + CI band"
+    <> xLabel "x" <> yLabel "y"
+
+  -- stat line (= 値系列に対する平均/中央値の水平基準線。 ggplot geom_hline 相当、
+  -- DoE の grand-mean 線と同じ用法)。 旧版は histogram (count 軸) の上に値の水平線を
+  -- 重ねており、 y 軸が count と値の混在で意味的に破綻 (= findings「y-range 微妙」) して
+  -- いた。 statMean/statMedian は y=値 の水平線なので、 値そのものを y に取る系列
+  -- (scatter + connect 折れ線) に重ねるのが正しい (Phase 8 B17)。
+  let statIdx = map fromIntegral [1 .. length vals] :: [Double]
+  emit "statistical/stat-line.svg" $
+       purePlot
+    <> layer (scatter (inline statIdx) (inline vals) <> connect <> alpha 0.85 <> size 5)
+    <> layer (statMean (inline vals)   <> color (fromHex "#dc2626") <> stroke 2)
+    <> layer (statMedian (inline vals) <> color (fromHex "#2563eb") <> stroke 2)
+    <> title "series + statMean (red) + statMedian (blue)"
+    <> xLabel "index" <> yLabel "value"
+
+  -- Forest plot (Phase 6 A2)
+  let studies :: [Text]
+      studies = ["Study 1", "Study 2", "Study 3", "Study 4", "Study 5", "Pooled"]
+      ests :: [Double]
+      ests    = [0.3, -0.2, 0.5, 0.1, 0.4, 0.22]
+      errs :: [Double]
+      errs    = [0.2, 0.15, 0.18, 0.25, 0.13, 0.08]
+  emit "statistical/forest.svg" $
+       purePlot
+    <> layer (forest (inlineCat studies) (inline ests) (inline errs)
+               <> color (fromHex "#377EB8") <> size 7)
+    <> title "Forest plot (= meta-analysis、 中央 null line + horizontal CI)"
+    <> xLabel "effect size" <> yLabel "study"
+
+  -- Phase 10 A5 ②: forest の flip (standalone・自前 x tick / 群ラベル左 の flip 辺入替確認)。
+  emit "coord/forest-flip.svg" $
+       purePlot
+    <> layer (forest (inlineCat studies) (inline ests) (inline errs)
+               <> color (fromHex "#377EB8") <> size 7)
+    <> coordFlip
+    <> title "Forest plot + coord_flip"
+    <> xLabel "effect size" <> yLabel "study"
+
+  -- Streamgraph (Phase 52.D2): 中心化積層 area。 color aes で 3 系列に分割し、
+  -- 各 x 点で系列を積層、 baseline を -(Σy)/2 から (silhouette 中心化) で描く。
+  let streamT :: [Double]
+      streamT = concat (replicate 3 [0, 1, 2, 3, 4, 5])
+      streamV :: [Double]
+      streamV = [ 1, 2, 4, 3, 2, 1     -- 系列 A
+                , 2, 3, 3, 4, 5, 4     -- 系列 B
+                , 1, 1, 2, 2, 3, 5 ]   -- 系列 C
+      streamG :: [Text]
+      streamG = concat [ replicate 6 "A", replicate 6 "B", replicate 6 "C" ]
+  emit "statistical/streamgraph.svg" $
+       purePlot
+    <> layer (stream (inline streamT) (inline streamV)
+               <> colorBy (inlineCat streamG) <> alpha 0.85)
+    <> title "Streamgraph (= 中心化積層 area、 ThemeRiver 風、 color で系列分割)"
+    <> xLabel "time" <> yLabel "value (centered)"
+
+  -- Funnel plot (Phase 6 A3)
+  let funnelEff :: [Double]
+      funnelEff = [0.3, 0.25, 0.4, 0.1, -0.2, 0.5, 0.35, 0.15, 0.45, 0.05, 0.6, -0.1]
+      funnelSE :: [Double]
+      funnelSE  = [0.1, 0.15, 0.2, 0.18, 0.22, 0.25, 0.12, 0.28, 0.16, 0.3, 0.08, 0.35]
+  emit "statistical/funnel.svg" $
+       purePlot
+    <> layer (funnel (inline funnelEff) (inline funnelSE)
+               <> color (fromHex "#377EB8") <> size 6)
+    <> title "Funnel plot (= publication bias、 ±1.96 SE envelope)"
+    <> xLabel "effect size" <> yLabel "SE"
+
+  -- autocorrelation (Phase 6 A4、 P19 解消)
+  -- AR(1) process: x_{t+1} = 0.7 * x_t + ε_t
+  let ar1Vals :: [Double]
+      ar1Vals = take 500 $ iterate (\v -> 0.7 * v + 0.3 * sin (v * 13.0)) 0.5
+  emit "statistical/autocorr.svg" $
+       purePlot
+    <> layer (autocorr (inline ar1Vals) <> autocorrMaxLag 30
+               <> color (fromHex "#377EB8"))
+    <> title "autocorr (= MCMC AR(1)、 max lag 30、 ±1.96/√N band)"
+    <> xLabel "lag" <> yLabel "r(τ)"
+
+  -- ESS (Phase 8 B13): パラメータごとの計算済み ESS 値を棒に (= ggplot/bayesplot 流、
+  -- 計算は統計ライブラリの責務、 plot は値を描くだけ)。 閾値 100/400 で色分け。
+  let essParams :: [Text]
+      essParams = ["alpha", "beta", "sigma", "mu", "tau", "theta"]
+      essValues :: [Double]
+      essValues = [820.0, 640.0, 95.0, 410.0, 280.0, 55.0]  -- sigma/theta が低い (= 要注意)
+  emit "statistical/ess.svg" $
+       purePlot
+    <> layer (ess (inlineCat essParams) (inline essValues))
+    <> title "ESS (= parameter ごとの有効サンプルサイズ、 閾値 100/400)"
+    <> xLabel "parameter" <> yLabel "ESS"
+    <> title "ESS (= 4 chain、 Geyer initial positive sequence)"
+    <> xLabel "chain" <> yLabel "ESS"
+
+  -- MCMC trace
+  let mcmcSteps = [0, 1 .. 199] :: [Double]
+      mcmcVals = map (\i -> sin (i * 0.05) + i * 0.001) mcmcSteps
+  emit "statistical/trace.svg" $
+       purePlot
+    <> layer (trace (inline mcmcSteps) (inline mcmcVals) <> stroke 1.5)
+    <> title "MCMC trace plot"
+    <> xLabel "step" <> yLabel "θ"
+
+  -- pairs plot (Phase 8 B16): N=50、 列間に相関 (height=正、 weight=負) + 名前付き列
+  let p1 = gaussian 17 5.0 1.5 80 :: [Double]   -- N=80 で単峰が安定
+      p2 = zipWith (\a e -> 0.8 * a + e) p1 (gaussian 29 1.0 0.6 80)  -- p1 と正相関
+      p3 = zipWith (\a e -> negate 0.6 * a + e) p1 (gaussian 43 8.0 0.8 80)  -- p1 と負相関
+      p4 = gaussian 53 5.0 1.5 80    -- 独立
+      p5 = zipWith (\a e -> 0.5 * a + e) p2 (gaussian 59 2.0 0.7 80)
+      pairsResolver k = case k of
+        "age"    -> Just (NumData (V.fromList p1))
+        "height" -> Just (NumData (V.fromList p2))
+        "weight" -> Just (NumData (V.fromList p3))
+        "bmi"    -> Just (NumData (V.fromList p4))
+        "score"  -> Just (NumData (V.fromList p5))
+        _        -> Nothing
+  emitR "statistical/pairs.svg" pairsResolver
+    (purePlot
+       <> pairs ["age", "height", "weight", "bmi", "score"]
+       <> title "pairs plot (5 変数 × N=80、 相関あり)")
+
+  -- ===========================================================================
+  -- DECORATION
+  -- ===========================================================================
+
+  -- facet (= Resolver 経由で column 解決が必要、 inline では不可なので別 demo)
+  let facetResolver facetN = case facetN of
+        "x" -> Just (NumData (V.fromList [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]))
+        "y" -> Just (NumData (V.fromList [1, 4, 9, 16, 2, 5, 8, 12, 3, 6, 9, 15]))
+        "g" -> Just (TxtData (V.fromList ["A","A","A","A","B","B","B","B","C","C","C","C"]))
+        _   -> Nothing
+  emitR "decoration/facet.svg" facetResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "g" <> size 6)
+    <> facet "g"
+    <> title "facet (= Trellis、 3 panel)"
+
+  -- Phase 10 A5: facet + coord_flip。 各 panel が vsCoord を継承し、 flip scale も
+  -- panelArea に retarget されて panel が重ならないことを確認 (罠5 = flip scale 伝播)。
+  emitR "coord/facet-flip.svg" facetResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "g" <> size 6)
+    <> facet "g"
+    <> coordFlip
+    <> title "facet + coord_flip (3 panel・各 panel flip)"
+
+  -- Phase 8 C G7: facet_wrap 複数行 (ncol=3 → 5 群を 3 列 2 行に折り返し)。
+  -- 最下行のみ x 軸・左端列のみ y 軸 (ggplot facet_wrap の内側軸 drop)。
+  let facetWrapResolver facetN = case facetN of
+        "x" -> Just (NumData (V.fromList (concat (replicate 5 [1,2,3,4]))))
+        "y" -> Just (NumData (V.fromList
+                 [1,4,9,16, 2,5,8,12, 3,6,9,15, 2,3,7,10, 4,8,11,14]))
+        "g" -> Just (TxtData (V.fromList (concatMap (replicate 4)
+                 ["A","B","C","D","E"])))
+        _   -> Nothing
+  emitR "decoration/facet-wrap.svg" facetWrapResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "g" <> size 6)
+    <> facetWrap "g" 3
+    <> title "facet_wrap (ncol=3、 5 panel → 2 行)"
+
+  -- Phase 11 A7-b: facet free scales (= ggplot facet_wrap(scales="free"))。
+  --   群ごとに y のスケールが大きく違う (A: 0..16, B: 0..1200, C: 0..0.3)。 free だと
+  --   各 panel が自分のデータ範囲で軸を持ち、 全 panel に x/y 軸が出る。
+  let facetFreeResolver facetN = case facetN of
+        "x" -> Just (NumData (V.fromList [1,2,3,4, 1,2,3,4, 1,2,3,4]))
+        "y" -> Just (NumData (V.fromList
+                 [1,4,9,16,  300,600,900,1200,  0.05,0.1,0.2,0.3]))
+        "g" -> Just (TxtData (V.fromList (concatMap (replicate 4) ["A","B","C"])))
+        _   -> Nothing
+  emitR "decoration/facet-free.svg" facetFreeResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "g" <> size 6)
+    <> facet "g"
+    <> facetScales FacetFree
+    <> title "facet free scales (各 panel 独立 y 軸)"
+
+  -- Phase 8 C G7 part-b: facet_grid(row ~ col) 2 変数 cross (2 行 × 3 列)。
+  -- 上 strip = col 名 (各列頭)、 右 strip = row 名 (各行端・縦書き)、 軸は最下行 x・左端列 y。
+  let fgRows = ["top","bot"]
+      fgCols = ["L","M","R"]
+      fgX = concat (replicate 6 [1,2,3,4])
+      fgY = [1,2,3,4, 1,3,5,7, 1,4,7,10,  2,2,2,2, 4,3,2,1, 1,5,3,8]
+      fgR = concatMap (replicate 12) fgRows
+      fgC = concat (replicate 2 (concatMap (replicate 4) fgCols))
+      facetGridResolver fn = case fn of
+        "x" -> Just (NumData (V.fromList fgX))
+        "y" -> Just (NumData (V.fromList fgY))
+        "r" -> Just (TxtData (V.fromList fgR))
+        "c" -> Just (TxtData (V.fromList fgC))
+        _   -> Nothing
+  emitR "decoration/facet-grid.svg" facetGridResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> size 6)
+    <> facetGrid "r" "c"
+    <> title "facet_grid (r ~ c、 2 行 × 3 列)"
+
+  -- Phase 11 A7-b: facet_grid free scales + space="free"。 列ごとに x 範囲が違い
+  --   (L:0..3, M:0..6, R:0..12 = 1:2:4)、 行ごとに y 範囲が違う (top:0..4, bot:0..8 = 1:2)。
+  --   free_x = 列共有 x domain、 free_y = 行共有 y domain。 space free で列幅/行高を
+  --   data 範囲に比例 (= 各 panel の単位長が揃う)。 A6: 旧データは 1:10:100 / 1:100 と
+  --   範囲差が極端で L列・top行が比例で極小化し潰れたため、 穏当な比に緩和 (計算ロジックは不変)。
+  let fgfX = [ 0,1,2,3,  0,2,4,6,  0,4,8,12              -- top 行: L/M/R (範囲 3:6:12 = 1:2:4)
+             , 0,1,2,3,  0,2,4,6,  0,4,8,12 ]            -- bot 行
+      fgfY = [ 1,2,3,4,  1,2,3,4,  1,2,3,4               -- top 行: y 範囲 3
+             , 2,4,6,8,  2,4,6,8,  2,4,6,8 ]             -- bot 行: y 範囲 6 (top:bot = 1:2)
+      facetGridFreeResolver fn = case fn of
+        "x" -> Just (NumData (V.fromList fgfX))
+        "y" -> Just (NumData (V.fromList fgfY))
+        "r" -> Just (TxtData (V.fromList (concatMap (replicate 12) ["top","bot"])))
+        "c" -> Just (TxtData (V.fromList (concat (replicate 2 (concatMap (replicate 4) ["L","M","R"])))))
+        _   -> Nothing
+  emitR "decoration/facet-grid-free.svg" facetGridFreeResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> size 6)
+    <> facetGrid "r" "c"
+    <> facetScales FacetFree
+    <> facetSpace SpaceFree
+    <> title "facet_grid free + space (列x/行y 独立・幅比例)"
+
+  -- facet strip.background (灰矩形) の theme 連動デモ。 Parchment (羊皮紙 strip) で
+  -- strip 帯が panel/grid と調和して描かれることを確認。 facet_grid で 上 strip + 右 strip 両方。
+  emitR "decoration/facet-strip-themed.svg" facetGridResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> size 6)
+    <> facetGrid "r" "c"
+    <> theme ThemeParchment
+    <> title "facet strip + theme (parchment 羊皮紙 strip)"
+
+  -- subplots
+  let sp1 = purePlot
+              <> layer (scatter (inline [1.0,2,3,4,5 :: Double]) (inline [1.0,4,9,16,25]))
+              <> title "x²"
+      sp2 = purePlot
+              <> layer (line    (inline [1.0,2,3,4,5 :: Double]) (inline [2.0,4,8,16,32]))
+              <> title "2^x"
+      sp3 = purePlot
+              <> layer (bar     (inlineCat (["a","b","c","d"] :: [Text]))
+                                 (inline [3.0,7,5,9]))
+              <> title "bar"
+  emit "decoration/subplots.svg" $
+       purePlot
+    <> subplots [sp1, sp2, sp3]
+    <> subplotCols 3
+    <> title "subplots (任意 spec 並列)"
+    <> widthUnit (900 *~ px) <> heightUnit (350 *~ px)
+
+  -- Phase 52.D concat 合成 (Vega-Lite hconcat/vconcat 相当・patchwork 風演算子)。
+  -- (a <-> b <-> c) <:> d = 1 行目 3 列 + 2 行目を全幅 (1 行目セルの 3 倍幅)。
+  let sp4 = purePlot
+              <> layer (scatter (inline [0.0,1,2,3,4 :: Double]) (inline [0.0,3,1,4,2]))
+              <> title "full-width row"
+  emit "decoration/concat.svg" $
+       ((sp1 <-> sp2 <-> sp3) <:> sp4)
+    <> title "(a <-> b <-> c) <:> d  (横3列 + 全幅)"
+    <> widthUnit (900 *~ px) <> heightUnit (500 *~ px)
+
+  -- annotation (Phase 8 B20: 機能を並べるだけでなく「外れ値を指す」実用的な配置に)。
+  -- データはほぼ y=8x の直線だが x=3 だけ外れ値 (70、 本来 24)。 トレンド線 (Line)、
+  -- 外れ値を囲む focus 枠 (Rect)、 ラベル (Text) と そこから点への矢印 (Arrow) を連携させる。
+  let axs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double]
+      ays = [0, 8, 16, 70, 32, 40, 48, 56, 64, 72] :: [Double]
+  emit "decoration/annotation.svg" $
+       purePlot
+    <> layer (scatter (inline axs) (inline ays) <> size 5)
+    <> annotLine  0.0 0.0 9.0 72.0                -- トレンド線 (y=8x)
+    <> annotRect  2.5 62.0 1.0 16.0 "#f59e0b"     -- 外れ値 (3,70) を囲む focus 枠 (amber)
+    <> annotText  4.7 74.0 "outlier?"             -- ラベル (点の右上)
+    <> annotArrow 4.0 74.0 3.18 70.5              -- ラベル → 外れ値点 への矢印
+    <> title "annotation (text / arrow / line / rect)"
+    <> xLabel "x" <> yLabel "y"
+
+  -- Phase 11 A5-a: labs (title / subtitle / caption / tag)。
+  emit "decoration/labs.svg" $
+       purePlot
+    <> layer (scatter (inline axs) (inline ays) <> size 5)
+    <> labs emptyLabs { labsTitle    = Just "Fuel efficiency vs displacement"
+                      , labsSubtitle = Just "各点 = 1 車種 (sample data)"
+                      , labsCaption  = Just "Source: hgg gallery demo"
+                      , labsTag      = Just "A"
+                      , labsX        = Just "displacement"
+                      , labsY        = Just "mpg" }
+
+  -- inset axes
+  emit "decoration/inset.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline ys) <> size 5)
+    <> title  "scatter + inset (zoom)"
+    <> insetAt 0.6 0.05 0.35 0.35
+         (purePlot
+            <> layer (scatter (inline (take 4 xs)) (inline (take 4 ys)) <> size 7))
+
+  -- marginal (= 軸外に x/y の周辺 histogram)。
+  -- Phase 8 B18: 旧 demo は rxs=[0..9] (N=10) で各 bin がほぼ均一 → 周辺分布が見えなかった。
+  -- gaussian で相関のある 2 変数 (N=200) にし、 x/y それぞれ正規分布の山が周辺 hist に
+  -- 出るようにする (= seaborn jointplot 風)。
+  let mgx = gaussian 211 5.0 1.5 200 :: [Double]
+      mgy = zipWith (\a e -> a + 1.0 + e) mgx (gaussian 223 0.0 1.0 200)  -- mgx と正相関
+  emit "decoration/marginal.svg" $
+       purePlot
+    <> layer (scatter (inline mgx) (inline mgy) <> size 4 <> alpha 0.5)
+    <> marginal
+    <> title "scatter + marginal histograms"
+    <> xLabel "x" <> yLabel "y"
+    <> widthUnit (700 *~ px) <> heightUnit (700 *~ px)
+
+  -- dual Y
+  let dyL = map (* 1) ys
+      dyR = map (\v -> v / 10) ys
+  emit "decoration/dual-y.svg" $
+       purePlot
+    <> layer (line (inline xs) (inline dyL) <> color (fromHex "#2563eb") <> stroke 2)
+    <> layer (line (inline xs) (inline dyR)
+               <> color (fromHex "#dc2626") <> stroke 2 <> toRightY)
+    <> yAxisRight (mempty <> axisFormat (AxisDecimalFmt 2))
+    <> title "dual Y axis"
+
+  -- legend chip: Resolver 経由で color encoding (= 凡例に表示する category)
+  let legendResolver k = case k of
+        "x" -> Just (NumData (V.fromList ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+                                            0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double])))
+        "y" -> Just (NumData (V.fromList ([0, 1, 4, 9, 16, 25, 36, 49, 64, 81,
+                                            0, 0.7, 2.8, 6.3, 11.2, 17.5, 25.2, 34.3, 44.8, 56.7] :: [Double])))
+        "group" -> Just (TxtData (V.fromList (replicate 10 "Series A" ++ replicate 10 "Series B")))
+        _   -> Nothing
+  emitR "decoration/legend.svg" legendResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+    <> legend
+    <> legendPos LegendInsideTopLeft
+    <> title "legend chip (top-left)"
+
+  -- Phase 9 A-5: legend 配置を ggplot に揃える (panel を縮め図内に収める)。 Right (既定) は
+  -- panel 右の予約域、 Bottom は panel 下の予約域、 continuous は gradient bar。 HS/PS 同一。
+  emitR "decoration/legend-bottom.svg" legendResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+    <> legendPos LegendBottom
+    <> title "legend (bottom)"
+  emitR "decoration/legend-continuous.svg" legendResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorContinuousBy "y" <> size 5)
+    <> legend
+    <> title "legend (continuous gradient)"
+
+  -- Phase 11 A4-c: 凡例タイトル (= scale name / labs(color=))。 Right/Bottom 両方に出る。
+  emitR "decoration/legend-title.svg" legendResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+    <> legendTitle "Series"
+    <> title "legend title (= scale name)"
+  emitR "decoration/legend-title-bottom.svg" legendResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+    <> legendPos LegendBottom
+    <> legendTitle "Series"
+    <> title "legend title (bottom)"
+
+  -- Phase 11 A5-c: guides サブシステム (reverse / ncol / guide hide)。
+  emitR "decoration/legend-reverse.svg" legendResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+    <> legend
+    <> legendReverse
+    <> title "legend (reverse = キー逆順, 色は固定)"
+  -- ncol: 6 カテゴリを 2 列に。
+  let ncolResolver k = case k of
+        "x" -> Just (NumData (V.fromList (map fromIntegral [0 .. 11 :: Int])))
+        "y" -> Just (NumData (V.fromList (map (\i -> fromIntegral (i `mod` 6 :: Int)) [0 .. 11 :: Int])))
+        "g" -> Just (TxtData (V.fromList (concatMap (replicate 2)
+                 (["cat-1", "cat-2", "cat-3", "cat-4", "cat-5", "cat-6"] :: [Text]))))
+        _   -> Nothing
+  emitR "decoration/legend-ncol.svg" ncolResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "g" <> size 5)
+    <> legend
+    <> legendNcol 2
+    <> title "legend (ncol=2, 6 カテゴリ → 2 列)"
+  -- guide hide: 色分けはするが凡例を出さない (= guides(color="none"))。
+  emitR "decoration/guide-none.svg" legendResolver $
+       purePlot
+    <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+    <> guideColorNone
+    <> title "guides(color=none) = 凡例非表示"
+
+  -- refLine
+  emit "decoration/refline.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline ys) <> size 5)
+    <> refHorizontal 40
+    <> title "scatter + reference horizontal at y=40"
+
+  -- jitter (Phase 6 A8、 P14 解消)
+  let jxs = concat $ replicate 4 [1.0, 2, 3, 4, 5] :: [Double]
+      jys = concat $ replicate 4 [3.0, 3, 3, 3, 3]
+  emit "decoration/jitter.svg" $
+       purePlot
+    <> layer (scatter (inline jxs) (inline jys)
+               <> size 5
+               <> jitterX 0.02 <> jitterY 0.02
+               <> alpha 0.6)
+    <> title "scatter with jitter (deterministic、 hash-rand offset)"
+
+  -- ===========================================================================
+  -- AXES
+  -- ===========================================================================
+
+  -- log axis
+  let lxs = [1, 10, 100, 1000, 10000] :: [Double]
+      lys = [2.0, 8.0, 16.0, 64.0, 256.0]
+  emit "axes/log.svg" $
+       purePlot
+    <> layer (scatter (inline lxs) (inline lys) <> size 6)
+    <> xAxis logAxis
+    <> yAxis logAxis
+    <> title "log-log axes"
+
+  -- sqrt axis (Phase 6 A6 で Layout 対応済)
+  emit "axes/sqrt.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline ys) <> size 5)
+    <> yAxis sqrtAxis
+    <> title "sqrt y axis (= Phase 6 A6)"
+
+  -- time axis (Phase 6 A7 で Layout + niceTimeTicks 対応)
+  -- 入力: unix epoch (= seconds since 1970)
+  let tdays = [1735689600.0, 1735776000, 1735862400, 1735948800, 1736035200, 1736121600] :: [Double]
+      tvals = [10.0, 14.0, 11.0, 16.0, 18.0, 13.0]
+  emit "axes/time.svg" $
+       purePlot
+    <> layer (line (inline tdays) (inline tvals) <> stroke 2)
+    <> xAxis (timeAxis "%Y-%m-%d")
+    <> title "time axis (= Phase 6 A7、 unix epoch + AxisTimeFmt)"
+
+  -- format
+  emit "axes/format.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline (map (* 100000) ys)) <> size 5)
+    <> yAxis (axisFormat (AxisExponentFmt 1))
+    <> title "exponent y format"
+
+  -- coord_fixed (Phase 8 A2 Step2): aspect = panel 高/幅比。 1.0 で正方 panel を
+  -- 可用域内に取り中央寄せ (ggplot coord_fixed)。 600x400 なので左右に余白が出る。
+  emit "axes/coord-fixed.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline ys) <> size 5)
+    <> aspectRatio 1.0
+    <> title "coord_fixed (aspect = 1)"
+    <> xLabel "x" <> yLabel "y"
+
+  -- ===========================================================================
+  -- THEME
+  -- ===========================================================================
+
+  emit "theme/dark.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline ys) <> size 5 <> color (fromHex "#56B4E9"))
+    <> theme ThemeDark
+    <> title "dark theme"
+
+  emit "theme/light.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline ys) <> size 5)
+    <> theme ThemeLight
+    <> title "light theme (= default)"
+
+  -- theme preset (ggplot theme_grey) + ブランド 3 種。
+  -- color by group で各 theme の panel 背景・grid・既定 series palette を一度に確認。
+  -- series palette は 7 色あるので 7 群 (A-G) を扇状にずらした直線で配置し、 全色を一度に確認。
+  let gxs       = [0,1,2,3,4,5,6,7] :: [Double]
+      grpY i    = [ fromIntegral (i * 3) + (1.0 + fromIntegral i * 0.18) * x | x <- gxs ]
+      grpLabels = ["A","B","C","D","E","F","G"] :: [Text]
+      themeResolver k = case k of
+        "x"     -> Just (NumData (V.fromList (concat (replicate 7 gxs))))
+        "y"     -> Just (NumData (V.fromList (concat [ grpY i | i <- [0..6] :: [Int] ])))
+        "group" -> Just (TxtData (V.fromList (concat [ replicate 8 g | g <- grpLabels ])))
+        _       -> Nothing
+      themeDemo file thm ttl =
+        emitR file themeResolver $
+             purePlot
+          <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+          <> theme thm
+          <> legend
+          <> title ttl
+  themeDemo "theme/grey.svg"  ThemeGrey         "theme_grey (ggplot 既定: 灰 panel + 白 grid)"
+  themeDemo "theme/noir.svg"  ThemeNoir  "Noir (暗・寒色アクセント)"
+  themeDemo "theme/lumen.svg" ThemeLumen "Lumen (白基調・深い差し色)"
+  themeDemo "theme/canvas.svg"      ThemeParchment     "Parchment (明: 外周白 + 羊皮紙 panel)"
+  themeDemo "theme/canvas-dark.svg" ThemeParchmentDark "Parchment Dark (Charcoal)"
+  themeDemo "theme/bw.svg"       ThemeBW       "theme_bw (白背景 + 4 辺枠)"
+  themeDemo "theme/classic.svg"  ThemeClassic  "theme_classic (grid なし + 下/左 軸線)"
+  themeDemo "theme/void.svg"     ThemeVoid     "theme_void (背景・grid・枠 すべてなし)"
+  themeDemo "theme/linedraw.svg" ThemeLinedraw "theme_linedraw (細い黒 grid + 黒枠)"
+
+  -- 学術向け named series palette を theme と独立に適用するデモ (theme=Minimal 白背景固定で色だけ差替)。
+  let paletteDemo file pal ttl =
+        emitR file themeResolver $
+             purePlot
+          <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+          <> theme ThemeMinimal
+          <> palette pal
+          <> legend
+          <> title ttl
+  paletteDemo "palette/okabe-ito.svg"    okabeIto    "Okabe-Ito (colorblind-safe)"
+  paletteDemo "palette/tol-bright.svg"   tolBright   "Paul Tol bright (colorblind-safe)"
+  paletteDemo "palette/brewer-set2.svg"  brewerSet2  "ColorBrewer Set2"
+  paletteDemo "palette/brewer-dark2.svg" brewerDark2 "ColorBrewer Dark2"
+
+  -- A-2: element 単位 theme override の例 (preset に themeGrid/panelBorder 等を `<>` で重ねる)。
+  let overrideDemo file extra ttl =
+        emitR file themeResolver $
+             purePlot
+          <> layer (scatter "x" "y" <> colorBy "group" <> size 5)
+          <> extra
+          <> legend
+          <> title ttl
+  overrideDemo "theme/ov-grey-nogrid.svg"
+    (theme ThemeGrey <> themeGrid False)
+    "theme_grey + grid off (override)"
+  overrideDemo "theme/ov-canvas-custom.svg"
+    (theme ThemeParchment <> panelBorder True <> gridColor "#d8c9a8")
+    "canvas + border on + custom grid (override)"
+
+  -- A-3: 文字 theme 統合の例。 theme 経由で title / tick の font を上書き、 axis.text を回転。
+  overrideDemo "theme/ov-font.svg"
+    (theme ThemeMinimal
+      <> themeTitleFont (fontSize 20 <> fontWeight "bold" <> fontColor "#7A1F23")
+      <> themeTickFont  (fontSize 13 <> fontColor "#3E6A6F"))
+    "theme font override (title 20pt bold + tick 13pt teal)"
+  overrideDemo "theme/ov-axis-angle.svg"
+    (theme ThemeMinimal <> themeAxisTextAngle 45)
+    "theme axis.text angle = 45° (x/y 両軸)"
+
+  -- ===========================================================================
+  -- STATISTICAL: DAG / ModelGraph (= Phase 1 完了の hbm 風)
+  -- ===========================================================================
+
+  let alphaN  = ("alpha"  :: Text, "α"      :: Text, NodeLatent)
+      betaN   = ("beta"   :: Text, "β"      :: Text, NodeLatent)
+      sigmaN  = ("sigma"  :: Text, "σ"      :: Text, NodeLatent)
+      muN     = ("mu"     :: Text, "μ"      :: Text, NodeLatent)
+      yN      = ("y"      :: Text, "y"      :: Text, NodeObserved)
+      nodes   = [ dagNode (one t)   (two t)   (three t) 0 0
+                | t <- [alphaN, betaN, sigmaN, muN, yN] ]
+      edges   = [ dagEdge "alpha" "mu"
+                , dagEdge "beta"  "mu"
+                , dagEdge "mu"    "y"
+                , dagEdge "sigma" "y" ]
+      one (a,_,_) = a; two (_,b,_) = b; three (_,_,c) = c
+      -- ★ 階層 layout (Sugiyama) を適用して dnX/dnY を埋める。 これを通さないと
+      -- 全 node が dnX=dnY=0 のまま 1 点に潰れる (dagPlotWith の内部処理と同一)。
+      (positioned, routedEdges) = layoutHierarchicalFull nodes edges
+  emit "statistical/dag-hbm.svg" $
+       purePlot
+    <> layer (dagFromLists positioned routedEdges LayoutHierarchical)
+    <> title "DAG (HBM-like): Phase 1 Sugiyama framework"
+    <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+
+  putStrLn "Done. Outputs in design/gallery/"
+
+-- ===========================================================================
+-- helper: 共通 width/height + emptyResolver で saveSVG
+-- ===========================================================================
+
+-- ===========================================================================
+-- helper: 決定論的 Gaussian (= 再現可能な demo データ生成、 Phase 8)
+-- ===========================================================================
+
+-- | 線形合同法 (LCG) による決定論的 [0,1) 一様乱数列。 seed 固定で再現可能。
+-- glibc 系パラメータ (a=1103515245, c=12345, m=2^31)。
+lcg01 :: Int -> [Double]
+lcg01 seed = go (fromIntegral seed `mod` m)
+  where
+    a = 1103515245 :: Integer
+    c = 12345      :: Integer
+    m = 2147483648 :: Integer  -- 2^31
+    go s = let s' = (a * s + c) `mod` m
+           in fromIntegral s' / fromIntegral m : go s'
+
+-- | 決定論的 Gaussian N(mu, sigma) を n 個。 Box-Muller (LCG 一様列のペアから生成)。
+gaussian :: Int -> Double -> Double -> Int -> [Double]
+gaussian seed mu sigma n = take n (boxMuller (lcg01 seed))
+  where
+    boxMuller (u1 : u2 : rest) =
+      let r = sqrt (negate 2 * log (max 1e-12 u1))
+          z = r * cos (2 * pi * u2)
+      in (mu + sigma * z) : boxMuller rest
+    boxMuller _ = []
+
+-- | Phase 8 B2: 分布系プロット (raincloud/violin/strip/swarm/ridge/box) で共用する
+-- 群データ。 4 群 × 各 N=90、 群ごとに平均・分散を変えた正規分布 (= 参照画像の
+-- DD/DR/RD/RR 風)。 決定論的なので compare 回帰に使える。
+groupLabels :: [Text]
+groupLabels = ["DD", "DR", "RD", "RR"]
+
+groupSpecs :: [(Text, Int, Double, Double)]  -- (label, seed, mu, sigma)
+groupSpecs =
+  [ ("DD", 11, 95.0, 22.0)
+  , ("DR", 23, 82.0, 16.0)
+  , ("RD", 37, 101.0, 20.0)
+  , ("RR", 53, 78.0, 14.0)
+  ]
+
+-- | 群データを (繰り返しラベル, 値) の 2 列に flatten。 各群 N=90。
+groupedDemo :: ([Text], [Double])
+groupedDemo =
+  let n = 90
+      perGroup = [ (replicate n lbl, gaussian seed mu sigma n)
+                 | (lbl, seed, mu, sigma) <- groupSpecs ]
+  in (concatMap fst perGroup, concatMap snd perGroup)
+
+emit :: FilePath -> VisualSpec -> IO ()
+emit relPath spec = do
+  let sized = spec <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  saveSVG ("design/gallery/" ++ relPath) sized
+  -- DAG layer を含む spec は edge routing (deRoute) を JSON へ焼き込む。
+  -- PS canvas は routing を持たないため、 これで HS と同じ spline を再現できる
+  -- (DAG を含まない spec には no-op)。
+  writeSpecJSON relPath (bakeDAGRoutesInSpec sized)
+
+emitR :: FilePath -> Resolver -> VisualSpec -> IO ()
+emitR relPath r spec = do
+  let sized = spec <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  saveSVGWith ("design/gallery/" ++ relPath) r sized
+  -- Phase 8 B16: Resolver を JSON に焼き込む (ColByName → inline)。 PS は Resolver を
+  -- 持たないため、 これをしないと pairs/facet/legend 等が PS で空になる。
+  writeSpecJSON relPath (bakeSpec r sized)
+
+-- | spec を JSON 化して design/gallery/specs/ に同名 .json で保存。
+-- 比較 demo (= compare.html) で PS canvas backend に渡す入力になる。
+-- Resolver は inline data 前提なので保存しない (= ColNum / ColTxt は inline で完結)。
+writeSpecJSON :: FilePath -> VisualSpec -> IO ()
+writeSpecJSON relPath spec = do
+  let jsonPath = "design/gallery/specs/" ++ replaceExt relPath ".json"
+      dir      = takeDir jsonPath
+  System.Directory.createDirectoryIfMissing True dir
+  Data.ByteString.Lazy.writeFile jsonPath (Data.Aeson.encode spec)
+  where
+    replaceExt p _ = takeWhile (/= '.') p ++ ".json"
+    takeDir p =
+      let parts = reverse (splitOn '/' p)
+      in case parts of
+        []     -> "."
+        [_]    -> "."
+        _:rest -> joinWith '/' (reverse rest)
+    splitOn c s = case break (== c) s of
+      (h, [])   -> [h]
+      (h, _:t) -> h : splitOn c t
+    joinWith _ []     = ""
+    joinWith _ [x]    = x
+    joinWith c (x:xs) = x ++ [c] ++ joinWith c xs
diff --git a/examples/JsonDump.hs b/examples/JsonDump.hs
new file mode 100644
--- /dev/null
+++ b/examples/JsonDump.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+import Graphics.Hgg.Easy
+import           Graphics.Hgg.Unit         (px, (*~))
+import Data.Aeson (encode)
+import qualified Data.ByteString.Lazy.Char8 as BL
+
+main :: IO ()
+main = do
+  let s = purePlot
+            <> layer (scatter "x" "y" <> alpha 0.7 <> size 5
+                       <> colorBy "g")
+            <> layer (line (inline [1.0, 2.0, 3.0]) (inline [4.0, 5.0, 6.0])
+                       <> color (fromHex "#ff0000"))
+            <> title "demo"
+            <> theme ThemeDark
+            <> facet "region"
+            <> widthUnit (800 *~ px) <> heightUnit (600 *~ px)
+  BL.putStrLn (encode s)
diff --git a/examples/ScatterDemo.hs b/examples/ScatterDemo.hs
new file mode 100644
--- /dev/null
+++ b/examples/ScatterDemo.hs
@@ -0,0 +1,214 @@
+-- | Phase 26 §A-2 新 API デモ: 全 helper を `<>` で paren 無し合成。
+--
+-- @
+-- cabal run scatter-demo
+-- @
+-- → カレントディレクトリに @scatter-demo.svg@ が生成される。
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG, saveSVGWith, saveSVGInteractive)
+import           Graphics.Hgg.Unit         (px, (*~))
+import qualified Graphics.Hgg.DAG
+import           Graphics.Hgg.DAG         ((~>))
+import           Graphics.Hgg.Easy
+import qualified Graphics.Hgg.Spec
+import           Data.Text                (Text)
+import qualified Data.Vector              as V
+
+main :: IO ()
+main = do
+  -- y = x² + regression line を Layer monoid で構築
+  let xs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double]
+      ys = map (\x -> x * x) xs
+      fitY = map (\x -> 8 * x - 12) xs   -- 直線当てはめ (= demo 用)
+      spec
+        =  purePlot
+        <> layer (scatter (inline xs) (inline ys) <> alpha 0.85 <> size 5)
+        <> layer (line    (inline xs) (inline fitY) <> color (fromHex "#d62728") <> stroke 2)
+        <> title  "y = x²  (with linear fit)"
+        <> xLabel "x"
+        <> yLabel "y"
+        <> widthUnit (800 *~ px)
+        <> heightUnit (600 *~ px)
+  saveSVG "scatter-demo.svg"      spec
+  saveSVG "scatter-demo-dark.svg" (spec <> theme ThemeDark)
+  -- categorical 色分け demo: 30 点を 3 group に
+  let xs2 = [0, 1 .. 29] :: [Double]
+      ys2 = [ sin (x / 3) * 5 + 10 | x <- xs2 ]
+      grp = take 30 (cycle (["A", "B", "C"] :: [String]))
+      spec2
+        =  purePlot
+        <> layer (scatter (inline xs2) (inline ys2)
+                   <> colorBy (inlineCat grp)
+                   <> size 6
+                   <> alpha 0.85)
+        <> title  "Scatter by group (categorical color)"
+        <> xLabel "t" <> yLabel "f(t)"
+  saveSVG "scatter-categorical.svg" spec2
+  -- Interactive (= hover tooltip + drag pan + wheel zoom、 ブラウザで開いて確認)
+  saveSVGInteractive "scatter-interactive.svg" emptyResolver spec2
+  -- Box + Density 統計 chart demo (= Phase 26 §E-2)
+  let vals = [3.0, 4.0, 4.5, 5.0, 5.0, 5.2, 5.5, 6.0, 6.5, 7.0, 8.0, 12.0]
+      boxSpec   = purePlot <> layer (boxplot (inline vals))
+                          <> title "Boxplot demo" <> widthUnit (400 *~ px) <> heightUnit (600 *~ px)
+      densSpec  = purePlot <> layer (density (inline vals))
+                          <> title "Density (KDE) demo"
+                          <> xLabel "value" <> yLabel "density"
+  saveSVG "box-demo.svg"     boxSpec
+  saveSVG "density-demo.svg" densSpec
+  -- LogScale demo (= Phase 26 §C-2 #1)
+  let xsLog = [1, 10, 100, 1000, 10000] :: [Double]
+      ysLog = [2, 30, 500, 8000, 120000] :: [Double]
+      logSpec = purePlot
+        <> layer (scatter (inline xsLog) (inline ysLog) <> size 6)
+        <> xAxis logAxis
+        <> yAxis logAxis
+        <> title  "Log-Log demo (= xLog + yLog)"
+        <> xLabel "x (log)" <> yLabel "y (log)"
+  saveSVG "loglog-demo.svg" logSpec
+  -- AxisFormat demo (= Phase 26 §C-2 #2)
+  let fmtSpec = purePlot
+        <> layer (scatter (inline [0.0, 1.0, 2.0]) (inline [0.001234, 0.05678, 1.234]))
+        <> yAxis (axisFormat (AxisExponentFmt 2))
+        <> xAxis (axisFormat AxisIntegerFmt)
+        <> title  "Axis format: Y=exp(2digit), X=integer"
+        <> xLabel "x" <> yLabel "y"
+  saveSVG "axisformat-demo.svg" fmtSpec
+  -- Reference line demo (= Phase 26 §C-2 #3)
+  let xsR = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] :: [Double]
+      ysR = [0.1, 1.2, 1.8, 3.3, 4.1, 5.2] :: [Double]
+      refSpec = purePlot
+        <> layer (scatter (inline xsR) (inline ysR) <> size 6)
+        <> refIdentity                   -- y = x (Actual vs Predicted の対角線)
+        <> refHorizontal 2.5             -- y = 2.5
+        <> refVertical 3.0               -- x = 3.0
+        <> title  "Reference lines (y=x / y=2.5 / x=3)"
+        <> xLabel "predicted" <> yLabel "actual"
+  saveSVG "refline-demo.svg" refSpec
+  -- Trellis (facet) demo (= Phase 26 §C-2 #12)
+  let r facetN = case facetN of
+        "x" -> Just (NumData (V.fromList [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]))
+        "y" -> Just (NumData (V.fromList [1, 4, 9, 16, 2, 5, 8, 12, 3, 6, 9, 15]))
+        "g" -> Just (TxtData (V.fromList ["A","A","A","A","B","B","B","B","C","C","C","C"]))
+        _   -> Nothing
+      facetSpec = purePlot
+        <> layer (scatter "x" "y" <> colorBy "g" <> size 6)
+        <> facet "g"
+        <> title  "Facet by group (3 panel)"
+        <> xLabel "x" <> yLabel "y"
+  saveSVGWith "facet-demo.svg" r facetSpec
+  -- DAG demo (= §E-6: HBM ModelGraph、 algebraic-graphs 流 API)
+  let alphaN = ("alpha" :: Text, "α"     :: Text, NodeLatent)
+      betaN  = ("beta"  :: Text, "β"     :: Text, NodeLatent)
+      sigmaN = ("sigma" :: Text, "σ"     :: Text, NodeLatent)
+      yN     = ("y"     :: Text, "y obs" :: Text, NodeObserved)
+      hbmGraph =  alphaN ~> sigmaN
+               <> betaN  ~> sigmaN
+               <> sigmaN ~> yN
+               <> alphaN ~> yN
+               <> betaN  ~> yN
+      dagSpec = purePlot
+        <> layer (Graphics.Hgg.DAG.dagPlot hbmGraph <> size 22)
+        <> title  "HBM ModelGraph (§E-6 DAG, hierarchical layout)"
+        <> theme  ThemeLight
+        <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  saveSVG "dag-demo.svg" dagSpec
+  -- ユーザ PyMC モデル再現 (= s_C/s_P/b_C/b_P/b1〜b4 priors + Weather/A/Score MutableData
+  --   + x_J/x_C/x_P/x deterministic + Y Bernoulli observed、 record/course/person plates)
+  let pymcLikeSpec = purePlot
+        <> layer (Graphics.Hgg.Spec.dagFromListsWithPlates pymcNodes pymcEdges
+                    Graphics.Hgg.Spec.LayoutManual pymcPlates
+                  <> size 24)
+        <> title "PyMC モデル再現 (自作 hgg)"
+        <> theme  ThemeLight
+        <> widthUnit (1000 *~ px) <> heightUnit (720 *~ px)
+  saveSVG "pymc-like-demo.svg" pymcLikeSpec
+  -- TODO-3 (2026-05-29) sub-features demo: jitter + shapeBy + sizeBy + colorCats
+  let xs3 = [1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0] :: [Double]
+      ys3 = [1.5, 1.8, 2.2, 2.6, 3.1, 3.5, 4.0, 4.4, 4.7, 5.2] :: [Double]
+      g3  = ["A","B","A","B","A","B","A","B","A","B"] :: [Text]
+      s3  = [1.0, 5.0, 2.0, 4.0, 3.0, 6.0, 4.5, 2.5, 5.5, 3.5] :: [Double]
+      todo3Spec = purePlot
+        <> layer (scatter (inline xs3) (inline ys3)
+                   <> jitterX 0.03
+                   <> jitterY 0.03
+                   <> colorBy (inlineCat g3)
+                   <> colorCats ["A", "B"]
+                   <> shapeBy (inlineCat g3)
+                   <> shapeMapEntry "A" MShTriangle
+                   <> shapeMapEntry "B" MShSquare
+                   <> sizeBy (inline s3)
+                   <> alpha 0.9)
+        <> title  "TODO-3 demo (jitter + shapeBy + sizeBy + colorCats)"
+        <> xLabel "x" <> yLabel "y"
+        <> widthUnit (700 *~ px) <> heightUnit (500 *~ px)
+        <> xAxis (axisRotate 30)
+  saveSVG "todo3-demo.svg" todo3Spec
+  -- TODO-10 (2026-05-29) font sweep demo: titleFont / tickFont / axisLabelFont をカスタム
+  let todo10Spec = purePlot
+        <> layer (scatter (inline [1.0, 2.0, 3.0, 4.0, 5.0])
+                          (inline [1.0, 3.0, 2.0, 4.0, 3.5]) <> size 6)
+        <> title  "TODO-10 font sweep demo (bold italic title + monospace tick)"
+        <> xLabel "x axis" <> yLabel "y axis"
+        <> titleFont     (fontSize 20 <> fontWeight "bold" <> fontItalic True
+                                       <> fontColor "#c0392b")
+        <> axisLabelFont (fontSize 14 <> fontFamily "Georgia"
+                                       <> fontColor "#2c3e50")
+        <> tickFont      (fontSize 10 <> fontFamily "monospace"
+                                       <> fontColor "#7f8c8d")
+        <> widthUnit (700 *~ px) <> heightUnit (500 *~ px)
+  saveSVG "todo10-demo.svg" todo10Spec
+  putStrLn "wrote scatter + dark + categorical + interactive + box + density + loglog + axisformat + refline + facet + dag + pymc-like + todo3 + todo10"
+  where
+    -- PyMC モデル node 定義 (= LayoutManual で位置手動指定、 ユーザ画像と同じ配置)
+    -- 画像から読み取り: 4 階層 (= prior super → prior → deterministic → observed)
+    -- 上から: s_C/s_P → b_C/b_P/b1〜b4 + Weather/A/Score → x_J/x_C/x_P → x → Y
+    pymcNodes :: [Graphics.Hgg.Spec.DAGNode]
+    pymcNodes =
+      [ -- 階層 0 (= 超事前、 y=0.05): HalfCauchy hyper-priors (s_C / s_P)
+        Graphics.Hgg.Spec.dagNodeDist "sC" "s_C" Graphics.Hgg.Spec.NodeLatent "HalfCauchy" 0.30 0.05
+      , Graphics.Hgg.Spec.dagNodeDist "sP" "s_P" Graphics.Hgg.Spec.NodeLatent "HalfCauchy" 0.55 0.05
+        -- 階層 1 (= prior + data、 y=0.30)
+      , Graphics.Hgg.Spec.dagNodeDist "weather" "Weather" Graphics.Hgg.Spec.NodeData    "MutableData" 0.05 0.30
+      , Graphics.Hgg.Spec.dagNodeDist "b4"      "b4"      Graphics.Hgg.Spec.NodeLatent  "Flat" 0.18 0.30
+      , Graphics.Hgg.Spec.dagNodeDist "bC"      "b_C"     Graphics.Hgg.Spec.NodeLatent  "Normal" 0.30 0.30
+      , Graphics.Hgg.Spec.dagNodeDist "b2"      "b2"      Graphics.Hgg.Spec.NodeLatent  "Flat" 0.45 0.30
+      , Graphics.Hgg.Spec.dagNodeDist "bP"      "b_P"     Graphics.Hgg.Spec.NodeLatent  "Normal" 0.55 0.30
+      , Graphics.Hgg.Spec.dagNodeDist "A"       "A"       Graphics.Hgg.Spec.NodeData    "MutableData" 0.66 0.30
+      , Graphics.Hgg.Spec.dagNodeDist "score"   "Score"   Graphics.Hgg.Spec.NodeData    "MutableData" 0.78 0.30
+      , Graphics.Hgg.Spec.dagNodeDist "b3"      "b3"      Graphics.Hgg.Spec.NodeLatent  "Flat" 0.92 0.30
+        -- 階層 2 (= deterministic、 y=0.55)
+      , Graphics.Hgg.Spec.dagNodeDist "xJ" "x_J" Graphics.Hgg.Spec.NodeOther "Deterministic" 0.05 0.55
+      , Graphics.Hgg.Spec.dagNodeDist "b1" "b1"  Graphics.Hgg.Spec.NodeLatent "Flat" 0.20 0.55
+      , Graphics.Hgg.Spec.dagNodeDist "xC" "x_C" Graphics.Hgg.Spec.NodeOther  "Deterministic" 0.30 0.55
+      , Graphics.Hgg.Spec.dagNodeDist "xP" "x_P" Graphics.Hgg.Spec.NodeOther  "Deterministic" 0.66 0.55
+        -- 階層 3 (= 合算 deterministic、 y=0.78)
+      , Graphics.Hgg.Spec.dagNodeDist "x"  "x"   Graphics.Hgg.Spec.NodeOther  "Deterministic" 0.10 0.78
+        -- 階層 4 (= observed、 y=0.96)
+      , Graphics.Hgg.Spec.dagNodeDist "Y"  "Y"   Graphics.Hgg.Spec.NodeObserved "Bernoulli" 0.10 0.96
+      ]
+    pymcEdges :: [Graphics.Hgg.Spec.DAGEdge]
+    pymcEdges =
+      [ Graphics.Hgg.Spec.dagEdge "sC" "bC"
+      , Graphics.Hgg.Spec.dagEdge "sP" "bP"
+      , Graphics.Hgg.Spec.dagEdge "weather" "xJ"
+      , Graphics.Hgg.Spec.dagEdge "b4" "xJ"
+      , Graphics.Hgg.Spec.dagEdge "bC" "xC"
+      , Graphics.Hgg.Spec.dagEdge "b2" "xP"
+      , Graphics.Hgg.Spec.dagEdge "bP" "xP"
+      , Graphics.Hgg.Spec.dagEdge "A" "xP"
+      , Graphics.Hgg.Spec.dagEdge "score" "xP"
+      , Graphics.Hgg.Spec.dagEdge "b3" "x"
+      , Graphics.Hgg.Spec.dagEdge "xJ" "x"
+      , Graphics.Hgg.Spec.dagEdge "b1" "x"
+      , Graphics.Hgg.Spec.dagEdge "xC" "x"
+      , Graphics.Hgg.Spec.dagEdge "xP" "x"
+      , Graphics.Hgg.Spec.dagEdge "x"  "Y"
+      ]
+    pymcPlates :: [Graphics.Hgg.Spec.DAGPlate]
+    pymcPlates =
+      [ Graphics.Hgg.Spec.DAGPlate "record (2396)"  ["xJ", "x", "Y"]
+      , Graphics.Hgg.Spec.DAGPlate "course (10)"    ["bC", "xC"]
+      , Graphics.Hgg.Spec.DAGPlate "person (50)"    ["bP", "A", "score", "xP"]
+      ]
diff --git a/examples/Tutorial01Easy.hs b/examples/Tutorial01Easy.hs
new file mode 100644
--- /dev/null
+++ b/examples/Tutorial01Easy.hs
@@ -0,0 +1,26 @@
+-- | Tutorial 01 ─ Easy API (Layer 1)。 入門者向け、 [Double] を直接渡して 1 枚出す。
+--
+-- @
+-- cabal run tutorial-01-easy
+-- @
+--
+-- → カレントディレクトリに @tutorial-01-easy.svg@ を生成。
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG)
+import           Graphics.Hgg.Unit         (px, (*~))
+import           Graphics.Hgg.Easy
+
+main :: IO ()
+main = do
+  let xs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double]
+      ys = map (\x -> x * x) xs
+  -- Easy 層: `points` は値直接受け (= `scatter (inline xs) (inline ys)` の別名)、
+  -- 重畳は `overlay` で包む (`scatter <> line` の落とし穴を回避)。
+  saveSVG "tutorial-01-easy.svg" $
+       overlay [ points xs ys ]
+    <> title  "Easy API: y = x\xb2"
+    <> xLabel "x" <> yLabel "y"
+    <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  putStrLn "wrote tutorial-01-easy.svg"
diff --git a/examples/Tutorial02Grammar.hs b/examples/Tutorial02Grammar.hs
new file mode 100644
--- /dev/null
+++ b/examples/Tutorial02Grammar.hs
@@ -0,0 +1,32 @@
+-- | Tutorial 02 ─ Grammar API (Layer 2、 ggplot 風)。 `inline`/`inlineCat` で
+--   channel を作り、 `<>` で aesthetic を合成、 `scale_*` + `legend` で色分け。
+--
+-- @
+-- cabal run tutorial-02-grammar
+-- @
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG)
+import           Graphics.Hgg.Unit         (px, (*~))
+import           Graphics.Hgg.Spec
+import           Data.Text                (Text)
+
+main :: IO ()
+main = do
+  -- long-form データ: 各 row が (x, y, group)。 group で色分けする。
+  let xs = inline    (concat (replicate 2 ([1.0, 2.0, 3.0, 4.0] :: [Double])))
+      ys = inline    ([2.0, 3.0, 1.0, 4.0,  3.0, 1.0, 4.0, 2.0] :: [Double])
+      gs = inlineCat (concatMap (replicate 4) (["alpha", "beta"] :: [Text]))
+  -- ggplot で言えば:
+  --   ggplot(d, aes(x, y, color=group)) + geom_point(size=6) +
+  --     scale_color_manual(values=c(alpha="#1B9E77", beta="#D95F02"))
+  saveSVG "tutorial-02-grammar.svg" $
+       purePlot
+    <> layer (scatter xs ys <> colorBy gs <> size 6)
+    <> scaleColorManual [("alpha", "#1B9E77"), ("beta", "#D95F02")]
+    <> legend
+    <> title  "Grammar API: scale_color_manual"
+    <> xLabel "x" <> yLabel "y"
+    <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  putStrLn "wrote tutorial-02-grammar.svg"
diff --git a/examples/Tutorial03Overlay.hs b/examples/Tutorial03Overlay.hs
new file mode 100644
--- /dev/null
+++ b/examples/Tutorial03Overlay.hs
@@ -0,0 +1,26 @@
+-- | Tutorial 03 ─ 複数 layer の重畳。 散布点の上に折れ線を重ねる。
+--   各 `layer` は `<>` で合成され、 後に書いた layer が上に描かれる。
+--
+-- @
+-- cabal run tutorial-03-overlay
+-- @
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG)
+import           Graphics.Hgg.Unit         (px, (*~))
+import           Graphics.Hgg.Spec
+
+main :: IO ()
+main = do
+  let xs  = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] :: [Double]
+      ys  = [0.2, 1.1, 3.9, 9.2, 15.8, 25.1, 36.2, 48.9, 64.1, 80.8] :: [Double]
+      fit = map (\x -> x * x) xs
+  saveSVG "tutorial-03-overlay.svg" $
+       purePlot
+    <> layer (scatter (inline xs) (inline ys) <> alpha 0.85 <> size 5)
+    <> layer (line    (inline xs) (inline fit) <> color (fromHex "#dc2626") <> stroke 2)
+    <> title  "Overlay: observed points + fitted curve"
+    <> xLabel "x" <> yLabel "y"
+    <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  putStrLn "wrote tutorial-03-overlay.svg"
diff --git a/examples/Tutorial04Distribution.hs b/examples/Tutorial04Distribution.hs
new file mode 100644
--- /dev/null
+++ b/examples/Tutorial04Distribution.hs
@@ -0,0 +1,36 @@
+-- | Tutorial 04 ─ 分布の可視化。 群ごとの violin と box を並べる。
+--   分布系 mark は (群ラベル channel, 値 channel) を取る。
+--
+-- @
+-- cabal run tutorial-04-distribution
+-- @
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG)
+import           Graphics.Hgg.Unit         (px, (*~))
+import           Graphics.Hgg.Spec
+import           Data.Text                (Text)
+
+main :: IO ()
+main = do
+  -- 各 row が (群, 値)。 ここでは 3 群 × 各数点の小さな例。
+  let cats = inlineCat (concatMap (replicate 6)
+                         (["control", "low", "high"] :: [Text]))
+      vals = inline ([ 4.8, 5.1, 5.3, 4.9, 5.0, 5.2      -- control
+                     , 6.0, 6.4, 5.8, 6.2, 6.1, 6.3      -- low
+                     , 7.1, 7.5, 6.9, 7.3, 7.0, 7.4      -- high
+                     ] :: [Double])
+  saveSVG "tutorial-04-violin.svg" $
+       purePlot
+    <> layer (violin vals <> groupBy cats)
+    <> title  "Distribution: violin by group"
+    <> xLabel "dose" <> yLabel "response"
+    <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  saveSVG "tutorial-04-box.svg" $
+       purePlot
+    <> layer (boxplot vals <> groupBy cats)
+    <> title  "Distribution: box by group"
+    <> xLabel "dose" <> yLabel "response"
+    <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  putStrLn "wrote tutorial-04-violin.svg / tutorial-04-box.svg"
diff --git a/examples/Tutorial05Theme.hs b/examples/Tutorial05Theme.hs
new file mode 100644
--- /dev/null
+++ b/examples/Tutorial05Theme.hs
@@ -0,0 +1,28 @@
+-- | Tutorial 05 ─ テーマと配色。 同じ図を ThemeDefault / ThemeDark で出し分ける。
+--   `theme` は VisualSpec を返すだけなので `<>` で足すだけ。
+--
+-- @
+-- cabal run tutorial-05-theme
+-- @
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG)
+import           Graphics.Hgg.Unit         (px, (*~))
+import           Graphics.Hgg.Spec
+import           Data.Text                (Text)
+
+main :: IO ()
+main = do
+  let xs = inline    (concat (replicate 3 ([1.0, 2.0, 3.0, 4.0, 5.0] :: [Double])))
+      ys = inline    ([ 1, 2, 1.5, 3, 2.5,  2, 3, 2.5, 4, 3.5,  3, 4, 3.5, 5, 4.5 ] :: [Double])
+      gs = inlineCat (concatMap (replicate 5) (["x", "y", "z"] :: [Text]))
+      base nm = purePlot
+             <> layer (scatter xs ys <> colorBy gs <> size 6)
+             <> legend
+             <> title nm
+             <> xLabel "x" <> yLabel "y"
+             <> widthUnit (600 *~ px) <> heightUnit (400 *~ px)
+  saveSVG "tutorial-05-light.svg" (base "ThemeDefault" <> theme ThemeDefault)
+  saveSVG "tutorial-05-dark.svg"  (base "ThemeDark"    <> theme ThemeDark)
+  putStrLn "wrote tutorial-05-light.svg / tutorial-05-dark.svg"
diff --git a/examples/VegaLiteGallery.hs b/examples/VegaLiteGallery.hs
new file mode 100644
--- /dev/null
+++ b/examples/VegaLiteGallery.hs
@@ -0,0 +1,191 @@
+-- | Vega-Lite examples gallery (https://vega.github.io/vega-lite/examples/) の
+--   再現デモ。 各 Vega-Lite セクションの代表例を hgg で描き直す。
+--
+--   @cabal run vega-lite-gallery@ → @design/vega-lite-gallery/*.svg@ を生成。
+--
+--   ★描けない例 (地理 / 対話 / image = 仕様外、 concat/repeat 等 = 今後の課題) は
+--     本デモには含めない (docs/comparison-vega-lite.md の分類を参照)。
+{-# LANGUAGE OverloadedStrings #-}
+module Main (main) where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG, saveSVGWith)
+import           Graphics.Hgg.Easy
+import qualified Data.Vector              as V
+import           System.Directory         (createDirectoryIfMissing)
+
+out :: FilePath -> VisualSpec -> IO ()
+out name = saveSVG ("design/vega-lite-gallery/" <> name <> ".svg")
+
+main :: IO ()
+main = do
+  createDirectoryIfMissing True "design/vega-lite-gallery"
+
+  -- == Bar Charts ==========================================================
+  -- Simple Bar Chart
+  out "01-bar-simple" $ purePlot
+    <> layer (bar (inlineCat (["A","B","C","D","E"] :: [String])) (inline [28,55,43,91,81]))
+    <> title "Simple Bar Chart" <> xLabel "category" <> yLabel "value"
+
+  -- Grouped Bar Chart (dodge)
+  let gcat = inlineCat (concatMap (replicate 3) (["A","B","C"] :: [String]))
+      ggrp = inlineCat (take 9 (cycle (["x","y","z"] :: [String])))
+      gval = inline [3,5,2, 4,1,6, 2,3,4]
+  out "02-bar-grouped" $ purePlot
+    <> layer (bar gcat gval <> colorBy ggrp <> position PosDodge)
+    <> title "Grouped Bar Chart (dodge)" <> xLabel "category" <> yLabel "value"
+
+  -- Stacked Bar Chart
+  out "03-bar-stacked" $ purePlot
+    <> layer (bar gcat gval <> colorBy ggrp <> position PosStack)
+    <> title "Stacked Bar Chart" <> xLabel "category" <> yLabel "value"
+
+  -- Normalized (Percentage) Stacked Bar Chart
+  out "04-bar-normalized" $ purePlot
+    <> layer (bar gcat gval <> colorBy ggrp <> position PosFill)
+    <> title "Normalized (Percentage) Stacked Bar Chart"
+
+  -- == Histograms / Density / Cumulative ===================================
+  let vals = [3.0,4.0,4.5,5.0,5.0,5.2,5.5,6.0,6.5,7.0,8.0,12.0
+             ,4.8,5.1,5.3,5.9,6.2,6.8,7.5,4.2,5.6,6.1]
+  out "05-histogram" $ purePlot
+    <> layer (histogram (inline vals))
+    <> title "Histogram" <> xLabel "x" <> yLabel "count"
+
+  out "06-density" $ purePlot
+    <> layer (density (inline vals))
+    <> title "Density Plot" <> xLabel "x" <> yLabel "density"
+
+  -- Cumulative Frequency Distribution
+  out "07-ecdf" $ purePlot
+    <> layer (ecdf (inline vals))
+    <> title "Cumulative Frequency Distribution (ECDF)" <> xLabel "x" <> yLabel "F(x)"
+
+  -- == Scatter & Strip Plots ===============================================
+  let sx = [1.0,2,3,4,5,6,7,8,9,10]
+      sy = [2.1,3.9,6.0,7.7,10.2,11.8,14.1,15.9,18.2,20.0]
+  out "08-scatter" $ purePlot
+    <> layer (scatter (inline sx) (inline sy) <> size 6)
+    <> title "Scatterplot" <> xLabel "x" <> yLabel "y"
+
+  -- Bubble Plot (color + size)
+  let bgrp = inlineCat (take 10 (cycle (["A","B"] :: [String])))
+      bsz  = inline [2,8,3,9,4,7,5,6,3,8]
+  out "09-bubble" $ purePlot
+    <> layer (scatter (inline sx) (inline sy) <> colorBy bgrp <> sizeBy bsz <> alpha 0.8)
+    <> title "Bubble Plot" <> xLabel "x" <> yLabel "y"
+
+  -- 1D Strip Plot
+  out "10-strip" $ purePlot
+    <> layer (strip (inline vals) <> groupBy (inlineCat (replicate (length vals) ("v" :: String))))
+    <> title "Strip Plot" <> yLabel "value"
+
+  -- == Line Charts =========================================================
+  let lx = [0.0,1,2,3,4,5,6,7,8,9]
+  out "11-line" $ purePlot
+    <> layer (line (inline lx) (inline (map (\x -> sin (x/2)*5+10) lx)) <> stroke 2)
+    <> title "Line Chart" <> xLabel "t" <> yLabel "f(t)"
+
+  -- Multi Series Line Chart
+  out "12-line-multi" $ purePlot
+    <> layer (line (inline lx) (inline (map (\x -> sin (x/2)*5+10) lx)) <> color (fromHex "#1f77b4") <> stroke 2)
+    <> layer (line (inline lx) (inline (map (\x -> cos (x/2)*5+10) lx)) <> color (fromHex "#d62728") <> stroke 2)
+    <> title "Multi Series Line Chart" <> xLabel "t" <> yLabel "f(t)"
+
+  -- Step Chart
+  out "13-step" $ purePlot
+    <> layer (step (inline lx) (inline [0,0,1,1,3,3,2,2,4,4]) <> stroke 2)
+    <> title "Step Chart" <> xLabel "t" <> yLabel "v"
+
+  -- == Area Charts =========================================================
+  -- Area Chart = band を 0..y で塗る
+  let ax = [0.0,1,2,3,4,5,6,7,8,9]
+      ay = [1.0,3,2,5,4,6,5,7,6,8]
+  out "14-area" $ purePlot
+    <> layer (band (inline ax) (inline (replicate 10 0.0)) (inline ay) <> alpha 0.5)
+    <> layer (line (inline ax) (inline ay) <> stroke 2)
+    <> title "Area Chart" <> xLabel "t" <> yLabel "v"
+
+  -- == Table-based Plots ===================================================
+  -- Table Heatmap
+  let hx = inlineCat [ c | c <- ["A","B","C"] :: [String], _ <- [1::Int ..3] ]
+      hy = inlineCat (take 9 (cycle (["P","Q","R"] :: [String])))
+      hv = inline [1,5,9, 3,7,2, 8,4,6]
+  out "15-heatmap" $ purePlot
+    <> layer (heatmap hx hy hv)
+    <> title "Table Heatmap" <> xLabel "col" <> yLabel "row"
+
+  -- == Circular Plots ======================================================
+  let pc = inlineCat (["A","B","C","D"] :: [String])
+      pv = inline [30,25,25,20]
+  out "16-pie" $ purePlot
+    <> layer (pie pc pv)
+    <> title "Pie Chart"
+
+  -- Radial Plot (polar bar)
+  out "17-radial" $ purePlot
+    <> layer (bar pc pv <> colorBy pc)
+    <> coordPolar
+    <> title "Radial Plot (polar bar)"
+
+  -- == Advanced Calculations ===============================================
+  -- Linear Regression: plot 側では fit しないので OLS 当てはめ値 yhat を作り line で重ねる
+  -- (データから自動 fit するなら analyze の statLm)。
+  out "18-regression" $
+    let n    = fromIntegral (length sx)
+        a    = (n * sum (zipWith (*) sx sy) - sum sx * sum sy)
+                 / (n * sum (map (^ (2 :: Int)) sx) - sum sx ^ (2 :: Int))
+        b    = (sum sy - a * sum sx) / n
+        yhat = [ a * x + b | x <- sx ]
+    in purePlot
+    <> layer (scatter (inline sx) (inline sy) <> size 6)
+    <> layer (line (inline sx) (inline yhat) <> color (fromHex "#d62728") <> stroke 2)
+    <> title "Linear Regression" <> xLabel "x" <> yLabel "y"
+
+  -- Quantile-Quantile Plot (QQ Plot)
+  out "19-qq" $ purePlot
+    <> layer (qq (inline vals) <> size 6)
+    <> title "Quantile-Quantile Plot (QQ Plot)" <> xLabel "theoretical" <> yLabel "sample"
+
+  -- Parallel Coordinate Plot
+  out "20-parallel" $ purePlot
+    <> layer (parallelCoords [ inline [1,2,3,4], inline [4,3,2,1], inline [2,4,1,3] ])
+    <> title "Parallel Coordinate Plot"
+
+  -- Waterfall Chart of Monthly Profit and Loss
+  out "21-waterfall" $ purePlot
+    <> layer (waterfall (inlineCat (["Begin","Q1","Q2","Q3","Q4","End"] :: [String]))
+                        (inline [1000, 300, -200, 400, -100, 1400]))
+    <> title "Waterfall Chart"
+
+  -- == Error Bars & Error Bands ============================================
+  -- Error Bars Showing Confidence Interval (pointRange = x, y, err)
+  out "22-errorbar" $ purePlot
+    <> layer (pointRange (inline [1.0,2,3,4,5]) (inline [2.0,3.5,3.0,4.2,5.1])
+                         (inline [0.5,0.8,0.4,0.6,0.7]))
+    <> title "Error Bars Showing Confidence Interval" <> xLabel "x" <> yLabel "mean ± CI"
+
+  -- == Box Plots ===========================================================
+  out "23-boxplot" $ purePlot
+    <> layer (boxplot (inline vals))
+    <> title "Box Plot (Tukey 1.5 IQR)" <> yLabel "value"
+
+  -- == Distributions =======================================================
+  let dcat = inlineCat (concatMap (replicate 8) (["A","B","C"] :: [String]))
+      dval = inline ([3,4,4.5,5,5.2,5.5,6,7] ++ [5,5.5,6,6.2,6.8,7,7.5,8] ++ [2,2.5,3,3.2,3.8,4,4.5,5])
+  out "24-violin"   $ purePlot <> layer (violin dval <> groupBy dcat)   <> title "Violin Plot"   <> yLabel "value"
+  out "25-swarm"    $ purePlot <> layer (swarm dval <> groupBy dcat)    <> title "Swarm Plot"    <> yLabel "value"
+  out "26-raincloud"$ purePlot <> layer (raincloud dval <> groupBy dcat)<> title "Raincloud Plot"<> yLabel "value"
+  out "27-ridge"    $ purePlot <> layer (ridge dval <> groupBy dcat)    <> title "Ridgeline Plot"<> yLabel "group"
+
+  -- == Faceting (Trellis) ==================================================
+  let r facetN = case facetN of
+        "x" -> Just (NumData (V.fromList [1,2,3,4, 1,2,3,4, 1,2,3,4]))
+        "y" -> Just (NumData (V.fromList [1,4,9,16, 2,5,8,12, 3,6,9,15]))
+        "g" -> Just (TxtData (V.fromList ["A","A","A","A","B","B","B","B","C","C","C","C"]))
+        _   -> Nothing
+  saveSVGWith "design/vega-lite-gallery/28-trellis-scatter.svg" r $ purePlot
+    <> layer (scatter "x" "y" <> colorBy "g" <> size 6)
+    <> facet "g"
+    <> title "Trellis Scatter Plot (facet)" <> xLabel "x" <> yLabel "y"
+
+  putStrLn "wrote design/vega-lite-gallery/*.svg (28 examples)"
diff --git a/hgg-svg.cabal b/hgg-svg.cabal
new file mode 100644
--- /dev/null
+++ b/hgg-svg.cabal
@@ -0,0 +1,193 @@
+cabal-version:      3.0
+name:               hgg-svg
+version:            0.1.0.0
+extra-doc-files:    CHANGELOG.md
+synopsis:           SVG backend for hgg (pure Haskell)
+description:
+  Browser-independent SVG output backend for hgg, written in pure
+  Haskell. This is the backend most users go through to generate plots.
+license:            BSD-3-Clause
+homepage:           https://github.com/frenzieddoll/hgg
+license-file:       LICENSE
+author:             Toshiaki Honda
+maintainer:         frenzieddoll@gmail.com
+copyright:          2026 Aelysce Project (Toshiaki Honda)
+category:           Graphics
+build-type:         Simple
+
+common warnings
+  ghc-options:        -Wall -Wcompat -Widentities -Wincomplete-record-updates
+                      -Wincomplete-uni-patterns -Wpartial-fields
+                      -Wredundant-constraints
+
+library
+  import:           warnings
+  exposed-modules:  Graphics.Hgg.Backend.SVG
+                    Graphics.Hgg.Quick
+  hs-source-dirs:   src
+  build-depends:    base               >= 4.17 && < 5
+                  , text               >= 2.0  && < 2.2
+                  , vector             >= 0.13 && < 0.14
+                  , hgg-core  ^>= 0.1
+                  , hgg-frame ^>= 0.1
+  default-language: Haskell2010
+
+executable scatter-demo
+  import:           warnings
+  main-is:          ScatterDemo.hs
+  hs-source-dirs:   examples
+  build-depends:    base               >= 4.17 && < 5
+                  , text
+                  , vector
+                  , hgg-core
+                  , hgg-svg
+  default-language: Haskell2010
+
+executable custom-mark-demo
+  import:           warnings
+  main-is:          CustomMarkDemo.hs
+  hs-source-dirs:   examples
+  build-depends:    base               >= 4.17 && < 5
+                  , vector
+                  , hgg-core
+                  , hgg-svg
+  default-language: Haskell2010
+
+executable vega-lite-gallery
+  import:           warnings
+  main-is:          VegaLiteGallery.hs
+  hs-source-dirs:   examples
+  build-depends:    base               >= 4.17 && < 5
+                  , vector
+                  , directory
+                  , hgg-core
+                  , hgg-svg
+  default-language: Haskell2010
+
+executable df-plot-demo
+  import:           warnings
+  main-is:          DfPlotDemo.hs
+  hs-source-dirs:   examples
+  build-depends:    base               >= 4.17 && < 5
+                  , containers
+                  , directory
+                  , text
+                  , vector
+                  , hgg-core
+                  , hgg-frame
+                  , hgg-svg
+  default-language: Haskell2010
+
+executable gallery-demo
+  import:           warnings
+  main-is:          GalleryDemo.hs
+  hs-source-dirs:   examples
+  build-depends:    base               >= 4.17 && < 5
+                  , text
+                  , vector
+                  , directory
+                  , aeson
+                  , bytestring
+                  , hgg-core
+                  , hgg-svg
+  default-language: Haskell2010
+
+executable dag-comparison-demo
+  import:           warnings
+  main-is:          DagComparisonDemo.hs
+  hs-source-dirs:   examples
+  build-depends:    base               >= 4.17 && < 5
+                  , text
+                  , hgg-core
+                  , hgg-svg
+  default-language: Haskell2010
+
+executable dag-parity-bench
+  import:           warnings
+  main-is:          DagParityBench.hs
+  hs-source-dirs:   examples
+  -- Safety net: cap the heap at 2.5G so a runaway case fails with a clean
+  -- GHC RTS "heap overflow" exception instead of exhausting system memory.
+  ghc-options:      -rtsopts "-with-rtsopts=-M2500m"
+  build-depends:    base               >= 4.17 && < 5
+                  , text
+                  , directory
+                  , hgg-core
+                  , hgg-svg
+  default-language: Haskell2010
+
+executable json-dump
+  import:           warnings
+  main-is:          JsonDump.hs
+  hs-source-dirs:   examples
+  build-depends:    base               >= 4.17 && < 5
+                  , bytestring
+                  , aeson
+                  , hgg-core
+  default-language: Haskell2010
+
+-- Runnable examples for the docs/ tutorials (referenced from docs/getting-started.md / api-guide.md)
+executable tutorial-01-easy
+  import:           warnings
+  main-is:          Tutorial01Easy.hs
+  hs-source-dirs:   examples
+  build-depends:    base >= 4.17 && < 5, hgg-core, hgg-svg
+  default-language: Haskell2010
+
+executable tutorial-02-grammar
+  import:           warnings
+  main-is:          Tutorial02Grammar.hs
+  hs-source-dirs:   examples
+  build-depends:    base >= 4.17 && < 5, text, hgg-core, hgg-svg
+  default-language: Haskell2010
+
+executable tutorial-03-overlay
+  import:           warnings
+  main-is:          Tutorial03Overlay.hs
+  hs-source-dirs:   examples
+  build-depends:    base >= 4.17 && < 5, hgg-core, hgg-svg
+  default-language: Haskell2010
+
+executable tutorial-04-distribution
+  import:           warnings
+  main-is:          Tutorial04Distribution.hs
+  hs-source-dirs:   examples
+  build-depends:    base >= 4.17 && < 5, text, hgg-core, hgg-svg
+  default-language: Haskell2010
+
+executable tutorial-05-theme
+  import:           warnings
+  main-is:          Tutorial05Theme.hs
+  hs-source-dirs:   examples
+  build-depends:    base >= 4.17 && < 5, text, hgg-core, hgg-svg
+  default-language: Haskell2010
+
+-- Generates the figures embedded in docs/ (cabal run doc-figures, from the repo root)
+executable doc-figures
+  import:           warnings
+  main-is:          DocFigures.hs
+  hs-source-dirs:   examples
+  other-modules:    DocFig.Common
+                  , DocFig.Quickstart
+                  , DocFig.Layers
+                  , DocFig.EncodingScale
+                  , DocFig.Decoration
+  build-depends:    base >= 4.17 && < 5, text, vector, directory, hgg-core, hgg-svg
+  default-language: Haskell2010
+
+-- Smoke test for BoundPlot (df |>> spec) → SVG. Lives here rather than in
+-- frame:test because cabal would see frame <-> svg as a package cycle.
+test-suite hgg-svg-tests
+  import:           warnings
+  type:             exitcode-stdio-1.0
+  main-is:          Spec.hs
+  hs-source-dirs:   test
+  build-depends:    base               >= 4.17 && < 5
+                  , text
+                  , vector
+                  , containers
+                  , hgg-core
+                  , hgg-frame
+                  , hgg-svg
+                  , hspec      >= 2.10 && < 2.12
+  default-language: Haskell2010
diff --git a/src/Graphics/Hgg/Backend/SVG.hs b/src/Graphics/Hgg/Backend/SVG.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Hgg/Backend/SVG.hs
@@ -0,0 +1,332 @@
+-- |
+-- Module      : Graphics.Hgg.Backend.SVG
+-- Description : SVG backend (Phase 26 §B-1 Resolver 対応版)
+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
+-- License     : BSD-3-Clause
+{-# LANGUAGE OverloadedStrings #-}
+module Graphics.Hgg.Backend.SVG
+  ( -- * 通常 (Resolver 不要 = inline 列のみの図)
+    renderSVG
+  , saveSVG
+    -- * Resolver 同伴 (= 'ColByName' を含む図)
+  , renderSVGWith
+  , saveSVGWith
+  , renderSVGInteractive
+  , saveSVGInteractive
+  , plot
+    -- * Phase 14: BoundPlot (df バインド済) を描画する
+  , renderBound
+  , saveSVGBound
+    -- * Phase 3 A8: Primitive 列を直接 SVG にする helper (= hgg-3d 用)
+  , renderPrimitivesSVG
+  , savePrimitivesSVG
+  ) where
+
+import           Graphics.Hgg.Frame    (BoundPlot (..))
+import           Graphics.Hgg.Layout   (Layout (..), Rect (..),
+                                        ViewportSize (..), computeLayout)
+import           Graphics.Hgg.Render   (FillStyle (..), LineStyle (..),
+                                        PathSegment (..), Point (..),
+                                        Primitive (..), StrokeStyle (..),
+                                        TextAnchor (..), TextStyle (..),
+                                        renderToPrimitives, scalePrimitives)
+import           Graphics.Hgg.Spec     (MarkKind (..), Resolver, VisualSpec (..),
+                                        emptyResolver, lyKind, vsDpi)
+import           Data.Monoid           (getFirst, getLast)
+import           Graphics.Hgg.Validate (PlotDiagnostic, Severity (..),
+                                        diagnosticSeverity, renderDiagnostic)
+import           Data.Text             (Text)
+import qualified Data.Text             as T
+import qualified Data.Text.IO          as TIO
+import           System.IO             (hPutStrLn, stderr)
+
+-- | 'Resolver' を渡して 'VisualSpec' を SVG text に。 'ColByName' を含む図用。
+renderSVGWith :: Resolver -> VisualSpec -> Text
+renderSVGWith r spec =
+  let layout     = computeLayout r spec
+      -- ★ Phase 33 B5: layout/primitive は純 pt。SVG (raster 想定) は dpi/72 を
+      --   一度だけ掛けて device px へ (唯一の dpi 適用点)。既定 96dpi → k=4/3。
+      k          = maybe 96 id (getLast (vsDpi spec)) / 72
+      primitives = scalePrimitives k (renderToPrimitives r layout spec)
+      ViewportSize wpt hpt = lpViewport layout
+      w = round (fromIntegral wpt * k) :: Int
+      h = round (fromIntegral hpt * k) :: Int
+      header = T.concat
+        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" "
+        , "width=\"",  num w, "\" "
+        , "height=\"", num h, "\" "
+        , "viewBox=\"0 0 ", num w, " ", num h, "\">"
+        ]
+  in T.concat (header : primsToSvg primitives : ["</svg>"])
+
+-- | render 'VisualSpec' to SVG text。 Resolver 不要 (= 全 ColRef が inline、
+-- 'ColByName' が無い図で使う、 = 通常)。 列名参照を含む図は 'renderSVGWith'。
+renderSVG :: VisualSpec -> Text
+renderSVG = renderSVGWith emptyResolver
+
+-- | 'Resolver' を渡して SVG ファイルに保存。 'ColByName' を含む図用。
+saveSVGWith :: FilePath -> Resolver -> VisualSpec -> IO ()
+saveSVGWith path r spec = TIO.writeFile path (renderSVGWith r spec)
+
+-- | SVG ファイルに保存。 Resolver 不要 (= inline 列のみの図、 = 通常)。
+-- 列名参照を含む図は 'saveSVGWith'、 DataFrame は 'saveSVGBound' (@df |>> spec@)。
+saveSVG :: FilePath -> VisualSpec -> IO ()
+saveSVG path = saveSVGWith path emptyResolver
+
+-- | 'purePlot' (= 純粋値) と対をなす副作用関数。 中身は 'saveSVG' の alias、
+-- SVG backend が default。 他 backend (PDF / PNG) を使う場合はそれぞれの
+-- module の `plot` を import する。
+--
+-- > main = plot "out.svg" $ purePlot <> layer (scatter ...) <> title "..."
+plot :: FilePath -> VisualSpec -> IO ()
+plot = saveSVG
+
+-- | Phase 14: 'BoundPlot' (= @df |>> spec@ の結果) を SVG text に。
+-- 'renderSVGWith' を 'bpResolver' / 'bpSpec' で呼ぶ薄いラッパ。
+-- 検証診断 ('bpDiagnostics') は副作用が無いここでは無視する
+-- (報告は 'saveSVGBound' / 利用者が 'bpDiagnostics' を直接見る)。
+renderBound :: BoundPlot -> Text
+renderBound (BoundPlot r spec _) = renderSVGWith r spec
+
+-- | Phase 14: 'BoundPlot' を SVG ファイルに保存。
+-- 'bpDiagnostics' に Error severity があれば **stderr に報告**してから書き出す
+-- (描画自体は止めない = 純値 '(|>>)' の lenient 既定。 無検証で通したい場合は
+-- 'unBound' → 'saveSVGWith' を直接使う)。
+saveSVGBound :: FilePath -> BoundPlot -> IO ()
+saveSVGBound path bp@(BoundPlot _ spec diags) = do
+  reportErrors diags
+  warnUnresolvedStats spec
+  TIO.writeFile path (renderBound bp)
+
+-- | Phase 16 footgun 緩和: spec に未解決 stat layer (@MStatLM@/@MStatSmooth@) が残っていたら
+-- stderr に警告 (描画では skip され回帰線が出ない)。 回帰を描くには analyze-bridge の
+-- @saveSVGBoundStats@ / @resolveStats@ を使う。 stat を使わない通常図では何もしない。
+warnUnresolvedStats :: VisualSpec -> IO ()
+warnUnresolvedStats spec
+  | any isStat (vsLayers spec) =
+      hPutStrLn stderr
+        "[hgg] 警告: 未解決の stat layer (statLm/statSmooth) があります。 \
+        \回帰線は描画されません。 analyze-bridge の saveSVGBoundStats で描画してください。"
+  | otherwise = pure ()
+  where
+    isStat ly = case getFirst (lyKind ly) of
+      Just MStatLM     -> True
+      Just MStatSmooth -> True
+      _                -> False
+
+-- | Error severity の診断のみ stderr に出す。
+reportErrors :: [PlotDiagnostic] -> IO ()
+reportErrors diags =
+  mapM_ (hPutStrLn stderr . T.unpack . renderDiagnostic)
+        (filter ((== SevError) . diagnosticSeverity) diags)
+
+-- | Interactive 版: hover tooltip (= 標準 native) に加えて、
+-- ドラッグで pan / wheel で zoom できる inline JS を末尾に embed。
+-- ブラウザで開いた時だけ動作、 raw SVG viewer では普通に静止画。
+renderSVGInteractive :: Resolver -> VisualSpec -> Text
+renderSVGInteractive r spec =
+  let base = renderSVGWith r spec
+      -- </svg> 直前に <script> を挿入
+      (pre, post) = T.breakOn "</svg>" base
+  in T.concat [pre, panZoomScript, post]
+
+saveSVGInteractive :: FilePath -> Resolver -> VisualSpec -> IO ()
+saveSVGInteractive path r spec = TIO.writeFile path (renderSVGInteractive r spec)
+
+-- | Phase 3 A8: '[Primitive]' を直接 SVG にする helper。
+-- 'renderSVG' は VisualSpec 経由だが、 hgg-3d のように外部で
+-- Primitive 列を生成済の場合に使う。 既存の 'primToSvg' converter をそのまま流用、
+-- 出力 SVG 構造 (= header + light bg + title + body) は 'renderSVG' と同形式。
+renderPrimitivesSVG :: Int -> Int -> Text -> [Primitive] -> Text
+renderPrimitivesSVG w h titleTxt prims =
+  let header = T.concat
+        [ "<svg xmlns=\"http://www.w3.org/2000/svg\" "
+        , "width=\"",  num w, "\" "
+        , "height=\"", num h, "\" "
+        , "viewBox=\"0 0 ", num w, " ", num h, "\">"
+        ]
+      -- bg + title
+      bg = T.concat
+        [ "<rect x=\"0\" y=\"0\" width=\"", num w
+        , "\" height=\"", num h
+        , "\" fill=\"#fafafa\"/>" ]
+      -- ★ Phase 43 既定に合わせタイトルは左寄せ (ggplot theme_grey)。 3D backend は
+      --   plotArea を持たないので左 margin は固定 20px。 2D の plot.title 左寄せと統一。
+      title_ = if T.null titleTxt then "" else T.concat
+        [ "<text x=\"20\" y=\"30\""
+        , " text-anchor=\"start\" font-family=\"sans-serif\""
+        , " font-size=\"16\" fill=\"#333\">", titleTxt, "</text>" ]
+  in T.concat (header : bg : title_ : primsToSvg prims : ["</svg>"])
+
+-- | 'renderPrimitivesSVG' をファイル書出し版。
+savePrimitivesSVG :: FilePath -> Int -> Int -> Text -> [Primitive] -> IO ()
+savePrimitivesSVG path w h t prims =
+  TIO.writeFile path (renderPrimitivesSVG w h t prims)
+
+-- | pan / zoom inline JS。 SVG の viewBox を操作するだけの最小実装。
+panZoomScript :: Text
+panZoomScript = T.concat
+  [ "<script type=\"application/ecmascript\"><![CDATA[\n"
+  , "(function(){\n"
+  , "  var svg = document.currentScript.parentNode;\n"
+  , "  var vb = svg.viewBox.baseVal;\n"
+  , "  var dragging = false, sx = 0, sy = 0, vx0 = 0, vy0 = 0;\n"
+  , "  svg.addEventListener('mousedown', function(e){\n"
+  , "    dragging = true; sx = e.clientX; sy = e.clientY;\n"
+  , "    vx0 = vb.x; vy0 = vb.y;\n"
+  , "    svg.style.cursor = 'grabbing';\n"
+  , "  });\n"
+  , "  window.addEventListener('mouseup', function(){\n"
+  , "    dragging = false; svg.style.cursor = 'default';\n"
+  , "  });\n"
+  , "  svg.addEventListener('mousemove', function(e){\n"
+  , "    if (!dragging) return;\n"
+  , "    var dx = (e.clientX - sx) * vb.width  / svg.clientWidth;\n"
+  , "    var dy = (e.clientY - sy) * vb.height / svg.clientHeight;\n"
+  , "    vb.x = vx0 - dx; vb.y = vy0 - dy;\n"
+  , "  });\n"
+  , "  svg.addEventListener('wheel', function(e){\n"
+  , "    e.preventDefault();\n"
+  , "    var scale = e.deltaY > 0 ? 1.1 : 1/1.1;\n"
+  , "    var rect = svg.getBoundingClientRect();\n"
+  , "    var mx = vb.x + (e.clientX - rect.left) * vb.width  / rect.width;\n"
+  , "    var my = vb.y + (e.clientY - rect.top)  * vb.height / rect.height;\n"
+  , "    vb.x = mx - (mx - vb.x) * scale;\n"
+  , "    vb.y = my - (my - vb.y) * scale;\n"
+  , "    vb.width  *= scale; vb.height *= scale;\n"
+  , "  });\n"
+  , "})();\n"
+  , "]]></script>"
+  ]
+
+-- ---------------------------------------------------------------------------
+-- Primitive → SVG element
+-- ---------------------------------------------------------------------------
+
+-- | Phase 11 A7-a: clip stack を解決して SVG body に変換。 'PClipPush' で
+--   @\<clipPath\>@ + @\<g clip-path\>@ を開き、 'PClipPop' で @\</g\>@ を閉じる。
+--   clip プリミティブが無い列では @map primToSvg@ と完全同一出力なので既存 SVG ゼロ diff。
+primsToSvg :: [Primitive] -> Text
+primsToSvg = T.concat . go (0 :: Int)
+  where
+    go _ [] = []
+    go n (PClipPush (Rect x y w h) : rest) =
+      let cid  = T.concat ["clip", num n]
+          open = T.concat
+            [ "<clipPath id=\"", cid, "\"><rect x=\"", numD x, "\" y=\"", numD y
+            , "\" width=\"", numD w, "\" height=\"", numD h, "\"/></clipPath>"
+            , "<g clip-path=\"url(#", cid, ")\">" ]
+      in open : go (n + 1) rest
+    go n (PClipPop : rest) = "</g>" : go n rest
+    go n (p : rest)        = primToSvg p : go n rest
+
+primToSvg :: Primitive -> Text
+primToSvg p = case p of
+  PLine (Point x1 y1) (Point x2 y2) (LineStyle c w d) ->
+    T.concat
+      [ "<line x1=\"", numD x1, "\" y1=\"", numD y1
+      , "\" x2=\"", numD x2, "\" y2=\"", numD y2
+      , "\" stroke=\"", c, "\" stroke-width=\"", numD w, "\""
+      , dashAttr d, "/>"
+      ]
+  PRect (Rect x y w h) (FillStyle fc fo) mStroke ->
+    T.concat
+      [ "<rect x=\"", numD x, "\" y=\"", numD y
+      , "\" width=\"", numD w, "\" height=\"", numD h
+      , "\" fill=\"", fc, "\" fill-opacity=\"", numD fo, "\""
+      , strokeAttr mStroke
+      , "/>"
+      ]
+  PCircle (Point cx cy) r (FillStyle fc fo) mStroke mTitle ->
+    case mTitle of
+      Nothing ->
+        T.concat
+          [ "<circle cx=\"", numD cx, "\" cy=\"", numD cy
+          , "\" r=\"", numD r
+          , "\" fill=\"", fc, "\" fill-opacity=\"", numD fo, "\""
+          , strokeAttr mStroke
+          , "/>"
+          ]
+      Just t ->
+        -- <circle><title>label</title></circle> でブラウザ native hover tooltip
+        T.concat
+          [ "<circle cx=\"", numD cx, "\" cy=\"", numD cy
+          , "\" r=\"", numD r
+          , "\" fill=\"", fc, "\" fill-opacity=\"", numD fo, "\""
+          , strokeAttr mStroke
+          , "><title>", escapeXml t, "</title></circle>"
+          ]
+  PText (Point x y) s (TextStyle c sz fam anchor rot weight italic) ->
+    let anchorAttr = case anchor of
+          AnchorStart  -> "start"
+          AnchorMiddle -> "middle"
+          AnchorEnd    -> "end"
+        -- Phase 50 A1: 内部 'tsRotate' は **CCW 正** (canonical)。 SVG rotate() は y-down で
+        --   CW 正なので、 ここで **符号反転** して device の CW へ変換する (唯一の変換点)。
+        rotAttr = if rot == 0
+          then ""
+          else T.concat [" transform=\"rotate(", numD (negate rot)
+                        , " ", numD x, " ", numD y, ")\""]
+        -- TODO-10 (2026-05-29): font-weight / font-style emit (default 値は省略)
+        weightAttr = if weight == "normal" || T.null weight
+          then ""
+          else T.concat [" font-weight=\"", weight, "\""]
+        italicAttr = if italic then " font-style=\"italic\"" else ""
+    in T.concat
+         [ "<text x=\"", numD x, "\" y=\"", numD y
+         , "\" fill=\"", c, "\" font-size=\"", numD sz
+         , "\" font-family=\"", fam
+         , "\" text-anchor=\"", anchorAttr, "\""
+         , weightAttr, italicAttr
+         , rotAttr
+         , ">"
+         , escapeXml s
+         , "</text>"
+         ]
+  PPath segs (FillStyle fc fo) mStroke ->
+    T.concat
+      [ "<path d=\"", pathSegs segs
+      , "\" fill=\"", fc, "\" fill-opacity=\"", numD fo, "\""
+      , strokeAttr mStroke
+      , "/>"
+      ]
+  PClipPush{}      -> ""
+  PClipPop         -> ""
+  PTransformPush{} -> ""
+  PTransformPop    -> ""
+
+pathSegs :: [PathSegment] -> Text
+pathSegs = T.intercalate " " . map seg
+  where
+    seg (MoveTo (Point x y))  = T.concat ["M ", numD x, " ", numD y]
+    seg (LineTo (Point x y))  = T.concat ["L ", numD x, " ", numD y]
+    seg (CurveTo (Point cx1 cy1) (Point cx2 cy2) (Point x y)) = T.concat
+      ["C ", numD cx1, " ", numD cy1, " ", numD cx2, " ", numD cy2, " ", numD x, " ", numD y]
+    seg ClosePath = "Z"
+
+strokeAttr :: Maybe StrokeStyle -> Text
+strokeAttr Nothing                  = " stroke=\"none\""
+strokeAttr (Just (StrokeStyle c w)) =
+  T.concat [" stroke=\"", c, "\" stroke-width=\"", numD w, "\""]
+
+-- | Phase 11 A4-b: stroke-dasharray 属性。 空配列 (= 実線) は attr を出さない
+--   (= 既存 SVG ゼロ diff の要)。 非空のみ \" stroke-dasharray=\\\"a,b,..\\\"\" を付す。
+dashAttr :: [Double] -> Text
+dashAttr [] = ""
+dashAttr ds = T.concat [" stroke-dasharray=\"", T.intercalate "," (map numD ds), "\""]
+
+num :: Int -> Text
+num = T.pack . show
+
+numD :: Double -> Text
+numD = T.pack . show
+
+escapeXml :: Text -> Text
+escapeXml = T.concatMap esc
+  where
+    esc '<'  = "&lt;"
+    esc '>'  = "&gt;"
+    esc '&'  = "&amp;"
+    esc '"'  = "&quot;"
+    esc '\'' = "&apos;"
+    esc c    = T.singleton c
diff --git a/src/Graphics/Hgg/Quick.hs b/src/Graphics/Hgg/Quick.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Hgg/Quick.hs
@@ -0,0 +1,56 @@
+-- |
+-- Module      : Graphics.Hgg.Quick
+-- Description : Easy 層の IO ワンショット保存 (= 1 行で SVG 出力、 Phase 11 A3)
+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
+-- License     : BSD-3-Clause
+--
+-- core は backend 非依存 (architecture §3.3) のため IO 保存ヘルパは backend 側
+-- (本 module) に置く。 値を渡すだけで SVG が 1 ファイル出る入門用 API。
+--
+-- @
+-- import Graphics.Hgg.Quick
+--
+-- main :: IO ()
+-- main = do
+--   quickScatter "scatter.svg" [1,2,3,4] [1,4,9,16]
+--   quickPlot    "overlay.svg" [ points [1,2,3] [1,4,9]
+--                              , lineXY [1,2,3] [1,4,9] ]
+-- @
+--
+-- `Graphics.Hgg.Easy` を再 export するので、 本 module 1 つの import で
+-- `points` / `lineXY` / `overlay` 等の Easy ヘルパも揃う。
+{-# LANGUAGE OverloadedStrings #-}
+module Graphics.Hgg.Quick
+  ( -- * Easy 層 (再 export)
+    module Graphics.Hgg.Easy
+    -- * IO ワンショット保存 (= 値直接受け)
+  , quickScatter
+  , quickLine
+  , quickBar
+  , quickHist
+    -- * layer 群をまとめて保存
+  , quickPlot
+  ) where
+
+import           Graphics.Hgg.Backend.SVG (saveSVG)
+import           Graphics.Hgg.Easy
+
+-- | 散布図を 1 行で SVG 保存。 @quickScatter path xs ys@。
+quickScatter :: FilePath -> [Double] -> [Double] -> IO ()
+quickScatter path xs ys = quickPlot path [points xs ys]
+
+-- | 折れ線を 1 行で SVG 保存。
+quickLine :: FilePath -> [Double] -> [Double] -> IO ()
+quickLine path xs ys = quickPlot path [lineXY xs ys]
+
+-- | 棒グラフを 1 行で SVG 保存。
+quickBar :: FilePath -> [Double] -> [Double] -> IO ()
+quickBar path xs ys = quickPlot path [bars xs ys]
+
+-- | ヒストグラムを 1 行で SVG 保存。
+quickHist :: FilePath -> [Double] -> IO ()
+quickHist path xs = quickPlot path [hist xs]
+
+-- | layer 群を 'overlay' で重畳して SVG 保存 (= 最も汎用な Easy 保存)。
+quickPlot :: FilePath -> [Layer] -> IO ()
+quickPlot path = saveSVG path . overlay
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,35 @@
+-- | hgg-svg テスト。 Phase 14 A3 = BoundPlot (df |>> spec) → SVG smoke。
+{-# LANGUAGE OverloadedStrings #-}
+module Main (main) where
+
+import           Graphics.Hgg.Backend.SVG (renderBound)
+import           Graphics.Hgg.Frame       ((|>>))
+import           Graphics.Hgg.Spec        (ColData (..), layer, scatter)
+import           Data.Map.Strict          (Map)
+import qualified Data.Map.Strict          as M
+import           Data.Text                (Text)
+import qualified Data.Text                as T
+import qualified Data.Vector              as V
+import           Test.Hspec
+
+-- 列順違いで同一データ (Map vs assoc-list)
+xyAssoc :: [(Text, ColData)]
+xyAssoc =
+  [ ("x", NumData (V.fromList [1, 2, 3]))
+  , ("y", NumData (V.fromList [4, 5, 6]))
+  ]
+
+xyMap :: Map Text ColData
+xyMap = M.fromList xyAssoc
+
+main :: IO ()
+main = hspec $
+  describe "renderBound (df |>> spec)" $ do
+    it "df |>> spec が SVG を出す" $ do
+      let svg = renderBound (xyMap |>> layer (scatter "x" "y"))
+      ("<svg"   `T.isInfixOf` svg) `shouldBe` True
+      ("</svg>" `T.isInfixOf` svg) `shouldBe` True
+    it "Map と assoc-list で同一 SVG (同データ、 spec-2 §7)" $
+      renderBound (xyMap   |>> layer (scatter "x" "y"))
+        `shouldBe`
+      renderBound (xyAssoc |>> layer (scatter "x" "y"))
