forma-arch 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,285 +1,600 @@
1
- <!doctype html>
2
- <html lang="it">
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>C4 Hologram</title>
6
+ <title>Forma · C4 Hologram</title>
7
7
  <style>
8
- :root{color-scheme:dark;
9
- --bg:#060a10;--ink:#c8d6e6;--dim:#6f8098;--faint:#3a4a63;
10
- --mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,"Liberation Mono",monospace;
11
- --sans:Inter,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;--font:var(--mono);--nodebg:rgba(10,18,28,.72);--nm:#eaf3fb;
12
- --done:#37d07f;--prog:#e6a545;--next:#4fc8d6;--plan:#7f8ea8;--prob:#f2565b;
13
- --line:rgba(79,200,214,.12)}
14
- *{box-sizing:border-box}
15
- html,body{margin:0;height:100%;background:radial-gradient(1200px 700px at 50% -10%,#0a1420,var(--bg) 60%),var(--bg);
16
- color:var(--ink);font-family:var(--font);font-size:14px;overflow:hidden}
17
- body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;opacity:.5;
18
- background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);
19
- background-size:46px 46px;mask:radial-gradient(1100px 640px at 50% 42%,#000 34%,transparent 82%)}
20
- .app{position:relative;z-index:2;display:flex;flex-direction:column;height:100%;padding:14px 22px 0}
21
- .top{display:flex;align-items:flex-start;gap:14px}
22
- h1{margin:0;font-size:22px;letter-spacing:.16em;color:#7fe9f0;text-shadow:0 0 18px rgba(79,200,214,.5);font-weight:700}
23
- .sub{color:var(--dim);font-size:12px;margin-top:4px;letter-spacing:.03em}
24
- .crumb{margin-top:5px;font-size:12px;color:var(--dim);letter-spacing:.05em}
25
- .crumb b{color:#7fe9f0}.crumb a{color:var(--next);cursor:pointer}.crumb .sep{color:var(--faint)}
26
- .prov{margin-top:4px;font-size:11px;color:var(--faint)}
27
- .prov.warn{color:var(--prog)}
28
- .btns{margin-left:auto;display:flex;gap:9px;flex-shrink:0}
29
- .btn{background:transparent;color:var(--dim);border:1px solid rgba(79,200,214,.28);border-radius:4px;
30
- padding:8px 13px;font-family:var(--mono);font-size:12px;letter-spacing:.1em;text-transform:uppercase;cursor:pointer;transition:.15s}
31
- .btn:hover{color:#eafaff;border-color:rgba(79,200,214,.6)}
32
- .btn.on{color:#eafcff;border-color:var(--next);box-shadow:0 0 14px rgba(79,200,214,.35) inset,0 0 10px rgba(79,200,214,.2)}
33
- .btn:disabled{opacity:.3;cursor:default}
34
- select.btn.sel{-webkit-appearance:none;appearance:none;padding-right:26px;
35
- background-image:linear-gradient(45deg,transparent 50%,var(--dim) 50%),linear-gradient(135deg,var(--dim) 50%,transparent 50%);
36
- background-position:calc(100% - 13px) 52%,calc(100% - 8px) 52%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
37
- select.btn.sel option{background:#0a121c;color:var(--ink);text-transform:none;letter-spacing:normal}
38
- html[data-skin="blueprint"] select.btn.sel option{background:#fff;color:var(--ink)}
39
- .stage-wrap{flex:1;position:relative;margin:12px 0;border:1px solid rgba(79,200,214,.22);border-radius:16px;
40
- box-shadow:0 0 40px rgba(79,200,214,.06) inset,0 0 30px rgba(0,0,0,.5);overflow:hidden}
41
- .stage{position:absolute;inset:24px;will-change:transform,opacity}
42
- svg.edges{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
43
- .node{position:absolute;transform:translate(-50%,-50%) scale(.94);opacity:0;min-width:210px;max-width:270px;padding:12px 14px 26px;
44
- border-radius:10px;cursor:pointer;background:var(--nodebg);border:1.5px solid var(--nc,var(--plan));
45
- box-shadow:0 0 0 1px rgba(0,0,0,.4),0 8px 26px rgba(0,0,0,.5),0 0 22px var(--ng,transparent);
46
- transition:opacity .4s,transform .4s cubic-bezier(.2,.8,.2,1),box-shadow .18s;backdrop-filter:blur(3px)}
47
- .node.in{opacity:1;transform:translate(-50%,-50%) scale(1)}
48
- .node.in:hover{transform:translate(-50%,-50%) scale(1.03)}
49
- .node.dashed{border-style:dashed;background:rgba(20,26,38,.5)}
50
- .node.sel{box-shadow:0 0 0 1px var(--nc) inset,0 8px 30px rgba(0,0,0,.55),0 0 34px var(--ng)}
51
- .node .top2{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
52
- .node .cat{font-size:10px;letter-spacing:.08em;color:var(--dim)}
53
- .node .pct{font-size:11px;font-weight:700;color:var(--nc)}
54
- .node .nm{font-size:15px;font-weight:700;color:var(--nm);margin-top:5px;letter-spacing:.01em}
55
- .node .ds{font-size:12px;color:var(--dim);margin-top:6px;line-height:1.5}
56
- .node .drill{position:absolute;right:12px;bottom:8px;font-size:11px;letter-spacing:.08em;color:var(--next)}
57
- .grid{position:absolute;inset:34px 26px 14px;overflow:auto;display:grid;gap:12px;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));align-content:start}
58
- .cell{opacity:0;transform:translateY(8px);transition:.35s;padding:11px 13px 22px;border-radius:9px;position:relative;cursor:pointer;
59
- background:var(--nodebg);border:1.4px solid var(--nc,var(--plan));box-shadow:0 0 14px var(--ng,transparent)}
60
- .cell.in{opacity:1;transform:none}.cell.dashed{border-style:dashed}
61
- .cell.sel{box-shadow:0 0 0 1px var(--nc) inset,0 0 20px var(--ng)}
62
- .cell .top2{display:flex;justify-content:space-between;gap:6px}.cell .cat{font-size:9.5px;color:var(--dim)}
63
- .cell .pct{font-size:10.5px;font-weight:700;color:var(--nc)}.cell .nm{font-size:13px;font-weight:700;color:var(--nm);margin-top:3px}
64
- .cell .ds{font-size:11px;color:var(--dim);margin-top:4px;line-height:1.45}.cell .drill{position:absolute;right:11px;bottom:7px;font-size:10px;color:var(--next)}
65
- .elabel{fill:#9fc4cf;font-size:10.5px;font-family:var(--mono)}
66
- .epill{fill:rgba(6,10,16,.85)}
67
- @keyframes flow{to{stroke-dashoffset:-30}}
68
- .eflow{fill:none;stroke:var(--next);stroke-width:1.3;opacity:.6;stroke-dasharray:2 8;animation:flow 1.1s linear infinite;filter:drop-shadow(0 0 3px var(--next))}
69
- .legend{display:flex;flex-wrap:wrap;gap:16px;align-items:center;padding:9px 2px 12px;font-size:11.5px;color:var(--dim)}
70
- .legend b{display:inline-flex;align-items:center;gap:6px;font-weight:400}
71
- .legend i{width:9px;height:9px;border-radius:2px;display:inline-block}
72
- .dot-done{background:var(--done);box-shadow:0 0 7px var(--done)}.dot-prog{background:var(--prog);box-shadow:0 0 7px var(--prog)}
73
- .dot-next{background:var(--next);box-shadow:0 0 7px var(--next)}.dot-plan{border:1px dashed var(--plan)}.dot-prob{background:var(--prob);box-shadow:0 0 7px var(--prob)}
74
- .panel{position:absolute;left:0;right:0;bottom:0;transform:translateY(101%);transition:transform .32s cubic-bezier(.2,.8,.2,1);
75
- background:rgba(8,13,20,.97);border-top:1px solid var(--nc,rgba(79,200,214,.4));padding:18px 24px 22px;z-index:9;max-height:46%;overflow:auto;box-shadow:0 -14px 40px rgba(0,0,0,.6)}
76
- .panel.open{transform:translateY(0)}
77
- .panel .ph{display:flex;justify-content:space-between;align-items:baseline}
78
- .panel h3{margin:0;font-size:18px;color:var(--nc);font-weight:700;letter-spacing:.02em}
79
- .panel .close{color:var(--dim);cursor:pointer;font-size:12px}
80
- .panel .lab{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-top:14px}
81
- .panel p{margin:5px 0 0;font-size:13.5px;line-height:1.55;color:#dbe7f4}
82
- .panel a{color:var(--next);text-decoration:none}.panel a:hover{text-shadow:0 0 8px rgba(79,200,214,.6)}
83
- .panel code{color:#bfe4ea}
84
- .toast{position:fixed;bottom:18px;left:50%;transform:translateX(-50%);background:rgba(8,13,20,.96);border:1px solid var(--next);
85
- color:#eafcff;padding:9px 15px;border-radius:5px;font-size:12px;z-index:20;opacity:0;transition:.3s}.toast.show{opacity:1}
86
- /* ── cyberpunk pass ────────────────────────────────────────── */
87
- :root{--mag:#ff5ad6}
88
- body::before{animation:griddrift 42s linear infinite}
89
- @keyframes griddrift{to{background-position:46px 46px}}
90
- body::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:1;opacity:.05;
91
- background:repeating-linear-gradient(0deg,#7fe9f0 0,#7fe9f0 1px,transparent 1px,transparent 3px)}
92
- .app::after{content:"";position:fixed;inset:0;pointer-events:none;z-index:1;
93
- background:radial-gradient(120% 90% at 50% 40%,transparent 62%,rgba(0,0,0,.55) 100%)}
94
- h1{position:relative;text-shadow:0 0 12px rgba(79,200,214,.75),2px 0 0 rgba(255,90,214,.4),-2px 0 0 rgba(79,200,214,.4)}
95
- h1::after{content:"_";margin-left:2px;color:#7fe9f0;animation:blink 1.1s step-end infinite}
96
- @keyframes blink{50%{opacity:0}}
97
- .stage-wrap{border-color:rgba(79,200,214,.42);box-shadow:0 0 60px rgba(79,200,214,.1) inset,0 0 30px rgba(0,0,0,.5),0 0 26px rgba(79,200,214,.14)}
98
- .node::before,.node::after{content:"";position:absolute;width:13px;height:13px;pointer-events:none;border:2px solid var(--nc);opacity:.9}
99
- .node::before{top:-1px;left:-1px;border-right:0;border-bottom:0}
100
- .node::after{bottom:-1px;right:-1px;border-left:0;border-top:0}
101
- .cell::before,.cell::after{content:"";position:absolute;width:10px;height:10px;pointer-events:none;border:1.5px solid var(--nc);opacity:.85}
102
- .cell::before{top:-1px;left:-1px;border-right:0;border-bottom:0}
103
- .cell::after{bottom:-1px;right:-1px;border-left:0;border-top:0}
104
- .node .nm{text-shadow:0 0 9px var(--ng)}.node .pct,.cell .pct{text-shadow:0 0 8px var(--nc)}
105
- .node .drill,.cell .drill{text-shadow:0 0 8px var(--next)}
106
- .node.in:hover{animation:flick .22s steps(2,end) 1}
107
- @keyframes flick{50%{opacity:.72;filter:brightness(1.4)}}
108
- .node.sel,.cell.sel{animation:selpulse 1.5s ease-in-out infinite}
109
- @keyframes selpulse{50%{box-shadow:0 0 0 1px var(--nc) inset,0 0 48px var(--ng)}}
110
- .eflow{stroke-width:1.7;opacity:.85}
111
- .elabel{fill:#bfeaf0}
112
- .btn.on{text-shadow:0 0 9px rgba(79,200,214,.65)}
113
- #reverify:hover{color:var(--mag);border-color:var(--mag);text-shadow:0 0 9px rgba(255,90,214,.6)}
114
- .legend i{box-shadow:0 0 8px currentColor}
115
- h3{text-shadow:0 0 7px var(--nc)}
116
- .panel{box-shadow:0 -16px 40px rgba(0,0,0,.72);border-top-width:2px}
117
- .panel .lab{color:var(--dim)}
118
- /* ── SKIN: blueprint (clean, light, no decor) — vince sul blocco cyberpunk ── */
119
- html[data-skin="blueprint"]{color-scheme:light;
120
- --bg:#eef3f9;--ink:#26374a;--dim:#5a6b7d;--faint:#93a3b4;--font:var(--sans);
121
- --done:#1f9d57;--prog:#c07a12;--next:#1f77b4;--plan:#8592a3;--prob:#d1453b;
122
- --line:rgba(31,119,180,.14);--nodebg:#ffffff;--nm:#16273a;--edge:#8aa0b4;--mag:#b5259e}
123
- html[data-skin="blueprint"] body{background:radial-gradient(1100px 640px at 50% -8%,#ffffff,var(--bg) 62%),var(--bg)}
124
- html[data-skin="blueprint"] body::before{animation:none;opacity:.7}
125
- html[data-skin="blueprint"] body::after,html[data-skin="blueprint"] .app::after{display:none}
126
- html[data-skin="blueprint"] h1{color:#123651;text-shadow:none}
127
- html[data-skin="blueprint"] h1::after{content:""}
128
- html[data-skin="blueprint"] .crumb b{color:#1f77b4}
129
- html[data-skin="blueprint"] .node,html[data-skin="blueprint"] .cell{backdrop-filter:none;box-shadow:0 1px 3px rgba(20,40,70,.12),0 0 0 1px rgba(20,40,70,.05)}
130
- html[data-skin="blueprint"] .node::before,html[data-skin="blueprint"] .node::after,html[data-skin="blueprint"] .cell::before,html[data-skin="blueprint"] .cell::after{display:none}
131
- html[data-skin="blueprint"] .node.dashed{background:#f4f7fb}
132
- html[data-skin="blueprint"] .node .nm,html[data-skin="blueprint"] .cell .nm,html[data-skin="blueprint"] h3{text-shadow:none}
133
- html[data-skin="blueprint"] .node.sel,html[data-skin="blueprint"] .cell.sel{animation:none;box-shadow:0 0 0 2px var(--nc) inset,0 2px 10px rgba(20,40,70,.18)}
134
- html[data-skin="blueprint"] .stage-wrap{border-color:rgba(31,119,180,.25);box-shadow:0 1px 8px rgba(20,40,70,.08)}
135
- html[data-skin="blueprint"] .panel{background:#ffffff;box-shadow:0 -8px 26px rgba(20,40,70,.14)}
136
- html[data-skin="blueprint"] .panel p{color:#2b3c4e}html[data-skin="blueprint"] .panel code{color:#1f77b4}
137
- html[data-skin="blueprint"] .epill{fill:#ffffff}html[data-skin="blueprint"] .elabel{fill:#48596b}
138
- html[data-skin="blueprint"] .eflow{opacity:.45;filter:none}
139
- html[data-skin="blueprint"] .btn{border-color:rgba(31,119,180,.35)}
140
- html[data-skin="blueprint"] .btn:hover{color:#0c2c46}
141
- html[data-skin="blueprint"] .btn.on{text-shadow:none;box-shadow:0 0 0 1px var(--next) inset}
142
- html[data-skin="blueprint"] .legend i{box-shadow:none}
143
- html[data-skin="blueprint"] .toast{background:#ffffff;color:#16273a}
8
+ :root{color-scheme:light;
9
+ --bg:#050d14; --grid:rgba(62,231,255,.04); --halo:rgba(62,231,255,.10);
10
+ --cyan:#3ee7ff; --sec:#6fa8bc; --ter:#4c7f94; --ter2:#41738a; --nm:#bfe9f7;
11
+ --stagebg:rgba(4,12,19,.72); --stageborder:#143547; --nodebg:rgba(8,26,38,.92); --detailbg:rgba(6,20,30,.97);
12
+ --border:#1e4a5e; --done:#2fe08a; --prog:#ffc74d; --next:#3ee7ff; --plan:#2d5a72; --planTxt:#7fb3c6; --prob:#ff5d5d;
13
+ --edge:#2b6e8a; --edgeplan:#264d61; --elbl:#3d7f9c; --ddtxt:#7fb3c6; --now:#cfeefb; --tgt:#8fd8ec; --src:#4fbf8d;}
14
+ *{box-sizing:border-box;margin:0;padding:0}
15
+ body{font-family:"SF Mono",Menlo,Consolas,ui-monospace,monospace;background:var(--bg);color:var(--nm);overflow-x:hidden}
16
+ #wrap{padding:14px 18px 26px;min-height:100vh;background:
17
+ radial-gradient(ellipse at 50% -10%,var(--halo),transparent 55%),
18
+ repeating-linear-gradient(0deg,transparent,transparent 39px,var(--grid) 40px),
19
+ repeating-linear-gradient(90deg,transparent,transparent 39px,var(--grid) 40px)}
20
+ h1{font-size:16px;letter-spacing:.16em;color:var(--cyan);text-shadow:0 0 14px rgba(62,231,255,.7);text-transform:uppercase}
21
+ .sub{font-size:10.5px;color:var(--sec);margin-top:3px}
22
+ .top{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-bottom:8px}
23
+ .ctrls{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
24
+ button,select{font:inherit;font-size:10.5px;letter-spacing:.09em;cursor:pointer;border-radius:6px;padding:6px 12px;
25
+ background:transparent;border:1px solid var(--border);color:var(--sec)}
26
+ button:hover,select:hover{background:rgba(62,231,255,.10);color:var(--cyan)}
27
+ button.on{background:rgba(62,231,255,.18);color:var(--cyan);border-color:var(--cyan);text-shadow:0 0 8px rgba(62,231,255,.8)}
28
+ #back{border-color:var(--cyan);color:var(--cyan)}
29
+ #back[disabled]{opacity:.28;cursor:default;border-color:var(--border);color:var(--ter)}
30
+ select#skin option{background:#08202e;color:var(--nm)}
31
+ #crumb{font-size:11px;color:var(--sec);margin:8px 0}
32
+ #crumb b{color:var(--cyan)}
33
+ #crumb a{color:#8fd8ec;cursor:pointer}
34
+ #crumb a:hover{text-decoration:underline}
35
+ #stamp{font-size:9.5px;color:var(--ter2);margin-bottom:6px}
36
+ #stage{position:relative;width:100%;height:74vh;min-height:520px;border:1px solid var(--stageborder);border-radius:12px;background:var(--stagebg);overflow:hidden}
37
+ svg{width:100%;height:100%;display:block}
38
+ .nd{cursor:grab}
39
+ .nd.grabbing{cursor:grabbing}
40
+ .nd rect{fill:var(--nodebg);stroke-width:1.5;transition:filter .2s}
41
+ .nd text{font-family:inherit;pointer-events:none}
42
+ .nd .tt{font-size:11.5px;font-weight:700;letter-spacing:.03em}
43
+ .nd .kk{font-size:7.6px;letter-spacing:.14em;fill:var(--ter)}
44
+ .nd .dd{font-size:9px;fill:var(--ddtxt)}
45
+ .nd .pp{font-size:9px;font-weight:700}
46
+ .nd:hover rect{filter:brightness(1.32)}
47
+ .nd.drill .plus{font-size:9px;fill:var(--cyan);letter-spacing:.1em}
48
+ .nd .drillhit{fill:transparent;stroke:none;pointer-events:auto;cursor:zoom-in}
49
+ .nd .drillhit:hover{fill:rgba(62,231,255,.12)}
50
+ .s-done rect{stroke:var(--done);filter:drop-shadow(0 0 7px rgba(47,224,138,.55))}
51
+ .s-done .tt,.s-done .pp{fill:var(--done)}
52
+ .s-prog rect{stroke:var(--prog);filter:drop-shadow(0 0 7px rgba(255,199,77,.5))}
53
+ .s-prog .tt,.s-prog .pp{fill:var(--prog)}
54
+ .s-next rect{stroke:var(--next);stroke-width:2.2;filter:drop-shadow(0 0 11px rgba(62,231,255,.75))}
55
+ .s-next .tt,.s-next .pp{fill:var(--next)}
56
+ .s-plan rect{stroke:var(--plan);stroke-dasharray:5 4}
57
+ .s-plan .tt,.s-plan .pp{fill:var(--planTxt)}
58
+ .s-prob rect{stroke:var(--prob);filter:drop-shadow(0 0 8px rgba(255,93,93,.6))}
59
+ .s-prob .tt,.s-prob .pp{fill:var(--prob)}
60
+ .mode-t .nd rect{stroke:var(--next)!important;stroke-dasharray:none!important;filter:drop-shadow(0 0 8px rgba(62,231,255,.5))!important}
61
+ .mode-t .nd .tt,.mode-t .nd .pp{fill:var(--next)!important}
62
+ .edge{stroke:var(--edge);stroke-width:1.3;fill:none;marker-end:url(#ar);cursor:help}
63
+ .edge.flow{stroke-dasharray:5 7;animation:dash 1.1s linear infinite}
64
+ .edge.plan{stroke:var(--edgeplan);stroke-dasharray:3 6;opacity:.6;marker-end:url(#arp)}
65
+ .edge.hot{stroke:var(--cyan);stroke-width:2.6;opacity:1}
66
+ .edgehit{stroke:transparent;stroke-width:14;fill:none;cursor:help}
67
+ @keyframes dash{to{stroke-dashoffset:-24}}
68
+ .nd.pulse rect{animation:pul 2.4s ease-in-out infinite}
69
+ @keyframes pul{0%,100%{filter:drop-shadow(0 0 8px rgba(62,231,255,.5))}50%{filter:drop-shadow(0 0 18px rgba(62,231,255,.95))}}
70
+ @keyframes svgin{from{opacity:0;transform:scale(.975)}to{opacity:1;transform:none}}
71
+ @keyframes ndin{from{opacity:0}to{opacity:1}}
72
+ #stage.anim svg{animation:svgin .34s cubic-bezier(.2,.7,.2,1) both;transform-origin:50% 44%}
73
+ #stage.anim .nd{animation:ndin .55s ease both}
74
+ @media(prefers-reduced-motion:reduce){#stage.anim svg,#stage.anim .nd{animation:none}}
75
+ #etip{position:absolute;pointer-events:none;display:none;z-index:6;background:var(--detailbg);border:1px solid var(--border);
76
+ color:var(--nm);font-size:10px;letter-spacing:.03em;padding:3px 8px;border-radius:5px;white-space:nowrap;box-shadow:0 3px 12px rgba(0,0,0,.45)}
77
+ #legend{display:flex;gap:13px;font-size:9.5px;color:var(--sec);margin:10px 0 0;flex-wrap:wrap}
78
+ #legend i{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:4px}
79
+ .hint{color:var(--ter2)}
80
+ #detail{margin-top:12px;border:1px solid var(--border);border-radius:10px;padding:13px;background:var(--detailbg);display:none}
81
+ #detail h3{color:var(--cyan);font-size:12.5px;margin-bottom:8px;letter-spacing:.05em}
82
+ #detail h3 .cat{color:var(--ter);font-size:9px;letter-spacing:.12em}
83
+ .row{margin-bottom:7px;font-size:10.5px;line-height:1.5}
84
+ .lbl{color:var(--ter2);font-size:8.5px;letter-spacing:.15em;text-transform:uppercase;display:block}
85
+ .func{color:#dff4fc}.now{color:var(--now)}.tgt{color:var(--tgt)}.src{color:var(--src);font-size:9.5px}
86
+ .cl{float:right;color:var(--sec);cursor:pointer;font-size:10px}
87
+ #err{color:var(--prob);font-size:11px;padding:14px}
88
+ /* PRINT / EXPORT menu */
89
+ .menuwrap{position:relative}
90
+ #pmenu{position:absolute;right:0;top:calc(100% + 4px);z-index:9;display:none;flex-direction:column;min-width:150px;
91
+ background:var(--detailbg);border:1px solid var(--border);border-radius:8px;padding:4px;box-shadow:0 6px 18px rgba(0,0,0,.5)}
92
+ #pmenu.open{display:flex}
93
+ #pmenu button{border:none;background:transparent;text-align:left;width:100%;border-radius:5px;padding:7px 10px;color:var(--sec)}
94
+ #pmenu button:hover{background:rgba(62,231,255,.12);color:var(--cyan)}
95
+ /* catalogue (collapsed homogeneous group) node */
96
+ .nd.cat rect{stroke-dasharray:2 3}
97
+ .nd.cat .plus{fill:var(--sec)}
98
+ /* detail LIST variant (catalogue roster) */
99
+ #detail .rostersearch{width:100%;font:inherit;font-size:10.5px;margin:2px 0 10px;padding:6px 9px;border-radius:6px;
100
+ background:transparent;border:1px solid var(--border);color:var(--nm)}
101
+ #detail .roster{max-height:44vh;overflow-y:auto;border-top:1px solid var(--border)}
102
+ #detail .ritem{padding:7px 2px 7px 4px;border-bottom:1px solid rgba(30,74,94,.4);display:block}
103
+ #detail .ritem .rn{color:#dff4fc;font-size:11px;font-weight:700}
104
+ #detail .ritem .rp{float:right;color:var(--sec);font-size:9px;letter-spacing:.08em}
105
+ #detail .ritem .rd{color:var(--ddtxt);font-size:9.5px;line-height:1.45;display:block;margin-top:2px;clear:both}
106
+ #detail .rempty{color:var(--ter2);font-size:10px;padding:10px 4px}
107
+ @media print{
108
+ body{background:#fff}
109
+ #wrap{background:none;padding:0}
110
+ .ctrls,#legend,.sub,#etip,#pmenu{display:none!important}
111
+ #stage{border:1px solid #999;height:auto;min-height:0}
112
+ #stage svg{height:70vh}
113
+ }
114
+ html[data-skin="blueprint"]{--bg:#eef3f9;--grid:rgba(31,119,180,.10);--halo:rgba(31,119,180,.08);
115
+ --cyan:#1f77b4;--sec:#5a6b7d;--ter:#6b7b8c;--ter2:#5a6b7d;--nm:#16273a;
116
+ --stagebg:#ffffff;--stageborder:rgba(31,119,180,.25);--nodebg:#ffffff;--detailbg:#ffffff;--border:rgba(31,119,180,.35);
117
+ --done:#1f9d57;--prog:#c07a12;--next:#1f77b4;--plan:#8592a3;--planTxt:#5a6b7d;--prob:#d1453b;
118
+ --edge:#8aa0b4;--edgeplan:#b9c6d3;--elbl:#5a6b7d;--ddtxt:#40536a;--now:#16273a;--tgt:#1f5f8a;--src:#1f9d57}
119
+ html[data-skin="blueprint"] h1{text-shadow:none}
120
+ html[data-skin="blueprint"] .s-done rect,html[data-skin="blueprint"] .s-prog rect,html[data-skin="blueprint"] .s-next rect,html[data-skin="blueprint"] .s-prob rect{filter:none}
121
+ html[data-skin="blueprint"] .nd.pulse rect{animation:none}
122
+ html[data-skin="blueprint"] button.on{text-shadow:none}
144
123
  </style>
145
124
  </head>
146
125
  <body>
147
- <div class="app">
126
+ <div id="wrap">
148
127
  <div class="top">
149
- <div>
150
- <h1 id="title">C4 · HOLOGRAM</h1>
151
- <div class="sub">click nodo = dettaglio · nodo con [+] = drill-down · ESC o BACK per risalire</div>
152
- <div class="crumb" id="crumb"></div>
153
- <div class="prov" id="prov"></div>
154
- </div>
155
- <div class="btns">
156
- <button class="btn" id="back">← back</button>
157
- <button class="btn on" id="mAttuale">stato attuale</button>
158
- <button class="btn" id="mFinito">stato finito</button>
159
- <button class="btn" id="reverify">re-verifica</button>
160
- <select class="btn sel" id="skin" title="skin"></select>
128
+ <div><h1 id="title">Forma · C4 Hologram</h1>
129
+ <div class="sub" id="subhelp"></div></div>
130
+ <div class="ctrls">
131
+ <button id="back" type="button" disabled></button>
132
+ <button id="bnow" class="on" type="button"></button>
133
+ <button id="btgt" type="button"></button>
134
+ <button id="brf" type="button"></button>
135
+ <button id="brl" type="button"></button>
136
+ <span class="menuwrap">
137
+ <button id="pexp" type="button" aria-haspopup="true" aria-expanded="false"></button>
138
+ <span id="pmenu" role="menu">
139
+ <button id="pexpSvg" type="button" role="menuitem"></button>
140
+ <button id="pexpPng" type="button" role="menuitem"></button>
141
+ <button id="pexpPrint" type="button" role="menuitem"></button>
142
+ </span>
143
+ </span>
144
+ <select id="skin" title="skin"></select>
161
145
  </div>
162
146
  </div>
163
- <div class="stage-wrap">
164
- <div class="stage" id="stage"><svg class="edges" id="edges"></svg></div>
165
- <div class="panel" id="panel"></div>
166
- </div>
167
- <div class="legend">
168
- <b><i class="dot-done"></i>fatto (provato)</b><b><i class="dot-prog"></i>in corso</b><b><i class="dot-next"></i>prossimo</b>
169
- <b><i class="dot-plan"></i>solo progetto</b><b><i class="dot-prob"></i>problema</b>
170
- <span style="color:var(--faint)">— freccia animata = flusso attivo · tratteggio = da costruire</span>
147
+ <div id="crumb"></div>
148
+ <div id="stamp"></div>
149
+ <div id="stage"><div id="etip"></div><div id="err" style="display:none"></div></div>
150
+ <div id="legend">
151
+ <span><i style="background:#2fe08a"></i><span id="lg-done"></span></span>
152
+ <span><i style="background:#ffc74d"></i><span id="lg-prog"></span></span>
153
+ <span><i style="background:#3ee7ff"></i><span id="lg-next"></span></span>
154
+ <span><i style="background:#2d5a72"></i><span id="lg-plan"></span></span>
155
+ <span><i style="background:#ff5d5d"></i><span id="lg-prob"></span></span>
156
+ <span class="hint" id="lg-hint"></span>
171
157
  </div>
158
+ <div id="detail"></div>
172
159
  </div>
173
- <div class="toast" id="toast"></div>
174
160
  <script>
175
- // Curated positions are OPTIONAL and come from the model (a node's `pos:[x,y]`), never hardcoded
176
- // here — the viewer stays stack-agnostic. When absent, layout is an auto ring so the edge-drawing
177
- // view works for ANY repo (else unknown ids would fall back to a grid with no edges).
178
- function layoutPositions(kids){
179
- const pos={};
180
- if(kids.every(n=>Array.isArray(n.pos))){kids.forEach(n=>pos[n.id]=n.pos);return pos;}
181
- const hub=kids.find(n=>n.kind==='system');
182
- const ring=kids.filter(n=>n!==hub),N=Math.max(ring.length,1);
183
- if(hub)pos[hub.id]=[50,49];
184
- ring.forEach((n,i)=>{const a=-Math.PI/2+(i/N)*2*Math.PI,rx=hub?37:42,ry=hub?33:36;
185
- pos[n.id]=[Math.round(50+rx*Math.cos(a)),Math.round(50+ry*Math.sin(a))];});
186
- return pos;
161
+ (function(){
162
+ "use strict";
163
+ // ---- i18n: every user-facing string. en is DEFAULT. ----
164
+ var STRINGS={
165
+ en:{
166
+ subhelp:"click = explain (any level) · double-click or [+] = drill in · drag to move · hover an arrow = label · ESC/BACK",
167
+ back:"← BACK", now:"CURRENT STATE", tgt:"TARGET STATE", reverify:"RE-VERIFY",
168
+ resetLayout:"RESET LAYOUT", resetLayoutTitle:"re-arrange",
169
+ printExport:"PRINT / EXPORT", exportSvg:"Export SVG", exportPng:"Export PNG", print:"Print",
170
+ legDone:"DONE", legProg:"IN PROGRESS", legNext:"NEXT", legPlan:"PLAN ONLY", legProb:"PROBLEM",
171
+ legHint:"— animated arrow = active flow · dashed = to build",
172
+ crumbRoot:"Context", crumbLevel:"C4", lvlContext:"CONTEXT",
173
+ lvlNames:{context:"Context",container:"Container",component:"Components",leaf:"Leaves"},
174
+ detClose:"[x] close", detFunc:"What it does", detNow:"Current state (facts)",
175
+ detTgt:"Target state (project)", detSrc:"Verification source", detIssue:"Issue",
176
+ detTarget:"TARGET",
177
+ rosterSearch:"filter…", rosterCount:"members", rosterEmpty:"no match",
178
+ stampBase:"Fact base:", stampMethod:"method:", stampVerify:"code+topology",
179
+ errEmpty:"Model unreachable or empty.",
180
+ errNotFound:"Model not found (./c4-model.json). Open via `forma serve` or inject window.__C4_MODEL__.",
181
+ errUnavail:"Model unavailable."
182
+ },
183
+ it:{
184
+ subhelp:"click = spiega (ogni livello) · doppio-click o [+] = entra · trascina per spostare · hover su una freccia = etichetta · ESC/BACK",
185
+ back:"← BACK", now:"STATO ATTUALE", tgt:"STATO FINITO", reverify:"RE-VERIFICA",
186
+ resetLayout:"RESET LAYOUT", resetLayoutTitle:"ridisponi",
187
+ printExport:"STAMPA / ESPORTA", exportSvg:"Esporta SVG", exportPng:"Esporta PNG", print:"Stampa",
188
+ legDone:"FATTO", legProg:"IN CORSO", legNext:"PROSSIMO", legPlan:"SOLO PROGETTO", legProb:"PROBLEMA",
189
+ legHint:"— freccia animata = flusso attivo · tratteggio = da costruire",
190
+ crumbRoot:"Contesto", crumbLevel:"C4", lvlContext:"CONTESTO",
191
+ lvlNames:{context:"Contesto",container:"Container",component:"Componenti",leaf:"Foglie"},
192
+ detClose:"[x] chiudi", detFunc:"Cosa fa", detNow:"Stato attuale (fatti)",
193
+ detTgt:"Stato finito (progetto)", detSrc:"Fonte verifica", detIssue:"Issue",
194
+ detTarget:"TARGET",
195
+ rosterSearch:"filtra…", rosterCount:"elementi", rosterEmpty:"nessun risultato",
196
+ stampBase:"Base fatti:", stampMethod:"metodo:", stampVerify:"code+topology",
197
+ errEmpty:"Modello non raggiungibile o vuoto.",
198
+ errNotFound:"Modello non trovato (./c4-model.json). Apri via `forma serve` o inietta window.__C4_MODEL__.",
199
+ errUnavail:"Modello non disponibile."
200
+ }
201
+ };
202
+ var LANG="en", STR=STRINGS.en;
203
+ function pickLang(model){
204
+ var q=null;try{q=new URLSearchParams(location.search).get("lang");}catch(e){}
205
+ var cand=q||window.__C4_LANG__||(model&&model.meta&&model.meta.lang)||"en";
206
+ cand=String(cand).toLowerCase().slice(0,2);
207
+ return STRINGS[cand]?cand:"en";
208
+ }
209
+ var CATALOG_THRESHOLD=24; // group of same-category siblings above this collapses to one catalogue node
210
+ var SKINS=[["holo","◆ holo"],["blueprint","▭ blueprint"]], SKIN_IDS=["holo","blueprint"];
211
+ var STMAP={done:"done","in-progress":"prog",next:"next",planned:"plan",problem:"prob"};
212
+ var M=null, stack=[null], mode="now", POS={}, drag=null, justDragged=false, CATNODES={};
213
+ function $(i){return document.getElementById(i)}
214
+ function esc(s){return String(s==null?"":s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/"/g,"&quot;")}
215
+ function setSkin(s){var k=SKIN_IDS.indexOf(s)>-1?s:"holo";document.documentElement.dataset.skin=k;var el=$("skin");if(el)el.value=k;}
216
+ function keyOf(){return stack.join(">")||"root";}
217
+ function childrenOf(pid){var o=[];for(var i=0;i<M.nodes.length;i++){var n=M.nodes[i];if((pid==null&&!n.parent)||n.parent===pid)o.push(n);}return o;}
218
+ // FEATURE #4: collapse large homogeneous catalogs.
219
+ // Given real children, returns render items where any group of >CATALOG_THRESHOLD
220
+ // leaf siblings sharing the same category is replaced by ONE synthetic catalogue node
221
+ // carrying its members (for the LIST detail variant). Real drill-down nodes are never
222
+ // collapsed. Small groups pass through unchanged and keep their id order.
223
+ function collapseCatalogs(pid,kids){
224
+ var groups={},order=[],i;
225
+ for(i=0;i<kids.length;i++){
226
+ var n=kids[i];
227
+ // only leaf-like, non-drillable nodes with a category are collapse candidates
228
+ var collapsible=!hasKids(n.id)&&(n.category!=null&&n.category!=="");
229
+ var key=collapsible?("cat:"+n.category):("solo:"+i);
230
+ if(!groups[key]){groups[key]={key:key,cat:n.category,members:[],collapsible:collapsible};order.push(key);}
231
+ groups[key].members.push(n);
232
+ }
233
+ var out=[];
234
+ for(i=0;i<order.length;i++){
235
+ var g=groups[order[i]];
236
+ if(g.collapsible&&g.members.length>CATALOG_THRESHOLD){
237
+ out.push(makeCatalogNode(pid,g));
238
+ }else{
239
+ for(var j=0;j<g.members.length;j++)out.push(g.members[j]);
240
+ }
241
+ }
242
+ return out;
243
+ }
244
+ function makeCatalogNode(pid,g){
245
+ // Aggregate status: worst-of-a-few heuristic — problem > next > in-progress > planned > done.
246
+ var rank={problem:5,next:4,"in-progress":3,planned:2,done:1},best=0,bestKey="done",done=0,tot=g.members.length;
247
+ for(var i=0;i<g.members.length;i++){
248
+ var s2=g.members[i].status2||(g.members[i].status==="planned"?"planned":"done");
249
+ if((rank[s2]||0)>best){best=rank[s2]||0;bestKey=s2;}
250
+ if(s2==="done")done++;
251
+ }
252
+ var lvl=(g.members[0]&&g.members[0].level)||"leaf";
253
+ return {__catalog:true, id:"__cat__"+pid+"__"+g.cat, name:g.cat, category:g.cat, kind:"catalog",
254
+ level:lvl, status2:bestKey, completion:Math.round(done/tot*100),
255
+ statusWord:g.cat+" · "+tot, members:g.members, count:tot};
256
+ }
257
+ function hasKids(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].parent===id)return true;return false;}
258
+ function node(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].id===id)return M.nodes[i];return null;}
259
+ function nameOf(id){var n=node(id);return n?n.name:id;}
260
+ function srcOf(n){if(n.verify){if(typeof n.verify==="string")return n.verify;if(n.verify.source)return n.verify.source;if(n.verify.ref)return n.verify.ref;if(n.verify.issue)return "#"+n.verify.issue;}
261
+ var ev=(n.evidence||[]).filter(function(e){return e.type==="doc"||e.type==="path";})[0];return ev?ev.ref:((M.meta&&M.meta.verifyMethod)||"");}
262
+
263
+ function seedLayout(kids){
264
+ var NW=228,NH=106,pos={},i,hub=null;
265
+ for(i=0;i<kids.length;i++)if(kids[i].kind==="system"){hub=kids[i];break;}
266
+ if(hub&&kids.length<=10){
267
+ var W=1080,H=660,cx=W/2,cy=H/2;
268
+ pos[hub.id]={x:cx-NW/2,y:cy-NH/2,w:NW,h:NH};
269
+ var others=[];for(i=0;i<kids.length;i++)if(kids[i]!==hub)others.push(kids[i]);
270
+ var N=others.length,rx=Math.min(410,150+N*40),ry=Math.min(250,110+N*22);
271
+ for(i=0;i<N;i++){var a=-Math.PI/2+(i/N)*2*Math.PI;
272
+ pos[others[i].id]={x:cx+rx*Math.cos(a)-NW/2,y:cy+ry*Math.sin(a)-NH/2,w:NW,h:NH};}
273
+ return {W:W,H:H,pos:pos};
274
+ }
275
+ var n=kids.length,cols=n<=3?Math.max(n,1):Math.ceil(Math.sqrt(n));if(cols>4)cols=4;
276
+ var rows=Math.ceil(n/cols),gx=96,gy=82,px=64,py=58;
277
+ var blockW=cols*NW+(cols-1)*gx,W=Math.max(1020,blockW+px*2),H=Math.max(560,py*2+rows*NH+(rows-1)*gy);
278
+ var offx=(W-blockW)/2;
279
+ for(i=0;i<n;i++){var c=i%cols,r=Math.floor(i/cols);
280
+ pos[kids[i].id]={x:offx+c*(NW+gx),y:py+r*(NH+gy),w:NW,h:NH};}
281
+ return {W:W,H:H,pos:pos};
187
282
  }
188
- const COL={done:'var(--done)','in-progress':'var(--prog)',next:'var(--next)',planned:'var(--plan)',problem:'var(--prob)'};
189
- const GLOW={done:'rgba(55,208,127,.28)','in-progress':'rgba(230,165,69,.28)',next:'rgba(79,200,214,.32)',planned:'transparent',problem:'rgba(242,86,91,.3)'};
190
- const MODEL_URL="./c4-model.json";
191
- const SKINS=[['holo','◆ holo'],['blueprint','▭ blueprint']];
192
- const SKIN_IDS=SKINS.map(s=>s[0]);
193
- let MODEL=null,cur=null,mode="attuale",stack=[],curKids=[];
194
- function setSkin(s){const sk=SKIN_IDS.includes(s)?s:'holo';document.documentElement.dataset.skin=sk;const el=$('skin');if(el)el.value=sk;}
195
- const $=id=>document.getElementById(id),stage=$('stage'),edgesSvg=$('edges'),panel=$('panel');
196
- const childrenOf=id=>MODEL.nodes.filter(n=>(n.parent||null)===id);
197
- const visible=n=>true;
198
- const effStat=n=>mode==="finito"?"done":(n.status2||"done");
199
- const badge=n=>mode==="finito"?"100%":(n.statusWord?n.statusWord:(n.completion!=null?n.completion+'%':''));
200
- const rawBadge=n=>n.statusWord?n.statusWord:(n.completion!=null?n.completion+'%':'');
201
- const ndesc=n=>n.kind==='leaf'?(n.func||n.current||''):(mode==="finito"?(n.target||n.func||''):(n.func||n.current||n.description||''));
202
- const DEFS='<defs><marker id="ah" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#4fc8d6"/></marker><marker id="ahd" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#7f8ea8"/></marker></defs>';
283
+ function layoutFor(kids){var k=keyOf();if(!POS[k])POS[k]=seedLayout(kids);
284
+ // make sure every current kid has a slot (model may have changed)
285
+ var lay=POS[k];for(var i=0;i<kids.length;i++)if(!lay.pos[kids[i].id]){var s=seedLayout(kids);POS[k]=s;return s;}return lay;}
286
+
287
+ function draw(animate){
288
+ if(!M)return;
289
+ var pid=stack[stack.length-1],realKids=childrenOf(pid);
290
+ if(!realKids.length){if(stack.length>1){stack.pop();return draw(animate);}}
291
+ var kids=collapseCatalogs(pid,realKids);
292
+ CATNODES={};for(var ci=0;ci<kids.length;ci++)if(kids[ci].__catalog)CATNODES[kids[ci].id]=kids[ci];
293
+ var lay=layoutFor(kids),pos=lay.pos;
294
+ var idset={},memberMap={};
295
+ for(var z=0;z<kids.length;z++){idset[kids[z].id]=1;
296
+ if(kids[z].__catalog)for(var mm=0;mm<kids[z].members.length;mm++)memberMap[kids[z].members[mm].id]=kids[z].id;}
297
+ function mapId(id){return memberMap[id]||id;}
298
+ var edges=(M.edges||[]).map(function(e){return {from:mapId(e.from),to:mapId(e.to),label:e.label,estatus:e.estatus};})
299
+ .filter(function(e){return idset[e.from]&&idset[e.to]&&e.from!==e.to;});
300
+ var s='<svg viewBox="0 0 '+lay.W+' '+lay.H+'" preserveAspectRatio="xMidYMid meet" class="'+(mode==="target"?"mode-t":"")+'">'
301
+ +'<defs><marker id="ar" markerWidth="9" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0,9 3.5,0 7" fill="'+cssv("--edge")+'"/></marker>'
302
+ +'<marker id="arp" markerWidth="9" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0,9 3.5,0 7" fill="'+cssv("--edgeplan")+'"/></marker></defs>';
303
+ for(var i=0;i<edges.length;i++){
304
+ var a=pos[edges[i].from],b=pos[edges[i].to];if(!a||!b)continue;
305
+ var d=edgePath(a,b),cl=(edges[i].estatus==="to-build"?"plan":"flow");
306
+ s+='<path class="edge '+cl+'" data-e="'+i+'" d="'+d+'"/>';
307
+ s+='<path class="edgehit" data-e="'+i+'" data-label="'+esc(edges[i].label||"")+'" d="'+d+'"/>';
308
+ }
309
+ for(var j=0;j<kids.length;j++){
310
+ var n=kids[j],p=pos[n.id];
311
+ var st=STMAP[n.status2]||(n.status==="planned"?"plan":"done");
312
+ var pulse=(st==="next"||st==="prog")&&mode!=="target"&&!n.__catalog;
313
+ var isCat=!!n.__catalog,dr=!isCat&&hasKids(n.id),cls="nd s-"+st+(dr?" drill":"")+(isCat?" cat":"")+(pulse?" pulse":"");
314
+ var desc=isCat?(n.count+" "+STR.rosterCount):(mode==="target"?(n.target||n.func||""):(n.current||n.func||n.tech||""));
315
+ var badge=isCat?String(n.count):(mode==="target"?STR.detTarget:(n.statusWord||(n.completion!=null?n.completion+"%":(n.tech||""))));
316
+ var tmax=Math.floor((p.w-70)/6.7),tt=n.name;if(String(tt).length>tmax)tt=String(tt).slice(0,tmax-1)+"…";
317
+ s+='<g class="'+cls+'" data-id="'+esc(n.id)+'">'
318
+ +'<rect x="'+p.x+'" y="'+p.y+'" width="'+p.w+'" height="'+p.h+'" rx="10"/>'
319
+ +'<text class="kk" x="'+(p.x+13)+'" y="'+(p.y+16)+'">'+esc(n.category||n.kind||"")+'</text>'
320
+ +'<text class="tt" x="'+(p.x+13)+'" y="'+(p.y+33)+'">'+esc(tt)+'</text>'
321
+ +'<text class="pp" x="'+(p.x+p.w-13)+'" y="'+(p.y+16)+'" text-anchor="end">'+esc(badge)+'</text>';
322
+ var words=String(desc).split(" "),line="",ln=0,cpl=Math.floor((p.w-26)/5.4),MAXL=2;
323
+ for(var k=0;k<words.length&&ln<MAXL;k++){
324
+ if((line+words[k]).length>cpl){s+='<text class="dd" x="'+(p.x+13)+'" y="'+(p.y+51+ln*12.5)+'">'+esc(line.trim())+'</text>';line=words[k]+" ";ln++;}
325
+ else line+=words[k]+" ";}
326
+ if(ln<MAXL&&line)s+='<text class="dd" x="'+(p.x+13)+'" y="'+(p.y+51+ln*12.5)+'">'+esc(line.length>cpl?line.slice(0,cpl-1)+"…":line.trim())+'</text>';
327
+ else if(ln===MAXL&&line.trim())s+='<text class="dd" x="'+(p.x+13)+'" y="'+(p.y+51+ln*12.5)+'">…</text>';
328
+ if(dr)s+='<rect class="drillhit" data-drill="1" x="'+(p.x+p.w-96)+'" y="'+(p.y+p.h-25)+'" width="88" height="20" rx="5"/>'
329
+ +'<text class="plus" x="'+(p.x+p.w-13)+'" y="'+(p.y+p.h-9)+'" text-anchor="end">[+] DRILL</text>';
330
+ else if(isCat)s+='<text class="plus" x="'+(p.x+p.w-13)+'" y="'+(p.y+p.h-9)+'" text-anchor="end">[≡] '+esc(String(n.count))+'</text>';
331
+ s+='</g>';
332
+ }
333
+ s+='</svg>';
334
+ var st2=$("stage");
335
+ var etip=$("etip"); // preserve tooltip node
336
+ st2.innerHTML=s;st2.appendChild(etip);
337
+ st2.classList.toggle("anim",animate!==false);
338
+ // breadcrumb
339
+ var chain=[],c=pid;while(c){var nn=node(c);chain.unshift(nn);c=nn?nn.parent:null;}
340
+ var lvl=pid?levelName(kids[0]&&kids[0].level):STR.lvlContext;
341
+ var cb=STR.crumbLevel+" · "+lvl.toUpperCase()+" &nbsp;&nbsp; <a data-d='-1'>"+esc(STR.crumbRoot)+"</a>";
342
+ for(var w=0;w<chain.length;w++)cb+=" &rsaquo; "+(w===chain.length-1?"<b>"+esc(chain[w].name)+"</b>":"<a data-d='"+w+"'>"+esc(chain[w].name)+"</a>");
343
+ $("crumb").innerHTML=cb;
344
+ $("back").disabled=stack.length<2;
345
+ $("detail").style.display="none";
346
+ }
347
+ function edgePath(a,b){
348
+ var ax=a.x+a.w/2,ay=a.y+a.h/2,bx=b.x+b.w/2,by=b.y+b.h/2,dx=bx-ax,dy=by-ay,L=Math.sqrt(dx*dx+dy*dy)||1;
349
+ var x1=ax+(a.w/2+10)*dx/L,y1=ay+(a.h/2+10)*dy/L,x2=bx-(b.w/2+12)*dx/L,y2=by-(b.h/2+12)*dy/L;
350
+ var mx=(x1+x2)/2+(y2-y1)*.12,my=(y1+y2)/2-(x2-x1)*.12;
351
+ return "M"+x1+","+y1+" Q"+mx+","+my+" "+x2+","+y2;
352
+ }
353
+ function levelName(l){return (STR.lvlNames&&STR.lvlNames[l])||l||"—";}
354
+ function cssv(v){try{return getComputedStyle(document.documentElement).getPropertyValue(v).trim()||"#2b6e8a";}catch(e){return "#2b6e8a";}}
355
+
356
+ function showDetail(n){
357
+ if(n&&n.__catalog)return showRoster(n);
358
+ var src=srcOf(n),cur=n.current||n.func||n.tech||"",showSrc=src&&String(cur).indexOf(src)<0;
359
+ $("detail").innerHTML='<span class="cl" id="dc">'+esc(STR.detClose)+'</span><h3>'+esc(n.name)+' <span class="cat">· '+esc(n.category||n.kind||"")+(n.tech?" · "+esc(n.tech):"")+'</span></h3>'
360
+ +(n.func?'<div class="row"><span class="lbl">'+esc(STR.detFunc)+'</span><span class="func">'+esc(n.func)+'</span></div>':'')
361
+ +(cur&&cur!==n.func?'<div class="row"><span class="lbl">'+esc(STR.detNow)+'</span><span class="now">'+esc(cur)+'</span></div>':'')
362
+ +((n.target&&n.target!=="—")?'<div class="row"><span class="lbl">'+esc(STR.detTgt)+'</span><span class="tgt">'+esc(n.target)+'</span></div>':'')
363
+ +(showSrc?'<div class="row"><span class="lbl">'+esc(STR.detSrc)+'</span><span class="src">'+esc(src)+'</span></div>':'')
364
+ +((n.issues&&n.issues.length)?'<div class="row"><span class="lbl">'+esc(STR.detIssue)+'</span><span class="now">'+esc(n.issues.join(", "))+'</span></div>':'');
365
+ $("detail").style.display="block";
366
+ var dc=$("dc");if(dc)dc.addEventListener("click",function(){$("detail").style.display="none";});
367
+ }
368
+ // FEATURE #4: LIST detail variant — searchable roster of a collapsed catalogue's members.
369
+ // Each row keeps the per-leaf name + func/current description (the payoff must survive).
370
+ function showRoster(cat){
371
+ var members=cat.members||[];
372
+ var head='<span class="cl" id="dc">'+esc(STR.detClose)+'</span>'
373
+ +'<h3>'+esc(cat.name)+' <span class="cat">· '+esc(cat.count+" "+STR.rosterCount)+'</span></h3>'
374
+ +'<input class="rostersearch" id="rsq" type="text" placeholder="'+esc(STR.rosterSearch)+'" autocomplete="off">'
375
+ +'<div class="roster" id="rlist"></div>';
376
+ $("detail").innerHTML=head;$("detail").style.display="block";
377
+ var dc=$("dc");if(dc)dc.addEventListener("click",function(){$("detail").style.display="none";});
378
+ function rowHtml(m){
379
+ var d=m.current||m.func||m.tech||"";
380
+ var pct=m.completion!=null?(m.completion+"%"):(m.statusWord||"");
381
+ return '<span class="ritem">'+(pct?'<span class="rp">'+esc(pct)+'</span>':'')
382
+ +'<span class="rn">'+esc(m.name)+'</span>'
383
+ +(d?'<span class="rd">'+esc(d)+'</span>':'')+'</span>';
384
+ }
385
+ function render(q){
386
+ q=String(q||"").toLowerCase();var out="",shown=0,i;
387
+ for(i=0;i<members.length;i++){var m=members[i];
388
+ var hay=((m.name||"")+" "+(m.func||"")+" "+(m.current||"")+" "+(m.tech||"")).toLowerCase();
389
+ if(!q||hay.indexOf(q)>-1){out+=rowHtml(m);shown++;}}
390
+ $("rlist").innerHTML=shown?out:'<div class="rempty">'+esc(STR.rosterEmpty)+'</div>';
391
+ }
392
+ render("");
393
+ var q=$("rsq");if(q)q.addEventListener("input",function(){render(q.value);});
394
+ }
395
+
396
+ // ---- interaction: drag nodes + hover edge labels ----
397
+ function svgPt(evt){var svg=$("stage").querySelector("svg");if(!svg||!svg.getScreenCTM)return null;
398
+ var p=svg.createSVGPoint();p.x=evt.clientX;p.y=evt.clientY;try{return p.matrixTransform(svg.getScreenCTM().inverse());}catch(e){return null;}}
399
+ function curPos(){return POS[keyOf()]?POS[keyOf()].pos:null;}
400
+ function clamp(v,lo,hi){return v<lo?lo:v>hi?hi:v;}
203
401
 
204
- function boot(m){MODEL=m;
205
- $('skin').innerHTML=SKINS.map(([id,lb])=>`<option value="${id}">${lb}</option>`).join('');
206
- setSkin((new URLSearchParams(location.search).get('skin'))||window.__C4_SKIN__||(m.meta&&m.meta.skin)||'holo');
207
- $('title').textContent=(m.source&&m.source.repo?m.source.repo.toUpperCase():'C4')+' · C4 HOLOGRAM';
208
- const ve=m.meta&&m.meta.verifyError;
209
- $('prov').textContent='Base fatti '+((m.meta&&m.meta.verifiedAt)||m.generatedAt||'').slice(0,16).replace('T',' ')+'Z · commit '+((m.source&&m.source.commit)||'').slice(0,8)+(ve?' (re-verifica fallita: '+ve+')':'');
210
- $('prov').className='prov'+(ve?' warn':'');
211
- new ResizeObserver(()=>{if(curKids.length){edgesSvg.innerHTML=DEFS;drawEdges(curKids,false)}}).observe(stage);
212
- render(null);}
213
- function styleVars(el,n){const s=effStat(n);el.style.setProperty('--nc',COL[s]||'var(--plan)');el.style.setProperty('--ng',GLOW[s]||'transparent');}
214
- function render(pid){cur=pid;closePanel();
215
- [...stage.querySelectorAll('.node,.grid')].forEach(e=>e.remove());edgesSvg.innerHTML=DEFS;
216
- const pn=pid?MODEL.nodes.find(n=>n.id===pid):null;
217
- const lvl=pn?('C4-'+({container:'L2',component:'L3',leaf:'L4'}[childrenOf(pid)[0]?childrenOf(pid)[0].level:'']||'L?')+' · '+pn.name.toUpperCase()):'C4-L1 · CONTESTO';
218
- renderCrumb(pid,lvl);$('back').disabled=stack.length===0;
219
- const kids=childrenOf(pid).filter(visible);
220
- if(kids.length===0||kids.length>12||stage.clientWidth<640){curKids=[];return renderGrid(kids);}
221
- curKids=kids;const POS=layoutPositions(kids);
222
- kids.forEach((n,i)=>{const el=document.createElement('div'),hasK=childrenOf(n.id).length>0;
223
- el.className="node"+((n.status2==='planned'&&mode!=='finito')?' dashed':'')+(hasK?' hasK':'');el.dataset.id=n.id;
224
- const[x,y]=POS[n.id];el.style.left=x+'%';el.style.top=y+'%';styleVars(el,n);
225
- el.innerHTML=`<div class="top2"><span class="cat">${n.category||n.kind}</span><span class="pct">${badge(n)}</span></div>
226
- <div class="nm">${n.name}</div><div class="ds">${ndesc(n)}</div>${hasK?'<span class="drill">[+] DRILL</span>':''}`;
227
- el.onclick=e=>{e.stopPropagation();hasK?drill(n):select(n);};el.ondblclick=e=>{e.stopPropagation();select(n);};
228
- stage.appendChild(el);requestAnimationFrame(()=>setTimeout(()=>el.classList.add('in'),70+i*55));});
229
- requestAnimationFrame(()=>setTimeout(()=>drawEdges(kids,true),150));}
230
- function renderGrid(kids){const g=document.createElement('div');g.className="grid";
231
- kids.forEach((n,i)=>{const hasK=childrenOf(n.id).length>0,c=document.createElement('div');
232
- c.className="cell"+((n.status2==='planned'&&mode!=='finito')?' dashed':'');c.dataset.id=n.id;styleVars(c,n);
233
- c.innerHTML=`<div class="top2"><span class="cat">${n.category||n.kind}</span><span class="pct">${badge(n)}</span></div>
234
- <div class="nm">${n.name}</div><div class="ds">${ndesc(n)||n.tech||''}</div>${hasK?'<span class="drill">[+]</span>':''}`;
235
- c.onclick=()=>{hasK?drill(n):select(n);};c.ondblclick=()=>select(n);
236
- g.appendChild(c);requestAnimationFrame(()=>setTimeout(()=>c.classList.add('in'),i*16));});
237
- stage.appendChild(g);}
238
- function center(id){const el=stage.querySelector(`.node[data-id="${id}"]`);if(!el)return null;const s=stage.getBoundingClientRect(),r=el.getBoundingClientRect();return{x:r.left-s.left+r.width/2,y:r.top-s.top+r.height/2,w:r.width,h:r.height};}
239
- function drawEdges(kids,anim){const ids=new Set(kids.map(n=>n.id));
240
- MODEL.edges.filter(e=>ids.has(e.from)&&ids.has(e.to)).forEach((e,i)=>{const A=center(e.from),B=center(e.to);if(!A||!B)return;const ns="http://www.w3.org/2000/svg";
241
- let x1=A.x,y1=A.y,x2=B.x,y2=B.y;const dx=x2-x1,dy=y2-y1,d=Math.hypot(dx,dy)||1;
242
- x1+=dx/d*(A.w/2*.72);y1+=dy/d*(A.h/2*.9);x2-=dx/d*(B.w/2*.86);y2-=dy/d*(B.h/2*.95);
243
- const mx=(x1+x2)/2,my=(y1+y2)/2,cx=mx-dy*.08,cy=my+dx*.08,ds=`M${x1},${y1} Q${cx},${cy} ${x2},${y2}`;
244
- const build=e.estatus==='to-build';const p=document.createElementNS(ns,'path');p.setAttribute('d',ds);p.setAttribute('fill','none');
245
- p.setAttribute('stroke',build?'#5a6b82':'#2f6f86');p.setAttribute('stroke-width','1.4');p.setAttribute('marker-end',build?'url(#ahd)':'url(#ah)');
246
- if(build)p.setAttribute('stroke-dasharray','5 5');edgesSvg.appendChild(p);
247
- if(!build){const fl=document.createElementNS(ns,'path');fl.setAttribute('d',ds);fl.setAttribute('class','eflow');fl.style.animationDelay=(i*.12)+'s';edgesSvg.appendChild(fl);}
248
- if(e.label){const w=e.label.length*6.2+10,pill=document.createElementNS(ns,'rect');pill.setAttribute('x',cx-w/2);pill.setAttribute('y',cy-14);pill.setAttribute('width',w);pill.setAttribute('height',15);pill.setAttribute('rx',3);pill.setAttribute('class','epill');edgesSvg.appendChild(pill);
249
- const t=document.createElementNS(ns,'text');t.setAttribute('x',cx);t.setAttribute('y',cy-3);t.setAttribute('text-anchor','middle');t.setAttribute('class','elabel');t.textContent=e.label;edgesSvg.appendChild(t);}});}
250
- function zoomTo(o,s,cb){stage.style.transformOrigin=o[0]+'% '+o[1]+'%';stage.style.transition='transform .3s ease,opacity .3s ease';
251
- requestAnimationFrame(()=>{stage.style.transform='scale('+s+')';stage.style.opacity='0';});
252
- setTimeout(()=>{stage.style.transition='none';stage.style.transform='none';stage.style.opacity='1';cb();},260);}
253
- function drill(n){const c=center(n.id);const o=c&&stage.clientWidth?[100*c.x/stage.clientWidth,100*c.y/stage.clientHeight]:(n.pos||[50,45]);zoomTo(o,1.5,()=>{stack.push(cur);render(n.id);});}
254
- function back(){if(!stack.length)return;zoomTo([50,45],.74,()=>{render(stack.pop());});}
255
- function select(n){[...stage.querySelectorAll('.sel')].forEach(e=>e.classList.remove('sel'));const el=stage.querySelector(`[data-id="${n.id}"]`);if(el)el.classList.add('sel');
256
- const base=(MODEL.meta&&MODEL.meta.repoUrl)||'',br=(MODEL.meta&&MODEL.meta.docBranch)||'main';
257
- panel.style.setProperty('--nc',COL[n.status2]||'var(--next)');
258
- const v=n.verify||{};const issues=(n.issues||[]).map(x=>x).join(', ');
259
- const vref=v.ref||'';const vlink=base&&/^(docs|src|scripts|standards)\//.test(vref)?`${base}/blob/${br}/${vref.split(' ')[0]}`:'';
260
- const cosa=n.func||n.current||n.description||'—';
261
- const showCur=n.current&&n.current!==cosa&&!/^Esiste:/.test(n.current);
262
- const showTgt=n.target&&n.target!==''&&n.target!=='Idem.';
263
- panel.innerHTML=`<div class="ph"><h3>${n.name} <span style="font-size:12px;color:var(--dim)">· ${n.category||n.kind} · ${rawBadge(n)}</span></h3><span class="close" onclick="closePanel()">[x] chiudi</span></div>
264
- <div class="lab">Cosa fa</div><p>${cosa}</p>
265
- ${showCur?`<div class="lab">Stato attuale (fatti)</div><p>${n.current}</p>`:''}
266
- ${showTgt?`<div class="lab">Stato finito (progetto)</div><p>${n.target}</p>`:''}
267
- <div class="lab">Fonte verifica</div><p>${vlink?`<a href="${vlink}" target="_blank">${vref}</a>`:`<code>${vref||'code+topology'}</code>`} · <code>${((MODEL.meta&&MODEL.meta.verifiedAt)||'').slice(0,16).replace('T',' ')}Z</code></p>
268
- ${issues?`<div class="lab">Issue</div><p>${issues}</p>`:''}`;
269
- panel.classList.add('open');}
270
- function closePanel(){panel.classList.remove('open');[...stage.querySelectorAll('.sel')].forEach(e=>e.classList.remove('sel'));}
271
- function renderCrumb(pid,lvl){const chain=[];let c=pid;while(c){const n=MODEL.nodes.find(x=>x.id===c);chain.unshift(n);c=n?n.parent:null;}
272
- $('crumb').innerHTML=`<b>${lvl}</b>&nbsp;&nbsp;<a onclick="jump(null)">Contesto</a>`+chain.map(n=>` <span class="sep">›</span> <a onclick="jump('${n.id}')">${n.name}</a>`).join('');}
273
- function jump(id){const chain=[];let c=id;while(c){const n=MODEL.nodes.find(x=>x.id===c);chain.unshift(c);c=n?n.parent:null;}stack=id===null?[]:[null,...chain.slice(0,-1)];render(id);}
274
- function setMode(m){mode=m;$('mAttuale').classList.toggle('on',m==='attuale');$('mFinito').classList.toggle('on',m==='finito');render(cur);}
275
- let tt;function toast(x){const t=$('toast');t.textContent=x;t.classList.add('show');clearTimeout(tt);tt=setTimeout(()=>t.classList.remove('show'),2800);}
276
- $('back').onclick=back;$('mAttuale').onclick=()=>setMode('attuale');$('mFinito').onclick=()=>setMode('finito');
277
- $('reverify').onclick=()=>toast('Re-verifica: '+((MODEL.meta&&MODEL.meta.verifyMethod)||'code+topology')+'. La skill rilancia il check contro GitHub (gh) quando disponibile.');
278
- $('skin').onchange=(e)=>{setSkin(e.target.value);toast('skin: '+e.target.value);};
279
- document.addEventListener('keydown',e=>{if(e.key==='Escape'){panel.classList.contains('open')?closePanel():back();}});
280
- stage.onclick=()=>closePanel();
281
- (async function(){if(window.__C4_MODEL__)return boot(window.__C4_MODEL__);
282
- try{const r=await fetch(MODEL_URL,{cache:'no-store'});if(!r.ok)throw 0;boot(await r.json());}catch(_){$('title').textContent='c4-model.json non raggiungibile — servi la cartella o usa la build standalone';}})();
402
+ function wire(){
403
+ var stage=$("stage");
404
+ stage.addEventListener("mousedown",function(ev){
405
+ if(ev.target.getAttribute&&ev.target.getAttribute("data-drill"))return; // [+] is a click→drill, not a drag grip
406
+ var g=ev.target;while(g&&g!==stage&&!(g.getAttribute&&g.getAttribute("data-id")))g=g.parentNode;
407
+ if(!g||g===stage)return;
408
+ var id=g.getAttribute("data-id"),pos=curPos();if(!pos||!pos[id])return;
409
+ var p=svgPt(ev);if(!p)return;
410
+ drag={id:id,ox:p.x-pos[id].x,oy:p.y-pos[id].y,moved:false};
411
+ g.classList.add("grabbing");ev.preventDefault();
412
+ });
413
+ document.addEventListener("mousemove",function(ev){
414
+ if(!drag)return;var pos=curPos();if(!pos)return;var p=svgPt(ev);if(!p)return;var lay=POS[keyOf()];
415
+ pos[drag.id].x=clamp(p.x-drag.ox,4,lay.W-pos[drag.id].w-4);
416
+ pos[drag.id].y=clamp(p.y-drag.oy,4,lay.H-pos[drag.id].h-4);
417
+ drag.moved=true;draw(false);
418
+ });
419
+ document.addEventListener("mouseup",function(){if(drag){if(drag.moved)justDragged=true;drag=null;}});
420
+ // edge label on hover (invisible fat hit-path)
421
+ stage.addEventListener("mouseover",function(ev){
422
+ if(ev.target.classList&&ev.target.classList.contains("edgehit")){
423
+ var lb=ev.target.getAttribute("data-label");if(!lb)return;
424
+ var real=stage.querySelector('.edge[data-e="'+ev.target.getAttribute("data-e")+'"]');if(real)real.classList.add("hot");
425
+ var t=$("etip");t.textContent=lb;t.style.display="block";moveTip(ev);
426
+ }
427
+ });
428
+ stage.addEventListener("mousemove",function(ev){if($("etip").style.display==="block"&&ev.target.classList&&ev.target.classList.contains("edgehit"))moveTip(ev);});
429
+ stage.addEventListener("mouseout",function(ev){
430
+ if(ev.target.classList&&ev.target.classList.contains("edgehit")){$("etip").style.display="none";
431
+ var real=stage.querySelector('.edge.hot');if(real)real.classList.remove("hot");}
432
+ });
433
+ stage.addEventListener("click",function(ev){
434
+ if(justDragged){justDragged=false;return;}
435
+ var wantDrill=ev.target.getAttribute&&ev.target.getAttribute("data-drill");
436
+ var g=ev.target;while(g&&g!==stage&&!(g.getAttribute&&g.getAttribute("data-id")))g=g.parentNode;
437
+ if(!g||g===stage){ $("detail").style.display="none"; return; }
438
+ var id=g.getAttribute("data-id");
439
+ if(CATNODES[id]){showDetail(CATNODES[id]);return;}
440
+ var n=node(id);if(!n)return;
441
+ // click inspects at EVERY level (parents carry func/current/description too);
442
+ // drilling is the explicit [+] control or a double-click — it no longer steals the click.
443
+ if(wantDrill&&hasKids(id)){stack.push(id);draw(true);return;}
444
+ showDetail(n);
445
+ });
446
+ stage.addEventListener("dblclick",function(ev){
447
+ var g=ev.target;while(g&&g!==stage&&!(g.getAttribute&&g.getAttribute("data-id")))g=g.parentNode;
448
+ if(!g||g===stage)return;
449
+ var id=g.getAttribute("data-id");
450
+ if(CATNODES[id])return; // catalogues open their roster on click; nothing to drill into
451
+ if(hasKids(id)){$("detail").style.display="none";stack.push(id);draw(true);}
452
+ });
453
+ $("crumb").addEventListener("click",function(ev){if(ev.target.tagName==="A"){var d=+ev.target.getAttribute("data-d");
454
+ stack=stack.slice(0,d+2);if(!stack.length)stack=[null];draw(true);}});
455
+ $("back").addEventListener("click",function(){if(stack.length>1){stack.pop();draw(true);}});
456
+ document.addEventListener("keydown",function(e){if(e.key==="Escape"&&stack.length>1){stack.pop();draw(true);}});
457
+ $("bnow").addEventListener("click",function(){mode="now";$("bnow").className="on";$("btgt").className="";draw(false);});
458
+ $("btgt").addEventListener("click",function(){mode="target";$("btgt").className="on";$("bnow").className="";draw(false);});
459
+ $("brf").addEventListener("click",function(){var m=(M&&M.meta)||{};$("stamp").textContent=STR.stampBase+" "+(m.verifiedAt||(M&&M.source&&M.source.commit)||"—")+" · "+STR.stampMethod+" "+(m.verifyMethod||STR.stampVerify);});
460
+ $("brl").addEventListener("click",function(){delete POS[keyOf()];draw(true);});
461
+ $("skin").innerHTML=SKINS.map(function(s){return '<option value="'+s[0]+'">'+s[1]+'</option>';}).join("");
462
+ $("skin").addEventListener("change",function(e){setSkin(e.target.value);});
463
+ wireExport();
464
+ }
465
+ // ---- i18n: apply every static UI string from STR ----
466
+ function applyStrings(){
467
+ function set(id,txt){var e=$(id);if(e)e.textContent=txt;}
468
+ set("subhelp",STR.subhelp);set("back",STR.back);set("bnow",STR.now);set("btgt",STR.tgt);
469
+ set("brf",STR.reverify);set("brl",STR.resetLayout);var brl=$("brl");if(brl)brl.title=STR.resetLayoutTitle;
470
+ set("pexp",STR.printExport);set("pexpSvg",STR.exportSvg);set("pexpPng",STR.exportPng);set("pexpPrint",STR.print);
471
+ set("lg-done",STR.legDone);set("lg-prog",STR.legProg);set("lg-next",STR.legNext);
472
+ set("lg-plan",STR.legPlan);set("lg-prob",STR.legProb);set("lg-hint",STR.legHint);
473
+ }
474
+ // ---- FEATURE #2: PRINT / EXPORT (all client-side, offline) ----
475
+ function exportBaseName(){
476
+ var repo=(M&&M.source&&M.source.repo)||"forma";
477
+ var pid=stack[stack.length-1];
478
+ var lvl=pid?(node(pid)&&node(pid).name)||"level":"context";
479
+ return String(repo+"-"+lvl).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"forma";
480
+ }
481
+ // Collect the CSS rules the SVG needs so a standalone serialize renders styled.
482
+ function collectSvgCss(){
483
+ var out=[],ss=document.styleSheets,i,j;
484
+ for(i=0;i<ss.length;i++){
485
+ var rules;try{rules=ss[i].cssRules;}catch(e){continue;}
486
+ if(!rules)continue;
487
+ for(j=0;j<rules.length;j++){var r=rules[j],t=r.cssText||"";
488
+ // keep rules that target svg-internal classes/elements + the color vars on :root/html
489
+ if(/\.(nd|edge|edgehit|s-done|s-prog|s-next|s-plan|s-prob|mode-t|tt|kk|dd|pp|plus|cat)\b/.test(t)
490
+ ||/(^|[\s,])svg\b/.test(t)||/:root|html\[data-skin/.test(t)){
491
+ // drop animations for a static export (keepdefs stable)
492
+ out.push(t);
493
+ }}
494
+ }
495
+ return out.join("\n");
496
+ }
497
+ // Resolve the CURRENT skin's variables from the live document, so a standalone SVG
498
+ // (where `html[data-skin=...]` selectors have no ancestor to match) still renders
499
+ // with the on-screen palette. Returns a "--k:v;--k:v" inline-style string.
500
+ var SVG_VARS=["--bg","--grid","--halo","--cyan","--sec","--ter","--ter2","--nm",
501
+ "--stagebg","--stageborder","--nodebg","--detailbg","--border","--done","--prog",
502
+ "--next","--plan","--planTxt","--prob","--edge","--edgeplan","--elbl","--ddtxt",
503
+ "--now","--tgt","--src"];
504
+ function resolvedVars(){
505
+ var cs;try{cs=getComputedStyle(document.documentElement);}catch(e){return "";}
506
+ var out="";for(var i=0;i<SVG_VARS.length;i++){var v=(cs.getPropertyValue(SVG_VARS[i])||"").trim();if(v)out+=SVG_VARS[i]+":"+v+";";}
507
+ return out;
508
+ }
509
+ function serializeSvg(){
510
+ var svg=$("stage").querySelector("svg");if(!svg)return null;
511
+ var clone=svg.cloneNode(true);
512
+ clone.setAttribute("xmlns","http://www.w3.org/2000/svg");
513
+ clone.setAttribute("xmlns:xlink","http://www.w3.org/1999/xlink");
514
+ var vb=(svg.getAttribute("viewBox")||"0 0 1080 660").split(/\s+/);
515
+ var w=+vb[2]||1080,h=+vb[3]||660;
516
+ clone.setAttribute("width",w);clone.setAttribute("height",h);
517
+ // Inline the resolved variables on the root so var(--x) resolves standalone,
518
+ // independent of the skin-selector chain that won't exist in the exported file.
519
+ var vars=resolvedVars();if(vars)clone.setAttribute("style",vars);
520
+ // solid dark/light background so the standalone file isn't transparent
521
+ var bg=cssv("--stagebg")||"#050d14";
522
+ var rect='<rect x="0" y="0" width="'+w+'" height="'+h+'" fill="'+bg+'"/>';
523
+ var style="<style>"+collectSvgCss()+"</style>";
524
+ var inner=clone.innerHTML;
525
+ var attrs='';for(var a=0;a<clone.attributes.length;a++){var at=clone.attributes[a];attrs+=' '+at.name+'="'+String(at.value).replace(/"/g,"&quot;")+'"';}
526
+ var body='<svg'+attrs+'>'+style+rect+inner+'</svg>';
527
+ return {xml:'<?xml version="1.0" encoding="UTF-8"?>\n'+body,w:w,h:h,bg:bg};
528
+ }
529
+ function triggerDownload(blob,filename){
530
+ var url=URL.createObjectURL(blob),a=document.createElement("a");
531
+ a.href=url;a.download=filename;document.body.appendChild(a);a.click();
532
+ document.body.removeChild(a);setTimeout(function(){URL.revokeObjectURL(url);},4000);
533
+ }
534
+ function exportSvg(){
535
+ var s=serializeSvg();if(!s)return;
536
+ triggerDownload(new Blob([s.xml],{type:"image/svg+xml;charset=utf-8"}),exportBaseName()+".svg");
537
+ }
538
+ function exportPng(){
539
+ var s=serializeSvg();if(!s)return;
540
+ var scale=Math.min(3,Math.max(2,Math.round(2400/Math.max(1,s.w))))||2;
541
+ var cw=Math.round(s.w*scale),ch=Math.round(s.h*scale);
542
+ var img=new Image();
543
+ var url=URL.createObjectURL(new Blob([s.xml],{type:"image/svg+xml;charset=utf-8"}));
544
+ img.onload=function(){
545
+ try{
546
+ var cv=document.createElement("canvas");cv.width=cw;cv.height=ch;
547
+ var ctx=cv.getContext("2d");
548
+ ctx.fillStyle=s.bg||"#050d14";ctx.fillRect(0,0,cw,ch);
549
+ ctx.drawImage(img,0,0,cw,ch);
550
+ URL.revokeObjectURL(url);
551
+ if(cv.toBlob)cv.toBlob(function(b){if(b)triggerDownload(b,exportBaseName()+".png");},"image/png");
552
+ else{var dataUrl=cv.toDataURL("image/png");var a=document.createElement("a");a.href=dataUrl;a.download=exportBaseName()+".png";document.body.appendChild(a);a.click();document.body.removeChild(a);}
553
+ }catch(e){URL.revokeObjectURL(url);}
554
+ };
555
+ img.onerror=function(){URL.revokeObjectURL(url);};
556
+ img.src=url;
557
+ }
558
+ function wireExport(){
559
+ var btn=$("pexp"),menu=$("pmenu");if(!btn||!menu)return;
560
+ function close(){menu.classList.remove("open");btn.setAttribute("aria-expanded","false");}
561
+ function toggle(){var open=menu.classList.toggle("open");btn.setAttribute("aria-expanded",open?"true":"false");}
562
+ btn.addEventListener("click",function(ev){ev.stopPropagation();toggle();});
563
+ document.addEventListener("click",function(ev){if(!menu.contains(ev.target)&&ev.target!==btn)close();});
564
+ document.addEventListener("keydown",function(e){if(e.key==="Escape")close();});
565
+ var sv=$("pexpSvg");if(sv)sv.addEventListener("click",function(){close();exportSvg();});
566
+ var pn=$("pexpPng");if(pn)pn.addEventListener("click",function(){close();exportPng();});
567
+ var pr=$("pexpPrint");if(pr)pr.addEventListener("click",function(){close();window.print();});
568
+ }
569
+ function moveTip(ev){var st=$("stage").getBoundingClientRect(),t=$("etip");
570
+ var x=ev.clientX-st.left+12,y=ev.clientY-st.top+12;
571
+ t.style.left=Math.min(x,st.width-t.offsetWidth-6)+"px";t.style.top=Math.min(y,st.height-t.offsetHeight-6)+"px";}
572
+
573
+ function boot(model){
574
+ M=model;
575
+ LANG=pickLang(M);STR=STRINGS[LANG];
576
+ try{document.documentElement.setAttribute("lang",LANG);}catch(e){}
577
+ applyStrings();
578
+ if(!M||!M.nodes||!M.nodes.length){$("err").style.display="block";$("err").textContent=STR.errEmpty;return;}
579
+ if(M.source&&M.source.repo)$("title").textContent="Forma · "+M.source.repo;
580
+ var m=M.meta||{};$("stamp").textContent=STR.stampBase+" "+(m.verifiedAt||(M.source&&M.source.commit)||"—")+" · "+(m.verifyMethod||STR.stampVerify);
581
+ setSkin((new URLSearchParams(location.search).get("skin"))||window.__C4_SKIN__||(m.skin)||"holo");
582
+ stack=[null];mode="now";POS={};
583
+ draw(true);
584
+ }
585
+ function start(){
586
+ // Pick language from URL/global up-front so header + errors render even if the model never loads.
587
+ LANG=pickLang(null);STR=STRINGS[LANG];
588
+ try{document.documentElement.setAttribute("lang",LANG);}catch(e){}
589
+ applyStrings();
590
+ wire();
591
+ if(window.__C4_MODEL__){boot(window.__C4_MODEL__);return;}
592
+ try{fetch("./c4-model.json").then(function(r){return r.json();}).then(boot).catch(function(){
593
+ $("err").style.display="block";$("err").textContent=STR.errNotFound;});}
594
+ catch(e){$("err").style.display="block";$("err").textContent=STR.errUnavail;}
595
+ }
596
+ if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",start);else start();
597
+ })();
283
598
  </script>
284
599
  </body>
285
600
  </html>