Yachay commited on
Commit
4062afc
·
1 Parent(s): e2afcae

feat(roster): ship video-game-style 3D character-select roster

Browse files

Replaces the flat 5-tile grid + static AI-illustration hero banner with a
live Three.js (r160) character-select scene: five procedural low-poly heroes
(AMARU, SENTRA, ROSIE, KILLINCHU, A11OY) on colored rim-lit daises in an arc,
UDS NPCs orbiting in the background, KHIPU-style particle motes, slow camera
carousel orbit. Hover reveals animated superpower panels + activates each
hero's prop and dims the rest; click opens the hero's HF Space. Fully
procedural geometry + canvas-painted UDS swag (zero external assets). WebGL
fallback to the existing 5-tile SVG grid. prefers-reduced-motion honored.

Files: index.html, js/{main,heroes,uds_npcs,hover_panel,superpower_icons,canvas_decals}.js, css/style.css

Author: Yachay <yachay@szlholdings.dev>
Co-Authored-By: Perplexity Computer Agent <agent@perplexity.ai>
Signed-off-by: Yachay <yachay@szlholdings.dev>

ADDITIVE only; no force push. Doctrine v11 LOCKED 749/14/163 unchanged.
Lambda stays Conjecture 1. SLSA L1 honest. README frontmatter untouched.

Files changed (8) hide show
  1. css/style.css +85 -0
  2. index.html +50 -568
  3. js/canvas_decals.js +195 -0
  4. js/heroes.js +219 -0
  5. js/hover_panel.js +84 -0
  6. js/main.js +344 -0
  7. js/superpower_icons.js +105 -0
  8. js/uds_npcs.js +64 -0
css/style.css ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* style.css — minimal, mostly defaults. SZL palette: deep purple + gold. */
2
+ :root{
3
+ --purple-deep:#12091f; --purple-mid:#2d1b4e; --gold:#d4af37; --gold-light:#e8cc6a;
4
+ --text-main:#e8e0f0; --text-muted:#a090c0; --text-dim:#7060a0;
5
+ --font-head:'Cinzel',Georgia,serif; --font-mono:'JetBrains Mono','Fira Code',monospace;
6
+ }
7
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
8
+ html,body{height:100%;}
9
+ body{
10
+ background:var(--purple-deep);color:var(--text-main);
11
+ font-family:'Inter',system-ui,-apple-system,sans-serif;line-height:1.6;
12
+ -webkit-font-smoothing:antialiased;overflow-x:hidden;
13
+ background-image:radial-gradient(circle at 50% -10%,rgba(61,40,120,0.5),transparent 55%),
14
+ radial-gradient(circle at 90% 10%,rgba(212,175,55,0.07),transparent 45%);
15
+ }
16
+ .skip{position:absolute;left:-9999px;top:0.5rem;z-index:99;padding:.5rem 1rem;background:var(--gold);color:#150b22;font-weight:700;border-radius:4px;}
17
+ .skip:focus{left:1rem;}
18
+
19
+ /* Header / footer banding */
20
+ .roster-header,.roster-footer{text-align:center;font-family:var(--font-mono);letter-spacing:.18em;
21
+ text-transform:uppercase;color:var(--gold-light);padding:1.1rem 1rem .6rem;font-size:clamp(.62rem,1.6vw,.86rem);}
22
+ .roster-footer{color:var(--text-muted);padding:.7rem 1rem 1.4rem;font-size:clamp(.56rem,1.4vw,.74rem);}
23
+ .roster-footer b{color:var(--gold);}
24
+ .tagline{text-align:center;font-family:var(--font-head);color:var(--gold);
25
+ font-size:clamp(1.1rem,3vw,1.8rem);padding:.4rem 1rem 0;}
26
+ .tagline small{display:block;font-family:var(--font-mono);text-transform:none;letter-spacing:.04em;
27
+ font-size:.62rem;color:var(--text-dim);margin-top:.4rem;}
28
+
29
+ /* The 3D stage */
30
+ #stage{position:relative;width:100%;height:min(70vh,640px);min-height:420px;}
31
+ #roster{display:block;width:100%;height:100%;}
32
+ #loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
33
+ font-family:var(--font-mono);color:var(--text-dim);font-size:.8rem;letter-spacing:.1em;}
34
+
35
+ /* Holographic hover panel */
36
+ .hover-panel{position:absolute;transform:translate(-50%,-100%);min-width:240px;max-width:300px;
37
+ padding:.8rem .9rem;pointer-events:none;opacity:0;transition:opacity .18s ease-out;z-index:40;
38
+ background:linear-gradient(180deg,rgba(18,9,31,.92),rgba(18,9,31,.78));
39
+ border:1px solid var(--hc,#d4af37);border-radius:10px;
40
+ box-shadow:0 0 24px -4px var(--hc,#d4af37),inset 0 0 18px -8px var(--hc,#d4af37);
41
+ backdrop-filter:blur(6px);}
42
+ .hover-panel.hp-visible{opacity:1;}
43
+ .hover-panel::after{content:'';position:absolute;left:50%;bottom:-7px;transform:translateX(-50%);
44
+ width:0;height:0;border:7px solid transparent;border-top-color:var(--hc,#d4af37);}
45
+ .hp-name{font-family:var(--font-mono);font-weight:700;font-size:1.05rem;letter-spacing:.08em;}
46
+ .hp-role{font-size:.62rem;color:var(--text-muted);margin:.1rem 0 .5rem;}
47
+ .hp-head{font-family:var(--font-mono);font-size:.6rem;letter-spacing:.22em;color:var(--text-dim);
48
+ border-top:1px solid rgba(255,255,255,.12);padding-top:.4rem;margin-bottom:.4rem;}
49
+ .hp-list{list-style:none;display:flex;flex-direction:column;gap:.4rem;}
50
+ .hp-line{display:flex;align-items:center;gap:.5rem;font-family:var(--font-mono);font-size:.72rem;
51
+ color:var(--text-main);min-height:22px;opacity:.25;}
52
+ .hp-line.hp-shown{opacity:1;}
53
+ .hp-line.hp-pulse{animation:hp-pulse 1.4s ease-in-out infinite;}
54
+ @keyframes hp-pulse{0%,100%{filter:none;}50%{filter:drop-shadow(0 0 5px var(--hc));}}
55
+ .sp-icon{display:inline-flex;width:22px;height:22px;flex:0 0 22px;}
56
+ .sp-static *{animation:none !important;}
57
+ .hp-text{white-space:nowrap;overflow:hidden;}
58
+
59
+ /* Accessible focus proxies (visually hidden but focusable) */
60
+ .a11y-heroes{position:absolute;width:1px;height:1px;overflow:hidden;}
61
+ .a11y-heroes button{width:1px;height:1px;}
62
+ .a11y-hero:focus{position:fixed;left:8px;top:8px;width:auto;height:auto;z-index:200;
63
+ padding:.4rem .7rem;background:var(--gold);color:#150b22;border:0;border-radius:4px;
64
+ font-family:var(--font-mono);font-size:.7rem;}
65
+
66
+ /* SVG fallback grid (WebGL unavailable) */
67
+ #fallback[hidden]{display:none;}
68
+ #fallback{max-width:1100px;margin:1rem auto;padding:0 1rem;}
69
+ .fb-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1rem;}
70
+ .fb-cell{display:flex;flex-direction:column;align-items:center;text-decoration:none;
71
+ background:#0a0e14;border:1px solid rgba(212,175,55,.18);border-radius:14px;padding:.9rem;
72
+ transition:transform .2s,border-color .2s;}
73
+ .fb-cell:hover,.fb-cell:focus-visible{transform:translateY(-4px);border-color:var(--gold);}
74
+ .fb-cell img{width:100%;aspect-ratio:1/1;border-radius:12px;}
75
+ .fb-name{font-family:var(--font-head);color:var(--gold);margin-top:.5rem;}
76
+ .fb-role{font-family:var(--font-mono);font-size:.6rem;color:var(--text-dim);
77
+ text-transform:uppercase;letter-spacing:.06em;text-align:center;}
78
+ @media(max-width:900px){.fb-grid{grid-template-columns:repeat(3,1fr);}}
79
+ @media(max-width:560px){.fb-grid{grid-template-columns:repeat(2,1fr);}}
80
+
81
+ @media(prefers-reduced-motion:reduce){
82
+ .hover-panel{transition:none;}
83
+ .hp-line.hp-pulse{animation:none;}
84
+ .sp-anim *{animation:none !important;}
85
+ }
index.html CHANGED
@@ -2,579 +2,61 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8"/>
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=yes"/>
6
- <meta name="apple-mobile-web-app-capable" content="yes"/>
7
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
8
- <meta name="theme-color" content="#1a0d2e"/>
9
- <title>SZL Holdings — Governed Agentic Mesh</title>
10
- <meta name="description" content="SZL Holdings: a governed agentic mesh. rosie commands a11oy; a11oy enforces; amaru remembers; sentra guards; vessels carries the load. Doctrine v11 (749/14/163), formal Lean proofs, signed receipts."/>
11
- <meta property="og:title" content="SZL Holdings — Governed Agentic Mesh"/>
12
- <meta property="og:description" content="Five organs, ten sub-organs. Every call leaves a receipt. Doctrine v11 LOCKED · 749/14/163."/>
13
  <meta property="og:image" content="https://huggingface.co/spaces/SZLHOLDINGS/README/resolve/main/assets/szl_banner.png"/>
14
  <meta property="og:type" content="website"/>
15
  <link rel="preconnect" href="https://fonts.googleapis.com"/>
16
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
17
- <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
18
- <style>
19
- /* =========================================================
20
- SZL SHOWCASE — inherits amaru-platform design language
21
- Palette: deep purple #1a0d2e · gold #d4af37
22
- ========================================================= */
23
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
24
- :root{
25
- --purple-deep:#1a0d2e; --purple-mid:#2d1b4e; --purple-light:#3d2878;
26
- --gold:#d4af37; --gold-light:#e8cc6a; --gold-dim:#b8952e;
27
- --text-main:#e8e0f0; --text-muted:#a090c0; --text-dim:#7060a0;
28
- --border:rgba(212,175,55,0.18); --glass:rgba(45,27,78,0.55);
29
- --coral:#ff7a59; --cyan:#00d4ff; --green:#3ddc84; --ocean:#0099cc;
30
- --font-head:'Cinzel','Palatino Linotype',Georgia,serif;
31
- --font-body:'Inter',system-ui,-apple-system,sans-serif;
32
- --font-mono:'JetBrains Mono','Fira Code',monospace;
33
- --radius:12px; --radius-lg:20px; --content-max:1180px;
34
- }
35
- html{scroll-behavior:smooth;font-size:16px;}
36
- body{background:var(--purple-deep);color:var(--text-main);font-family:var(--font-body);line-height:1.7;-webkit-font-smoothing:antialiased;
37
- background-image:radial-gradient(circle at 20% -10%,rgba(61,40,120,0.45),transparent 45%),radial-gradient(circle at 90% 10%,rgba(212,175,55,0.06),transparent 40%);}
38
- h1,h2,h3,h4{font-family:var(--font-head);letter-spacing:0.03em;line-height:1.2;}
39
- h1{font-size:clamp(2.4rem,6vw,4.2rem);color:var(--gold);}
40
- h2{font-size:clamp(1.6rem,3.5vw,2.5rem);color:var(--gold-light);margin-bottom:0.4rem;}
41
- h3{font-size:1.1rem;color:var(--gold);margin-bottom:0.35rem;}
42
- h4{font-size:0.8rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.12em;}
43
- p{margin-bottom:1rem;color:var(--text-main);}
44
- a{color:var(--gold);text-decoration:none;border-bottom:1px solid transparent;transition:border-color .2s,color .2s;}
45
- a:hover{border-color:var(--gold);}
46
- code,.mono{font-family:var(--font-mono);font-size:0.85em;background:rgba(212,175,55,0.08);padding:0.12em 0.45em;border-radius:5px;color:var(--gold-light);}
47
- strong{color:var(--gold-light);font-weight:600;}
48
- .container{max-width:var(--content-max);margin:0 auto;padding:0 1.6rem;}
49
- section{padding:5rem 0;position:relative;}
50
- section+section{border-top:1px solid var(--border);}
51
- .gold-bar{width:54px;height:3px;background:linear-gradient(90deg,var(--gold),transparent);margin:0.6rem 0 2rem;border-radius:2px;}
52
- .section-label{font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.2em;text-transform:uppercase;color:var(--gold-dim);}
53
- .skip{position:absolute;left:-9999px;top:1rem;z-index:9999;padding:0.5rem 1.25rem;background:var(--gold);color:var(--purple-deep);font-weight:700;border-radius:4px;}
54
- .skip:focus{left:1rem;}
55
- /* NAV */
56
- nav{position:sticky;top:0;z-index:100;background:rgba(26,13,46,0.82);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);}
57
- .nav-inner{max-width:var(--content-max);margin:0 auto;padding:0.7rem 1.6rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;}
58
- .nav-logo{display:flex;align-items:center;gap:0.5rem;font-family:var(--font-head);font-weight:700;color:var(--gold);font-size:1.05rem;}
59
- .nav-links{display:flex;gap:1.05rem;flex-wrap:wrap;font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.08em;text-transform:uppercase;}
60
- .nav-links a{color:var(--text-muted);border:none;}
61
- .nav-links a:hover{color:var(--gold-light);}
62
- /* HERO */
63
- #hero{padding-top:4rem;}
64
- .hero-eyebrow{font-family:var(--font-mono);font-size:0.75rem;letter-spacing:0.18em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:1rem;}
65
- .pill{display:inline-block;font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.05em;padding:0.32rem 0.85rem;border-radius:999px;border:1px solid var(--border);background:var(--glass);color:var(--text-muted);margin:0.2rem 0.3rem 0.2rem 0;}
66
- .pill.warn{border-color:rgba(255,122,89,0.4);color:#ffb39e;}
67
- .pill.gold{border-color:var(--gold);color:var(--gold-light);}
68
- .hero-tagline{font-size:clamp(1.05rem,2.2vw,1.4rem);color:var(--text-main);max-width:760px;margin:1.1rem auto 1.4rem;}
69
- .hero-grid{display:grid;grid-template-columns:1.05fr 0.95fr;gap:2.5rem;align-items:center;}
70
- .hero-img{width:100%;max-width:540px;border-radius:var(--radius-lg);border:1px solid var(--border);background:#fff;}
71
- .ctas{display:flex;gap:0.8rem;flex-wrap:wrap;margin-top:1.6rem;}
72
- .btn{font-family:var(--font-body);font-weight:600;font-size:0.92rem;padding:0.7rem 1.3rem;border-radius:var(--radius);border:1px solid var(--border);transition:transform .15s,box-shadow .15s;}
73
- .btn-primary{background:linear-gradient(135deg,var(--gold),var(--gold-dim));color:var(--purple-deep);border-color:var(--gold);}
74
- .btn-primary:hover{transform:translateY(-2px);box-shadow:0 8px 24px rgba(212,175,55,0.25);border-color:var(--gold);}
75
- .btn-secondary{background:var(--glass);color:var(--text-main);}
76
- .btn-secondary:hover{transform:translateY(-2px);border-color:var(--gold);}
77
- /* ORGAN GRID */
78
- .organ-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1.2rem;margin-top:1.5rem;}
79
- .organ-card{background:var(--glass);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.2rem 1rem;text-align:center;transition:transform .18s,border-color .18s,box-shadow .18s;display:flex;flex-direction:column;}
80
- .organ-card:hover{transform:translateY(-4px);border-color:var(--accent,var(--gold));box-shadow:0 12px 30px rgba(0,0,0,0.35);}
81
- .organ-card img{width:118px;height:118px;border-radius:50%;margin:0 auto 0.7rem;border:2px solid var(--accent,var(--gold));}
82
- .organ-name{font-family:var(--font-head);font-size:1.25rem;color:var(--gold);margin-bottom:0.15rem;}
83
- .organ-role{font-size:0.82rem;color:var(--text-muted);min-height:2.4em;}
84
- .organ-quote{font-family:var(--font-mono);font-size:0.72rem;color:var(--text-main);background:rgba(255,255,255,0.04);border-radius:8px;padding:0.5rem;margin:0.7rem 0;min-height:3.4em;}
85
- .organ-facts{font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim);letter-spacing:0.02em;margin-bottom:0.8rem;}
86
- .organ-card a.go{margin-top:auto;font-size:0.8rem;font-weight:600;}
87
- /* SUB-ORGAN GRID */
88
- .sub-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:0.8rem;margin-top:1.5rem;}
89
- .sub-card{background:var(--glass);border:1px solid var(--border);border-radius:var(--radius);padding:0.85rem;font-size:0.8rem;}
90
- .sub-card b{color:var(--gold-light);font-size:0.92rem;}
91
- .sub-card .where{display:block;font-family:var(--font-mono);font-size:0.66rem;color:var(--text-dim);margin-top:0.4rem;letter-spacing:0.04em;}
92
- /* PhD STRIP */
93
- .phd-note{font-size:0.85rem;color:var(--text-muted);background:rgba(255,122,89,0.07);border:1px solid rgba(255,122,89,0.25);border-radius:var(--radius);padding:0.7rem 1rem;margin-bottom:1.4rem;}
94
- .phd-strip{display:flex;gap:1rem;overflow-x:auto;padding:0.6rem 0.2rem 1.2rem;scroll-snap-type:x mandatory;}
95
- .phd-badge{flex:0 0 220px;scroll-snap-align:start;background:var(--glass);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.1rem;text-align:center;transition:transform .18s,border-color .18s;}
96
- .phd-badge:hover{transform:translateY(-4px);border-color:var(--gold);}
97
- .phd-badge img{width:120px;height:120px;border-radius:50%;margin:0 auto 0.6rem;display:block;}
98
- .phd-badge .title{font-family:var(--font-head);color:var(--gold);font-size:1.05rem;margin-bottom:0.4rem;}
99
- .phd-badge .ship{font-size:0.76rem;color:var(--text-muted);line-height:1.5;}
100
- /* STATE TABLE */
101
- table.state{width:100%;border-collapse:collapse;margin-top:1.2rem;font-size:0.88rem;}
102
- table.state th,table.state td{text-align:left;padding:0.6rem 0.8rem;border-bottom:1px solid var(--border);}
103
- table.state th{font-family:var(--font-mono);font-size:0.7rem;letter-spacing:0.1em;text-transform:uppercase;color:var(--gold-dim);}
104
- table.state td:nth-child(2){font-family:var(--font-mono);color:var(--gold-light);}
105
- /* FOOTER */
106
- footer{padding:3rem 0 4rem;border-top:1px solid var(--border);font-size:0.85rem;color:var(--text-muted);}
107
- footer .links{display:flex;flex-wrap:wrap;gap:0.6rem 1.2rem;margin:1rem 0;font-family:var(--font-mono);font-size:0.8rem;}
108
- footer .built{font-family:var(--font-mono);font-size:0.74rem;color:var(--text-dim);margin-top:1rem;letter-spacing:0.03em;}
109
- /* RESPONSIVE */
110
- @media(max-width:980px){
111
- .hero-grid{grid-template-columns:1fr;text-align:center;}
112
- .hero-img{margin:0 auto;}
113
- .ctas{justify-content:center;}
114
- .organ-grid{grid-template-columns:repeat(2,1fr);}
115
- .sub-grid{grid-template-columns:repeat(2,1fr);}
116
- }
117
- @media(max-width:560px){
118
- .organ-grid{grid-template-columns:1fr;}
119
- .sub-grid{grid-template-columns:1fr;}
120
- }
121
- /* =========================================================
122
- AMBIENT EMOJI LAYER (Wave4 — additive)
123
- 5 character emojis hanging around the page edges, animated.
124
- Additive layer: does NOT touch the banner or the organ-card
125
- painterly avatars. Uses transform/opacity (GPU compositor).
126
- z-index 40: above the page background, below nav (100) + skip (9999).
127
- ========================================================= */
128
- .emoji-layer{position:fixed;inset:0;z-index:40;pointer-events:none;overflow:hidden;}
129
- /* outer wrapper = resting spot + slow drift; inner img = breathing + hover pulse */
130
- .ambient-emoji{position:fixed;width:84px;height:84px;pointer-events:auto;}
131
- .ae-inner{display:block;width:100%;height:100%;opacity:0.9;
132
- filter:drop-shadow(0 4px 14px rgba(0,0,0,0.45));will-change:transform,opacity;
133
- transition:transform .25s ease,filter .25s ease,opacity .25s ease;}
134
- /* fixed resting spots — corners/edges, clear of the top-center banner */
135
- .ae-a11oy{top:88px;left:22px;} /* upper-left, below nav */
136
- .ae-rosie{top:118px;right:26px;} /* upper-right */
137
- .ae-amaru{top:46%;left:14px;} /* mid-left edge */
138
- .ae-sentra{top:54%;right:16px;} /* mid-right edge */
139
- .ae-vessels{bottom:42px;right:40px;} /* lower-right */
140
- /* hover + keyboard focus: non-motion cue (brighten) survives reduced-motion */
141
- .ambient-emoji:hover .ae-inner,.ambient-emoji:focus-visible .ae-inner{opacity:1;
142
- filter:drop-shadow(0 6px 20px rgba(212,175,55,0.55));}
143
- .ambient-emoji:focus-visible{outline:2px solid var(--gold);outline-offset:4px;border-radius:50%;}
144
- /* shrink + tuck on small screens so emojis never block reading */
145
- @media(max-width:760px){
146
- .ambient-emoji{width:54px;height:54px;opacity:0.8;}
147
- .ae-amaru,.ae-sentra{display:none;}
148
- }
149
-
150
- /* Motion enabled for users who have NOT requested reduced motion */
151
- @media(prefers-reduced-motion:no-preference){
152
- @keyframes ae-breathe{0%,100%{transform:translateY(0) scale(1);opacity:0.88;}
153
- 50%{transform:translateY(-6px) scale(1.05);opacity:1;}}
154
- @keyframes ae-drift-a{0%{transform:translate3d(0,0,0);}25%{transform:translate3d(10px,-8px,0);}
155
- 50%{transform:translate3d(4px,8px,0);}75%{transform:translate3d(-8px,4px,0);}100%{transform:translate3d(0,0,0);}}
156
- @keyframes ae-drift-b{0%{transform:translate3d(0,0,0);}30%{transform:translate3d(-12px,6px,0);}
157
- 60%{transform:translate3d(6px,-10px,0);}100%{transform:translate3d(0,0,0);}}
158
- @keyframes ae-pulse{0%{transform:scale(1);}40%{transform:scale(1.16);}100%{transform:scale(1);}}
159
- /* idle breathing on the inner image; slow 30s drift on the wrapper */
160
- .ae-inner{animation:ae-breathe 5.5s ease-in-out infinite;}
161
- .ambient-emoji{animation:ae-drift-a 30s ease-in-out infinite;}
162
- .ae-a11oy .ae-inner{animation-delay:-0.2s;}
163
- .ae-rosie .ae-inner{animation-delay:-1.4s;}
164
- .ae-amaru .ae-inner{animation-delay:-2.6s;}
165
- .ae-sentra .ae-inner{animation-delay:-3.8s;}
166
- .ae-vessels .ae-inner{animation-delay:-4.9s;}
167
- .ae-rosie,.ae-sentra{animation-name:ae-drift-b;}
168
- .ambient-emoji:hover .ae-inner,.ambient-emoji:focus-visible .ae-inner{animation:ae-pulse .6s ease-in-out;}
169
- }
170
-
171
- @media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important;}
172
- .ae-inner{animation:none!important;}.ambient-emoji{animation:none!important;}}
173
-
174
- /* =========================================================
175
- MISSION ROOM — joint cyber-ops action band (Wave4, additive)
176
- Replaces the static team picture. Six characters, each an
177
- animated WebP looping its own cyber task. The looping is baked
178
- into each WebP; CSS adds just a very slight ambient float that
179
- is fully disabled under prefers-reduced-motion. A static
180
- first-frame WebP is stacked underneath and revealed when motion
181
- is reduced (the animated layer is hidden in that case).
182
- ========================================================= */
183
- #mission-room{padding:2.4rem 0 1.6rem;border-top:none;text-align:center;}
184
- .mr-head{font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.18em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:0.2rem;}
185
- .mr-band{display:flex;justify-content:center;align-items:flex-end;gap:0.3rem;flex-wrap:wrap;
186
- padding:0.6rem 0 0.4rem;margin:0 auto;max-width:1140px;}
187
- .mr-unit{position:relative;width:clamp(120px,15vw,178px);height:clamp(120px,15vw,178px);
188
- display:flex;align-items:flex-end;justify-content:center;}
189
- .mr-unit img{display:block;width:100%;height:100%;object-fit:contain;
190
- filter:drop-shadow(0 6px 16px rgba(0,0,0,0.45));}
191
- /* the static layer sits beneath the moving layer; hidden by default */
192
- .mr-static{position:absolute;inset:0;opacity:0;}
193
- .mr-cap{font-family:var(--font-mono);font-size:0.74rem;letter-spacing:0.14em;
194
- color:var(--text-muted);margin-top:0.5rem;}
195
- .mr-cap b{color:var(--gold-light);font-weight:500;}
196
- @media(max-width:760px){
197
- .mr-band{gap:0.2rem;}
198
- .mr-unit{width:clamp(92px,29vw,120px);height:clamp(92px,29vw,120px);}
199
- }
200
- /* ambient float for motion-OK users; offset per unit so the band is alive, not in lock-step */
201
- @media(prefers-reduced-motion:no-preference){
202
- @keyframes mr-bob{0%,100%{transform:translateY(0);}50%{transform:translateY(-7px);}}
203
- @keyframes mr-blip{0%,92%,100%{opacity:0;transform:scale(0.6);}96%{opacity:0.9;transform:scale(1);}}
204
- .mr-unit{animation:mr-bob 4.6s ease-in-out infinite;}
205
- .mr-unit:nth-child(1){animation-delay:-0.1s;}
206
- .mr-unit:nth-child(2){animation-delay:-1.3s;animation-duration:5.1s;}
207
- .mr-unit:nth-child(3){animation-delay:-2.4s;animation-duration:4.2s;}
208
- .mr-unit:nth-child(4){animation-delay:-3.0s;animation-duration:5.4s;}
209
- .mr-unit:nth-child(5){animation-delay:-1.9s;animation-duration:4.8s;}
210
- .mr-unit:nth-child(6){animation-delay:-3.7s;animation-duration:5.0s;}
211
- /* occasional sparkle blip near a couple of units */
212
- .mr-unit::after{content:"";position:absolute;top:8%;right:10%;width:7px;height:7px;border-radius:50%;
213
- background:var(--gold-light);box-shadow:0 0 8px var(--gold-light);opacity:0;pointer-events:none;}
214
- .mr-unit:nth-child(1)::after{animation:mr-blip 7.5s ease-in-out infinite;}
215
- .mr-unit:nth-child(4)::after{animation:mr-blip 9.2s ease-in-out infinite 2s;}
216
- }
217
- /* reduced motion: kill float + sparkle, hide the animated frame, show the still */
218
- @media(prefers-reduced-motion:reduce){
219
- .mr-unit{animation:none!important;}
220
- .mr-unit::after{display:none!important;}
221
- .mr-anim{opacity:0!important;}
222
- .mr-static{opacity:1!important;}
223
- }
224
- </style>
225
-
226
- <style id="szl-mobile-card-safety">
227
- /* SZL org-card mobile safety net (ADDITIVE — Yachay). Overrides via later cascade. */
228
- html, body { -webkit-tap-highlight-color: transparent; }
229
- body { max-width: 100vw; overflow-x: hidden; }
230
- img, canvas, svg, video, iframe { max-width: 100%; height: auto; }
231
- /* intermediate tablet/large-phone layout: 5-col -> 2-col before the 560px 1-col fallback */
232
- @media (max-width: 900px) and (min-width: 561px) {
233
- .organ-grid, .sub-grid { grid-template-columns: repeat(2, 1fr) !important; }
234
- }
235
- @media (max-width: 560px) {
236
- body { font-size: 16px; }
237
- h1 { font-size: 24px; line-height: 1.2; }
238
- a, button, [role="button"] { min-height: 44px; }
239
- .organ-grid, .sub-grid { gap: 0.9rem; }
240
- }
241
- </style>
242
  </head>
243
  <body>
244
- <!-- ================= SZL 3D HERO FIGURES (Three.js) — replaces chibi 2D avatars =================
245
- Five distinct procedural low-poly 3D heroes, one per organ, each in its own scene cell.
246
- Click a figure -> opens that organ's live HF Space. Mobile-first per SZL_MOBILE_FIRST_STANDARD.
247
- Aesthetic: dark slate #0a0e14 cells, warm gold #d4a444 accents, white emissive highlights.
248
- Apache-2.0 · Signed Yachay <yachay@szlholdings.dev> · Doctrine v11 LOCKED (749/14/163).
249
- ADDITIVE: this block replaces ONLY the chibi avatar portrait row. -->
250
- <section id="szl-3d-heroes" style="padding:3rem 0 1rem;text-align:center;">
251
- <div class="container">
252
- <p class="mr-head" style="font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.18em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:0.2rem;">The Mesh · Five Organs · Live 3D</p>
253
- <div id="szl-hero-grid" role="group" aria-label="SZL Holdings five organs, interactive 3D figures">
254
- <a class="szl-hero-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/amaru" data-organ="amaru" aria-label="amaru — memory cortex (serpent + crystal core), open Space"><canvas data-organ="amaru" style="background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMTEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgb3BhY2l0eT0iMC45NSI+PGVsbGlwc2UgY3g9IjEyOCIgY3k9IjE5MCIgcng9IjU4IiByeT0iMTciLz48ZWxsaXBzZSBjeD0iMTI4IiBjeT0iMTUwIiByeD0iNTAiIHJ5PSIxNSIvPjxlbGxpcHNlIGN4PSIxMjgiIGN5PSIxMTIiIHJ4PSI0MCIgcnk9IjEyIi8+PGVsbGlwc2UgY3g9IjEyOCIgY3k9IjgwIiByeD0iMjgiIHJ5PSI5Ii8+PC9nPjxwb2x5Z29uIHBvaW50cz0iMTI4LDk4IDE1MCwxMzIgMTI4LDE1NiAxMDYsMTMyIiBmaWxsPSIjZThjYzZhIi8+PHBvbHlnb24gcG9pbnRzPSIxMjgsNjAgMTQ0LDg0IDEyOCw5OCAxMTIsODQiIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSIxNDIiIGN5PSI3NCIgcj0iNCIgZmlsbD0iI2Y0ZWVkZSIvPjwvc3ZnPg==');background-size:cover;background-position:center;"></canvas><span class="szl-hero-name">amaru</span><span class="szl-hero-role">cortex · serpent</span></a>
255
- <a class="szl-hero-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/sentra" data-organ="sentra" aria-label="sentra — immune shield, open Space"><canvas data-organ="sentra" style="background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIwLjkiPjxwb2x5Z29uIHBvaW50cz0iMTI4LDU2IDE5NiwxMDQgMTcwLDE4MiA4NiwxODIgNjAsMTA0Ii8+PHBvbHlnb24gcG9pbnRzPSIxMjgsNTYgMTcwLDE4MiA2MCwxMDQiLz48cG9seWdvbiBwb2ludHM9IjEyOCw1NiA4NiwxODIgMTk2LDEwNCIvPjxsaW5lIHgxPSI2MCIgeTE9IjEwNCIgeDI9IjE5NiIgeTI9IjEwNCIvPjxsaW5lIHgxPSIxMjgiIHkxPSI1NiIgeDI9IjEyOCIgeTI9IjE4MiIvPjwvZz48cG9seWdvbiBwb2ludHM9IjEwMCwxMTggMTU2LDExOCAxMjgsMTUwIiBmaWxsPSIjZDRhNDQ0IiBvcGFjaXR5PSIwLjU1Ii8+PGNpcmNsZSBjeD0iNjAiIGN5PSIxMzgiIHI9IjYiIGZpbGw9IiNmZjVhNDQiLz48Y2lyY2xlIGN4PSIyMDAiIGN5PSIxMjAiIHI9IjYiIGZpbGw9IiNmZjVhNDQiLz48Y2lyY2xlIGN4PSI3OCIgY3k9IjkyIiByPSI1IiBmaWxsPSIjNjY4OGZmIi8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iMTIwIiByPSIzNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZThjYzZhIiBzdHJva2Utd2lkdGg9IjQiIG9wYWNpdHk9IjAuOCIvPjwvc3ZnPg==');background-size:cover;background-position:center;"></canvas><span class="szl-hero-name">sentra</span><span class="szl-hero-role">immune · shield</span></a>
256
- <a class="szl-hero-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/rosie" data-organ="rosie" aria-label="rosie operator console (HUD rings), open Space"><canvas data-organ="rosie" style="background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48Y2lyY2xlIGN4PSIxMjgiIGN5PSIxMjgiIHI9IjQ0IiBmaWxsPSIjMTExNjFmIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMyIvPjxjaXJjbGUgY3g9IjEyOCIgY3k9IjEyOCIgcj0iMjIiIGZpbGw9IiNmNGVlZGUiIG9wYWNpdHk9IjAuOTIiLz48ZyBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBvcGFjaXR5PSIwLjg1Ij48bGluZSB4MT0iNjAiIHkxPSIxNzAiIHgyPSI2MCIgeTI9IjEyMCIvPjxsaW5lIHgxPSIxMDAiIHkxPSIxODAiIHgyPSIxMDAiIHkyPSIxMTAiLz48bGluZSB4MT0iMTU2IiB5MT0iMTgwIiB4Mj0iMTU2IiB5Mj0iMTEwIi8+PGxpbmUgeDE9IjE5NiIgeTE9IjE3MCIgeDI9IjE5NiIgeTI9IjEyMCIvPjwvZz48cmVjdCB4PSI1NiIgeT0iMTc2IiB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjgiIHJ4PSI0IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iMTI4IiByPSI2MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZThjYzZhIiBzdHJva2Utd2lkdGg9IjEuNiIgb3BhY2l0eT0iMC41Ii8+PC9zdmc+');background-size:cover;background-position:center;"></canvas><span class="szl-hero-name">rosie</span><span class="szl-hero-role">operator · console</span></a>
257
- <a class="szl-hero-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/killinchu" data-organ="killinchu" aria-label="killinchu kestrel drone intel, open Space"><canvas data-organ="killinchu" style="background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48cGF0aCBkPSJNNzAgMTQwIFExMjggOTYgMTg2IDE0MCBRMTUwIDEyOCAxMjggMTI4IFExMDYgMTI4IDcwIDE0MFoiIGZpbGw9IiNkNGE0NDQiLz48cG9seWdvbiBwb2ludHM9IjEyOCwxMTIgMTM0LDEyOCAxMjIsMTI4IiBmaWxsPSIjZThjYzZhIi8+PGcgZmlsbD0iI2Y0ZWVkZSIgb3BhY2l0eT0iMC45Ij48Y2lyY2xlIGN4PSI2NCIgY3k9IjEyMCIgcj0iMy4yIi8+PGNpcmNsZSBjeD0iOTIiIGN5PSI5NiIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTYwIiBjeT0iOTIiIHI9IjMiLz48Y2lyY2xlIGN4PSIxOTYiIGN5PSIxMTgiIHI9IjMuNCIvPjxjaXJjbGUgY3g9IjE1MCIgY3k9IjE2MCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTAwIiBjeT0iMTY4IiByPSIzIi8+PGNpcmNsZSBjeD0iMjAwIiBjeT0iMTUwIiByPSIyLjQiLz48Y2lyY2xlIGN4PSI1OCIgY3k9IjE1OCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iNzgiIHI9IjIuNCIvPjwvZz48ZyBmaWxsPSIjNjY4OGZmIiBvcGFjaXR5PSIwLjgiPjxjaXJjbGUgY3g9IjEyMCIgY3k9IjE1MCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTcwIiBjeT0iMTQwIiByPSIyLjYiLz48L2c+PC9zdmc+');background-size:cover;background-position:center;"></canvas><span class="szl-hero-name">killinchu</span><span class="szl-hero-role">kestrel · drone</span></a>
258
- <a class="szl-hero-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/a11oy" data-organ="a11oy" aria-label="a11oy — router knot-graph (Khipu cords), open Space"><canvas data-organ="a11oy" style="background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMi4yIiBvcGFjaXR5PSIwLjg1Ij48bGluZSB4MT0iMTI4IiB5MT0iNzIiIHgyPSIxODYiIHkyPSIxMDQiLz48bGluZSB4MT0iMTg2IiB5MT0iMTA0IiB4Mj0iMTk2IiB5Mj0iMTY4Ii8+PGxpbmUgeDE9IjE5NiIgeTE9IjE2OCIgeDI9IjE0MCIgeTI9IjE5NiIvPjxsaW5lIHgxPSIxNDAiIHkxPSIxOTYiIHgyPSI4NiIgeTI9IjE4NCIvPjxsaW5lIHgxPSI4NiIgeTE9IjE4NCIgeDI9IjY0IiB5Mj0iMTI4Ii8+PGxpbmUgeDE9IjY0IiB5MT0iMTI4IiB4Mj0iMTI4IiB5Mj0iNzIiLz48bGluZSB4MT0iMTI4IiB5MT0iNzIiIHgyPSI5NiIgeTI9Ijk2Ii8+PGxpbmUgeDE9Ijk2IiB5MT0iOTYiIHgyPSIxNTAiIHkyPSIxNDAiLz48bGluZSB4MT0iMTUwIiB5MT0iMTQwIiB4Mj0iMTg2IiB5Mj0iMTA0Ii8+PGxpbmUgeDE9IjE1MCIgeTE9IjE0MCIgeDI9IjE0MCIgeTI9IjE5NiIvPjxsaW5lIHgxPSI5NiIgeTE9Ijk2IiB4Mj0iNjQiIHkyPSIxMjgiLz48bGluZSB4MT0iMTUwIiB5MT0iMTQwIiB4Mj0iMTk2IiB5Mj0iMTY4Ii8+PC9nPjxjaXJjbGUgY3g9IjEyOCIgY3k9IjcyIiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTg2IiBjeT0iMTA0IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTk2IiBjeT0iMTY4IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTQwIiBjeT0iMTk2IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iODYiIGN5PSIxODQiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSI2NCIgY3k9IjEyOCIgcj0iNyIgZmlsbD0iI2Q0YTQ0NCIvPjxjaXJjbGUgY3g9Ijk2IiBjeT0iOTYiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSIxNTAiIGN5PSIxNDAiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSI5NiIgY3k9Ijk2IiByPSI3IiBmaWxsPSIjZjRlZWRlIi8+PC9zdmc+');background-size:cover;background-position:center;"></canvas><span class="szl-hero-name">a11oy</span><span class="szl-hero-role">router · wires</span></a>
259
- </div>
260
- <p style="font-family:var(--font-mono);font-size:0.68rem;color:var(--text-dim);margin-top:0.9rem;letter-spacing:0.04em;">Interactive 3D where WebGL is supported · rendered preview otherwise · tap a figure to open its Space</p>
261
- </div>
262
- </section>
263
-
264
- <style id="szl-3d-heroes-style">
265
- #szl-hero-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:1.1rem;margin-top:1.4rem;}
266
- .szl-hero-cell{display:flex;flex-direction:column;align-items:center;background:#0a0e14;
267
- border:1px solid rgba(212,164,68,0.22);border-radius:16px;padding:0.6rem 0.5rem 0.9rem;
268
- text-decoration:none;border-bottom:1px solid rgba(212,164,68,0.22);
269
- transition:transform .18s,border-color .18s,box-shadow .18s;min-height:44px;}
270
- .szl-hero-cell:hover,.szl-hero-cell:focus-visible{transform:translateY(-4px);
271
- border-color:#d4a444;box-shadow:0 12px 30px rgba(0,0,0,0.5);outline:none;}
272
- .szl-hero-cell canvas{width:100%;aspect-ratio:1/1;display:block;border-radius:12px;
273
- background:radial-gradient(circle at 50% 40%,#10151f,#06090f 70%);touch-action:none;}
274
- .szl-hero-name{font-family:var(--font-head,'Cinzel',serif);font-size:1.05rem;color:#d4a444;margin-top:0.55rem;}
275
- .szl-hero-role{font-family:var(--font-mono,monospace);font-size:0.62rem;color:#8c7d5e;letter-spacing:0.06em;text-transform:uppercase;}
276
- @media(max-width:900px) and (min-width:561px){#szl-hero-grid{grid-template-columns:repeat(3,1fr);}}
277
- @media(max-width:560px){#szl-hero-grid{grid-template-columns:repeat(2,1fr);gap:0.8rem;}
278
- .szl-hero-cell{width:80vw;max-width:none;margin:0 auto;}
279
- #szl-hero-grid .szl-hero-cell:last-child{grid-column:1 / -1;width:80vw;}}
280
- @media(prefers-reduced-motion:reduce){.szl-hero-cell{transition:none;}}
281
- </style>
282
-
283
- <script type="importmap">
284
- { "imports": { "three": "https://unpkg.com/three@0.171.0/build/three.module.js" } }
285
- </script>
286
- <script type="module">
287
- import * as THREE from 'three';
288
-
289
- const SZL_MOBILE = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
290
- const SZL_REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
291
- const PR = Math.min(window.devicePixelRatio || 1, SZL_MOBILE ? 1.5 : 2);
292
- const GOLD = 0xd4a444, GOLD_HI = 0xe8cc6a, WHITE = 0xffffff, SLATE = 0x0a0e14;
293
-
294
- const cells = [];
295
-
296
- function makeScene(canvas, organ){
297
- const renderer = new THREE.WebGLRenderer({
298
- canvas, antialias: !SZL_MOBILE, alpha: true,
299
- powerPreference: SZL_MOBILE ? 'low-power' : 'high-performance'
300
- });
301
- renderer.setPixelRatio(PR);
302
- // WebGL is live for this cell — drop the SVG poster fallback so the real 3D shows cleanly.
303
- canvas.style.backgroundImage = 'none';
304
- const scene = new THREE.Scene();
305
- scene.background = null;
306
- const cam = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
307
- cam.position.set(0, 0.4, 6.2);
308
-
309
- scene.add(new THREE.AmbientLight(0x404a5e, 1.1));
310
- const key = new THREE.DirectionalLight(GOLD_HI, 1.6); key.position.set(3,4,5); scene.add(key);
311
- const rim = new THREE.DirectionalLight(0x6688ff, 0.7); rim.position.set(-4,-2,-3); scene.add(rim);
312
- const pt = new THREE.PointLight(WHITE, 0.9, 20); pt.position.set(0,0,4); scene.add(pt);
313
-
314
- const root = new THREE.Group(); scene.add(root);
315
- const goldMat = new THREE.MeshStandardMaterial({color:GOLD, metalness:0.85, roughness:0.32, emissive:0x2a1d08, emissiveIntensity:0.4});
316
- const slateMat = new THREE.MeshStandardMaterial({color:0x1b2230, metalness:0.6, roughness:0.5});
317
- const emitMat = new THREE.MeshStandardMaterial({color:WHITE, emissive:WHITE, emissiveIntensity:1.4, metalness:0.2, roughness:0.3});
318
- const goldEmit = new THREE.MeshStandardMaterial({color:GOLD_HI, emissive:GOLD, emissiveIntensity:1.1});
319
-
320
- const extras = {}; // per-organ animated handles
321
-
322
- if(organ==='amaru'){
323
- // serpent (tube) coiled around a glowing crystal core
324
- const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.85,0), emitMat.clone());
325
- core.material.color.setHex(GOLD_HI); core.material.emissive.setHex(GOLD);
326
- root.add(core); extras.core = core;
327
- const pts=[]; const turns=3.2, N=160;
328
- for(let i=0;i<=N;i++){const t=i/N; const a=t*Math.PI*2*turns; const r=1.5-0.35*t;
329
- pts.push(new THREE.Vector3(Math.cos(a)*r, (t-0.5)*3.0, Math.sin(a)*r));}
330
- const tube = new THREE.Mesh(new THREE.TubeGeometry(new THREE.CatmullRomCurve3(pts),200,0.16,10,false), goldMat.clone());
331
- root.add(tube);
332
- const head = new THREE.Mesh(new THREE.ConeGeometry(0.26,0.6,8), goldEmit.clone());
333
- const hp = pts[pts.length-1]; head.position.copy(hp); root.add(head);
334
- const eyeL=new THREE.Mesh(new THREE.SphereGeometry(0.05,8,8),emitMat); eyeL.position.copy(hp).add(new THREE.Vector3(0.12,0.1,0.12)); root.add(eyeL);
335
- }
336
- else if(organ==='sentra'){
337
- // icosahedron shield with hex panels + threat dots absorbed
338
- const shield = new THREE.Mesh(new THREE.IcosahedronGeometry(1.5,1),
339
- new THREE.MeshStandardMaterial({color:0x16202f, metalness:0.7, roughness:0.35, flatShading:true}));
340
- root.add(shield);
341
- const wire = new THREE.LineSegments(new THREE.WireframeGeometry(shield.geometry),
342
- new THREE.LineBasicMaterial({color:GOLD})); root.add(wire);
343
- const lambda = new THREE.Mesh(new THREE.TorusGeometry(0.55,0.07,8,30), goldEmit.clone());
344
- root.add(lambda); extras.lambda=lambda;
345
- const threats=[]; const tg=new THREE.SphereGeometry(0.08,8,8);
346
- const tm=new THREE.MeshStandardMaterial({color:0xff5a44,emissive:0xff5a44,emissiveIntensity:1.2});
347
- for(let i=0;i<8;i++){const m=new THREE.Mesh(tg,tm.clone());
348
- m.userData={a:Math.random()*6.28, r:2.4+Math.random()*0.6, sp:0.6+Math.random()*0.5};
349
- root.add(m); threats.push(m);} extras.threats=threats;
350
- }
351
- else if(organ==='rosie'){
352
- // torus + sphere stack (head + HUD rings) + pulsing data lines
353
- const head = new THREE.Mesh(new THREE.SphereGeometry(0.7,24,24), slateMat.clone());
354
- head.material.color.setHex(0x1d2636); head.position.y=0.2; root.add(head);
355
- const visor = new THREE.Mesh(new THREE.TorusGeometry(0.55,0.09,10,30), goldEmit.clone());
356
- visor.rotation.x=Math.PI/2; visor.position.y=0.2; root.add(visor);
357
- const rings=[];
358
- for(let i=0;i<3;i++){const r=new THREE.Mesh(new THREE.TorusGeometry(1.0+i*0.42,0.025,8,48),
359
- new THREE.MeshStandardMaterial({color:GOLD,emissive:GOLD,emissiveIntensity:0.6,transparent:true,opacity:0.8-i*0.18}));
360
- r.rotation.x=Math.PI/2.1; r.position.y=-0.1; root.add(r); rings.push(r);} extras.rings=rings;
361
- const base=new THREE.Mesh(new THREE.CylinderGeometry(0.9,1.05,0.18,32), slateMat.clone()); base.position.y=-1.25; root.add(base);
362
- // pulsing data lines (vertical)
363
- const lines=[]; for(let i=0;i<6;i++){const a=i/6*6.28;
364
- const m=new THREE.Mesh(new THREE.BoxGeometry(0.05,1.2,0.05),goldEmit.clone());
365
- m.position.set(Math.cos(a)*1.3,-0.55,Math.sin(a)*1.3); root.add(m); lines.push(m);} extras.lines=lines;
366
- }
367
- else if(organ==='killinchu'){
368
- // low-poly kestrel over faceted terrain + 53 drone-dots circling
369
- const terrain=new THREE.Mesh(new THREE.ConeGeometry(2.3,0.7,3,1),
370
- new THREE.MeshStandardMaterial({color:0x14202c,metalness:0.4,roughness:0.7,flatShading:true}));
371
- terrain.position.y=-1.6; terrain.rotation.y=0.4; root.add(terrain);
372
- const bird=new THREE.Group(); bird.position.y=0.5; root.add(bird); extras.bird=bird;
373
- const body=new THREE.Mesh(new THREE.ConeGeometry(0.22,1.0,6),goldMat.clone());
374
- body.rotation.x=Math.PI/2; bird.add(body);
375
- const wgeo=new THREE.BufferGeometry(); const wv=new Float32Array([0,0,0, 1.5,0.15,-0.5, 1.3,-0.05,0.4]);
376
- wgeo.setAttribute('position',new THREE.BufferAttribute(wv,3)); wgeo.computeVertexNormals();
377
- const wmat=new THREE.MeshStandardMaterial({color:GOLD_HI,metalness:0.7,roughness:0.4,side:THREE.DoubleSide,flatShading:true});
378
- const wingL=new THREE.Mesh(wgeo,wmat); const wingR=new THREE.Mesh(wgeo,wmat); wingR.scale.x=-1;
379
- bird.add(wingL); bird.add(wingR); extras.wingL=wingL; extras.wingR=wingR;
380
- const eye=new THREE.Mesh(new THREE.SphereGeometry(0.06,8,8),emitMat); eye.position.set(0.08,0.05,0.5); bird.add(eye);
381
- // 53 drone-dots
382
- const dg=new THREE.SphereGeometry(0.04,6,6);
383
- const dm=new THREE.MeshStandardMaterial({color:WHITE,emissive:0x88ccff,emissiveIntensity:1.0});
384
- const drones=new THREE.InstancedMesh(dg,dm,53); const dummy=new THREE.Object3D(); const dd=[];
385
- for(let i=0;i<53;i++){dd.push({a:Math.random()*6.28, r:1.6+Math.random()*0.9, y:-0.3+Math.random()*1.4, sp:0.4+Math.random()*0.6});}
386
- root.add(drones); extras.drones=drones; extras.dd=dd; extras.dummy=dummy;
387
- }
388
- else if(organ==='a11oy'){
389
- // 16-node icosahedron knot-graph (Khipu cords) + edge pulses
390
- const ico=new THREE.IcosahedronGeometry(1.5,0);
391
- const pos=ico.attributes.position; const nodes=[]; const seen=new Set();
392
- for(let i=0;i<pos.count;i++){const v=new THREE.Vector3().fromBufferAttribute(pos,i);
393
- const k=v.toArray().map(n=>n.toFixed(2)).join(','); if(seen.has(k))continue; seen.add(k); nodes.push(v);}
394
- const ng=new THREE.SphereGeometry(0.13,12,12);
395
- nodes.forEach(v=>{const m=new THREE.Mesh(ng,goldEmit.clone()); m.position.copy(v); root.add(m);});
396
- // knotted edges
397
- const edgeMat=new THREE.LineBasicMaterial({color:GOLD,transparent:true,opacity:0.6});
398
- const segs=[]; for(let i=0;i<nodes.length;i++)for(let j=i+1;j<nodes.length;j++){
399
- if(nodes[i].distanceTo(nodes[j])<1.95){segs.push(nodes[i],nodes[j]);}}
400
- const eg=new THREE.BufferGeometry().setFromPoints(segs);
401
- root.add(new THREE.LineSegments(eg, edgeMat));
402
- // pulse traveling along edges
403
- const pulse=new THREE.Mesh(new THREE.SphereGeometry(0.1,10,10),emitMat); root.add(pulse);
404
- extras.pulse=pulse; extras.segs=segs;
405
- }
406
-
407
- // touch / mouse rotate (additive — auto-rotate continues unless reduced motion)
408
- let drag=false, px=0, py=0, vy=0, vx=0;
409
- const dn=e=>{drag=true; const p=e.touches?e.touches[0]:e; px=p.clientX; py=p.clientY;};
410
- const mv=e=>{if(!drag)return; const p=e.touches?e.touches[0]:e;
411
- vy=(p.clientX-px)*0.01; vx=(p.clientY-py)*0.01; root.rotation.y+=vy; root.rotation.x+=vx;
412
- px=p.clientX; py=p.clientY; if(e.touches)e.preventDefault();};
413
- const up=()=>{drag=false;};
414
- canvas.addEventListener('mousedown',dn); canvas.addEventListener('mousemove',mv); window.addEventListener('mouseup',up);
415
- canvas.addEventListener('touchstart',dn,{passive:true}); canvas.addEventListener('touchmove',mv,{passive:false}); canvas.addEventListener('touchend',up);
416
-
417
- function resize(){const w=canvas.clientWidth||220, h=canvas.clientHeight||220;
418
- if(canvas.width!==w*PR||canvas.height!==h*PR){renderer.setSize(w,h,false); cam.aspect=w/h; cam.updateProjectionMatrix();}}
419
-
420
- cells.push({renderer,scene,cam,root,organ,extras,resize});
421
- }
422
-
423
- // Resilient init: if WebGL is unavailable or the module/importmap is stripped by a
424
- // host sanitizer (e.g. the HF org-card iframe), each canvas keeps its inline SVG
425
- // poster background so the mesh is NEVER an empty box. Live 3D paints over it when supported.
426
- document.querySelectorAll('#szl-hero-grid canvas').forEach(function(c){
427
- try { makeScene(c, c.dataset.organ); }
428
- catch(err){ /* keep poster fallback for this organ */ }
429
- });
430
-
431
- let t0=performance.now();
432
- function loop(now){
433
- requestAnimationFrame(loop);
434
- if(document.hidden) return;
435
- const t=(now-t0)/1000;
436
- for(const c of cells){
437
- c.resize();
438
- if(!SZL_REDUCED) c.root.rotation.y += 0.0045;
439
- const e=c.extras;
440
- if(c.organ==='amaru' && e.core){ e.core.rotation.y+=0.02; e.core.scale.setScalar(1+Math.sin(t*2)*0.06); }
441
- if(c.organ==='sentra'){ if(e.lambda)e.lambda.rotation.z+=0.02;
442
- (e.threats||[]).forEach(m=>{m.userData.r-=m.userData.sp*0.012; if(m.userData.r<0.6){m.userData.r=2.6;}
443
- m.userData.a+=0.02; m.position.set(Math.cos(m.userData.a)*m.userData.r, Math.sin(m.userData.a*1.3)*0.6, Math.sin(m.userData.a)*m.userData.r);}); }
444
- if(c.organ==='rosie'){ (e.rings||[]).forEach((r,i)=>r.rotation.z+=0.01*(i+1));
445
- (e.lines||[]).forEach((l,i)=>{l.scale.y=0.6+0.5*(0.5+0.5*Math.sin(t*3+i)); l.material.emissiveIntensity=0.6+0.6*(0.5+0.5*Math.sin(t*3+i));}); }
446
- if(c.organ==='killinchu'){ if(e.bird)e.bird.position.y=0.5+Math.sin(t*1.5)*0.12;
447
- if(e.wingL){const f=Math.sin(t*5)*0.5; e.wingL.rotation.z=f; e.wingR.rotation.z=-f;}
448
- if(e.drones){const dummy=e.dummy; e.dd.forEach((d,i)=>{d.a+=d.sp*0.01;
449
- dummy.position.set(Math.cos(d.a)*d.r,d.y+Math.sin(t+d.a)*0.1,Math.sin(d.a)*d.r);
450
- dummy.updateMatrix(); e.drones.setMatrixAt(i,dummy.matrix);}); e.drones.instanceMatrix.needsUpdate=true;} }
451
- if(c.organ==='a11oy' && e.pulse && e.segs.length){
452
- const seg=Math.floor(t*0.7)%(e.segs.length/2); const a=e.segs[seg*2], b=e.segs[seg*2+1];
453
- const f=(t*0.7)%1; e.pulse.position.lerpVectors(a,b,f); }
454
- c.renderer.render(c.scene,c.cam);
455
- }
456
- }
457
- requestAnimationFrame(loop);
458
- </script>
459
- <!-- ================= /SZL 3D HERO FIGURES ================= -->
460
-
461
- <!-- BANNER — UNTOUCHED (5-hero painterly artwork is sacred) -->
462
- <section id="banner" style="padding:0;border-top:none;">
463
- <img src="https://huggingface.co/spaces/SZLHOLDINGS/README/resolve/main/assets/szl_banner.png" alt="SZL Holdings — the five-hero governed agentic mesh team" style="display:block;width:100%;height:auto;"/>
464
- </section>
465
-
466
-
467
-
468
- <!-- flag block: five hero portraits as links + org name + icon links + footer -->
469
- <main id="main">
470
- <section style="padding:3.5rem 0;text-align:center;">
471
- <div class="container">
472
-
473
- <!-- chibi portrait row replaced by 3D heroes (above) + live mesh panel (below) — Yachay -->
474
-
475
- <h1 style="margin-bottom:1.4rem;">SZL&nbsp;Holdings</h1>
476
-
477
- <div style="display:flex;justify-content:center;gap:1.6rem;font-size:1.8rem;">
478
- <a href="https://orcid.org/0009-0001-0110-4173" aria-label="ORCID" title="ORCID">✦</a>
479
- <a href="https://github.com/szl-holdings" aria-label="GitHub" title="GitHub">⌬</a>
480
- <a href="https://huggingface.co/spaces/SZLHOLDINGS/uds-demo" aria-label="UDS Demo" title="UDS Demo">▶</a>
481
- </div>
482
-
483
- </div>
484
- </section>
485
- </main>
486
- <!-- ============ LIVE MESH + LATEST RECEIPTS (ADDITIVE — Yachay) ============
487
- Polls /healthz of each flagship live (badge flips green/red) and pulls the
488
- 5 most recent Khipu receipts across the mesh. Apache-2.0. Doctrine v11 LOCKED. -->
489
- <section id="szl-live-mesh" style="padding:2.2rem 0;">
490
- <div class="container">
491
- <p class="mr-head" style="font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.18em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:0.2rem;text-align:center;">Live Mesh · /healthz</p>
492
- <div id="szl-mesh-grid" style="display:grid;grid-template-columns:repeat(5,1fr);gap:0.7rem;margin:1rem 0 0;">
493
- <div class="szl-mesh-cell" data-organ="amaru"><span class="szl-mesh-name">amaru</span><span class="szl-mesh-badge" data-code="…">…</span></div>
494
- <div class="szl-mesh-cell" data-organ="sentra"><span class="szl-mesh-name">sentra</span><span class="szl-mesh-badge" data-code="…">…</span></div>
495
- <div class="szl-mesh-cell" data-organ="rosie"><span class="szl-mesh-name">rosie</span><span class="szl-mesh-badge" data-code="…">…</span></div>
496
- <div class="szl-mesh-cell" data-organ="killinchu"><span class="szl-mesh-name">killinchu</span><span class="szl-mesh-badge" data-code="…">…</span></div>
497
- <div class="szl-mesh-cell" data-organ="a11oy"><span class="szl-mesh-name">a11oy</span><span class="szl-mesh-badge" data-code="…">…</span></div>
498
- </div>
499
- <p id="szl-receipt-counter" style="text-align:center;font-family:var(--font-mono);font-size:0.8rem;color:var(--gold-light);margin-top:1.1rem;">signed receipts across the mesh: <b data-receipts>—</b></p>
500
- <div style="max-width:760px;margin:1.2rem auto 0;">
501
- <p class="mr-head" style="font-family:var(--font-mono);font-size:0.7rem;letter-spacing:0.14em;text-transform:uppercase;color:var(--gold-dim);margin-bottom:0.4rem;text-align:center;">Latest signed receipts</p>
502
- <ul id="szl-receipt-list" style="list-style:none;padding:0;margin:0;font-family:var(--font-mono);font-size:0.72rem;color:var(--text-muted);">
503
- <li style="opacity:0.6;text-align:center;padding:0.5rem;">polling Khipu DAG…</li>
504
- </ul>
505
- </div>
506
- </div>
507
- </section>
508
- <style id="szl-live-mesh-style">
509
- .szl-mesh-cell{background:var(--glass,rgba(45,27,78,0.55));border:1px solid var(--border,rgba(212,175,55,0.18));border-radius:12px;padding:0.7rem 0.4rem;text-align:center;display:flex;flex-direction:column;gap:0.4rem;min-height:44px;}
510
- .szl-mesh-name{font-family:var(--font-mono,monospace);font-size:0.72rem;color:var(--text-muted,#a090c0);letter-spacing:0.04em;}
511
- .szl-mesh-badge{font-family:var(--font-mono,monospace);font-size:0.78rem;font-weight:600;padding:0.18rem 0.4rem;border-radius:6px;background:rgba(255,255,255,0.06);color:var(--text-dim,#7060a0);}
512
- .szl-mesh-badge.ok{background:rgba(61,220,132,0.16);color:#3ddc84;}
513
- .szl-mesh-badge.down{background:rgba(255,90,68,0.16);color:#ff7a59;}
514
- #szl-receipt-list li{padding:0.4rem 0.6rem;border-bottom:1px solid var(--border,rgba(212,175,55,0.12));overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
515
- #szl-receipt-list li b{color:var(--gold-light,#e8cc6a);}
516
- @media(max-width:900px) and (min-width:561px){#szl-mesh-grid{grid-template-columns:repeat(3,1fr);}}
517
- @media(max-width:560px){#szl-mesh-grid{grid-template-columns:repeat(2,1fr);}}
518
- </style>
519
- <script>
520
- (function(){
521
- var ORGANS=["amaru","sentra","rosie","killinchu","a11oy"];
522
- var receipts=[];
523
- function base(o){return "https://szlholdings-"+o+".hf.space";}
524
- function pollHealth(){
525
- ORGANS.forEach(function(o){
526
- var cell=document.querySelector('.szl-mesh-cell[data-organ="'+o+'"] .szl-mesh-badge');
527
- if(!cell)return;
528
- fetch(base(o)+"/healthz",{mode:"cors",cache:"no-store"}).then(function(r){
529
- cell.textContent=r.status; cell.dataset.code=r.status;
530
- cell.className="szl-mesh-badge "+(r.ok?"ok":"down");
531
- }).catch(function(){ cell.textContent="—"; cell.className="szl-mesh-badge down"; });
532
- });
533
- }
534
- function pollReceipts(){
535
- var total=0, got=0;
536
- ORGANS.forEach(function(o){
537
- // metrics counter for running total
538
- fetch(base(o)+"/metrics",{mode:"cors",cache:"no-store"}).then(function(r){return r.text();}).then(function(t){
539
- var m=t.match(/(\w*receipts_total)\s+(\d+)/);
540
- if(m){ total+=parseInt(m[2],10); }
541
- }).catch(function(){}).finally(function(){
542
- got++; if(got>=ORGANS.length){var b=document.querySelector('[data-receipts]'); if(b&&total>0)b.textContent=total.toLocaleString();}
543
- });
544
- // latest receipts
545
- fetch(base(o)+"/api/"+o+"/v2/khipu/lmdb/tail?n=2",{mode:"cors",cache:"no-store"}).then(function(r){return r.json();}).then(function(j){
546
- var arr=(j&&j.receipts)||j&&j.tail||[];
547
- arr.forEach(function(rec){ receipts.push({organ:o, id:(rec.id||rec.hash||rec.receipt_id||"").toString().slice(0,16), ts:rec.ts||rec.timestamp||""}); });
548
- renderReceipts();
549
- }).catch(function(){});
550
- });
551
- }
552
- function renderReceipts(){
553
- var ul=document.getElementById("szl-receipt-list"); if(!ul)return;
554
- var latest=receipts.slice(-5).reverse();
555
- if(!latest.length)return;
556
- ul.innerHTML=latest.map(function(r){return '<li><b>'+r.organ+'</b> · '+(r.id||'receipt')+' <span style="opacity:0.6;">'+(r.ts||'')+'</span></li>';}).join("");
557
- }
558
- pollHealth(); pollReceipts();
559
- setInterval(pollHealth,5000); setInterval(pollReceipts,15000);
560
- })();
561
- </script>
562
- <!-- ============ /LIVE MESH + LATEST RECEIPTS ============ -->
563
-
564
-
565
- <footer style="text-align:center;padding:1.5rem 0;border-top:1px solid var(--border);">
566
- <!-- Stills strip: the previous static team picture, relocated here from below the banner
567
- when the live Mission Room action band replaced it. Kept as a reference still. -->
568
- <!-- team-portrait still removed: replaced by 3D heroes (Yachay) -->
569
- <span class="mono" style="font-size:0.72rem;color:var(--text-dim);">— szl-holdings/.github @ d304951 · Doctrine v11 LOCKED · 749 declarations · 14 unique axioms · 163 sorries · ORCID 0009-0001-0110-4173</span>
570
- </footer>
571
-
572
- <!-- AMBIENT EMOJI LAYER (Wave4, additive) — 5 character emojis hanging around
573
- the page edges via position:fixed. Decorative (aria-hidden), placed at the
574
- end of <body> so that if a host strips the <style> block (e.g. the HF org-card
575
- HTML sanitizer), these small 64px images fall to the page bottom instead of
576
- overlaying content; explicit width/height attributes keep them small even
577
- with CSS removed. Animation is gated behind prefers-reduced-motion: no-preference. -->
578
- <!-- ambient chibi emoji layer removed: replaced by 3D heroes (Yachay) -->
579
  </body>
580
  </html>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
6
+ <meta name="theme-color" content="#12091f"/>
7
+ <title>SZL Holdings — Five Organs · Live 3D Character Roster</title>
8
+ <meta name="description" content="SZL Holdings character-select roster: five live organs as 3D heroes in UDS swag. Hover reveals superpowers; click opens each Space. Doctrine v11 LOCKED 749/14/163."/>
9
+ <meta property="og:title" content="SZL Holdings — Live 3D Character Roster"/>
10
+ <meta property="og:description" content="The Mesh · Five Organs · Live 3D · Character Roster · UDS Edition."/>
 
 
11
  <meta property="og:image" content="https://huggingface.co/spaces/SZLHOLDINGS/README/resolve/main/assets/szl_banner.png"/>
12
  <meta property="og:type" content="website"/>
13
  <link rel="preconnect" href="https://fonts.googleapis.com"/>
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
15
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"/>
16
+ <link rel="stylesheet" href="css/style.css"/>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  </head>
18
  <body>
19
+ <a class="skip" href="#stage">Skip to character roster</a>
20
+
21
+ <header class="roster-header">THE MESH · FIVE ORGANS · LIVE 3D · CHARACTER ROSTER</header>
22
+
23
+ <!-- 3D character-select stage. Replaces both the old flat 5-tile grid and the
24
+ static AI-illustration hero banner. -->
25
+ <main id="stage" aria-label="SZL Holdings five-organ 3D character roster">
26
+ <canvas id="roster" aria-hidden="true"></canvas>
27
+ <div id="loading">INITIALIZING ROSTER…</div>
28
+ <!-- Hidden focusable proxies for keyboard nav (Tab cycles, Enter opens Space) -->
29
+ <div id="a11y-heroes" class="a11y-heroes" aria-label="Hero select"></div>
30
+
31
+ <!-- WebGL fallback: the existing 5-tile SVG grid (graceful degradation) -->
32
+ <section id="fallback" hidden aria-label="SZL Holdings five organs (static)">
33
+ <div class="fb-grid">
34
+ <a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/amaru" aria-label="AMARU — cortex · serpent. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMTEiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgb3BhY2l0eT0iMC45NSI+PGVsbGlwc2UgY3g9IjEyOCIgY3k9IjE5MCIgcng9IjU4IiByeT0iMTciLz48ZWxsaXBzZSBjeD0iMTI4IiBjeT0iMTUwIiByeD0iNTAiIHJ5PSIxNSIvPjxlbGxpcHNlIGN4PSIxMjgiIGN5PSIxMTIiIHJ4PSI0MCIgcnk9IjEyIi8+PGVsbGlwc2UgY3g9IjEyOCIgY3k9IjgwIiByeD0iMjgiIHJ5PSI5Ii8+PC9nPjxwb2x5Z29uIHBvaW50cz0iMTI4LDk4IDE1MCwxMzIgMTI4LDE1NiAxMDYsMTMyIiBmaWxsPSIjZThjYzZhIi8+PHBvbHlnb24gcG9pbnRzPSIxMjgsNjAgMTQ0LDg0IDEyOCw5OCAxMTIsODQiIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSIxNDIiIGN5PSI3NCIgcj0iNCIgZmlsbD0iI2Y0ZWVkZSIvPjwvc3ZnPg==" alt="amaru emblem"/><span class="fb-name">AMARU</span><span class="fb-role">cortex · serpent</span></a>
35
+ <a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/sentra" aria-label="SENTRA immune · shield. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBmaWxsPSJub25lIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMi40IiBvcGFjaXR5PSIwLjkiPjxwb2x5Z29uIHBvaW50cz0iMTI4LDU2IDE5NiwxMDQgMTcwLDE4MiA4NiwxODIgNjAsMTA0Ii8+PHBvbHlnb24gcG9pbnRzPSIxMjgsNTYgMTcwLDE4MiA2MCwxMDQiLz48cG9seWdvbiBwb2ludHM9IjEyOCw1NiA4NiwxODIgMTk2LDEwNCIvPjxsaW5lIHgxPSI2MCIgeTE9IjEwNCIgeDI9IjE5NiIgeTI9IjEwNCIvPjxsaW5lIHgxPSIxMjgiIHkxPSI1NiIgeDI9IjEyOCIgeTI9IjE4MiIvPjwvZz48cG9seWdvbiBwb2ludHM9IjEwMCwxMTggMTU2LDExOCAxMjgsMTUwIiBmaWxsPSIjZDRhNDQ0IiBvcGFjaXR5PSIwLjU1Ii8+PGNpcmNsZSBjeD0iNjAiIGN5PSIxMzgiIHI9IjYiIGZpbGw9IiNmZjVhNDQiLz48Y2lyY2xlIGN4PSIyMDAiIGN5PSIxMjAiIHI9IjYiIGZpbGw9IiNmZjVhNDQiLz48Y2lyY2xlIGN4PSI3OCIgY3k9IjkyIiByPSI1IiBmaWxsPSIjNjY4OGZmIi8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iMTIwIiByPSIzNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZThjYzZhIiBzdHJva2Utd2lkdGg9IjQiIG9wYWNpdHk9IjAuOCIvPjwvc3ZnPg==" alt="sentra emblem"/><span class="fb-name">SENTRA</span><span class="fb-role">immune · shield</span></a>
36
+ <a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/rosie" aria-label="ROSIE — operator · console. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48Y2lyY2xlIGN4PSIxMjgiIGN5PSIxMjgiIHI9IjQ0IiBmaWxsPSIjMTExNjFmIiBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMyIvPjxjaXJjbGUgY3g9IjEyOCIgY3k9IjEyOCIgcj0iMjIiIGZpbGw9IiNmNGVlZGUiIG9wYWNpdHk9IjAuOTIiLz48ZyBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iNiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBvcGFjaXR5PSIwLjg1Ij48bGluZSB4MT0iNjAiIHkxPSIxNzAiIHgyPSI2MCIgeTI9IjEyMCIvPjxsaW5lIHgxPSIxMDAiIHkxPSIxODAiIHgyPSIxMDAiIHkyPSIxMTAiLz48bGluZSB4MT0iMTU2IiB5MT0iMTgwIiB4Mj0iMTU2IiB5Mj0iMTEwIi8+PGxpbmUgeDE9IjE5NiIgeTE9IjE3MCIgeDI9IjE5NiIgeTI9IjEyMCIvPjwvZz48cmVjdCB4PSI1NiIgeT0iMTc2IiB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjgiIHJ4PSI0IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iMTI4IiByPSI2MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZThjYzZhIiBzdHJva2Utd2lkdGg9IjEuNiIgb3BhY2l0eT0iMC41Ii8+PC9zdmc+" alt="rosie emblem"/><span class="fb-name">ROSIE</span><span class="fb-role">operator · console</span></a>
37
+ <a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/killinchu" aria-label="KILLINCHU — kestrel · drone. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48cGF0aCBkPSJNNzAgMTQwIFExMjggOTYgMTg2IDE0MCBRMTUwIDEyOCAxMjggMTI4IFExMDYgMTI4IDcwIDE0MFoiIGZpbGw9IiNkNGE0NDQiLz48cG9seWdvbiBwb2ludHM9IjEyOCwxMTIgMTM0LDEyOCAxMjIsMTI4IiBmaWxsPSIjZThjYzZhIi8+PGcgZmlsbD0iI2Y0ZWVkZSIgb3BhY2l0eT0iMC45Ij48Y2lyY2xlIGN4PSI2NCIgY3k9IjEyMCIgcj0iMy4yIi8+PGNpcmNsZSBjeD0iOTIiIGN5PSI5NiIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTYwIiBjeT0iOTIiIHI9IjMiLz48Y2lyY2xlIGN4PSIxOTYiIGN5PSIxMTgiIHI9IjMuNCIvPjxjaXJjbGUgY3g9IjE1MCIgY3k9IjE2MCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTAwIiBjeT0iMTY4IiByPSIzIi8+PGNpcmNsZSBjeD0iMjAwIiBjeT0iMTUwIiByPSIyLjQiLz48Y2lyY2xlIGN4PSI1OCIgY3k9IjE1OCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTI4IiBjeT0iNzgiIHI9IjIuNCIvPjwvZz48ZyBmaWxsPSIjNjY4OGZmIiBvcGFjaXR5PSIwLjgiPjxjaXJjbGUgY3g9IjEyMCIgY3k9IjE1MCIgcj0iMi42Ii8+PGNpcmNsZSBjeD0iMTcwIiBjeT0iMTQwIiByPSIyLjYiLz48L2c+PC9zdmc+" alt="killinchu emblem"/><span class="fb-name">KILLINCHU</span><span class="fb-role">kestrel · drone</span></a>
38
+ <a class="fb-cell" href="https://huggingface.co/spaces/SZLHOLDINGS/a11oy" aria-label="A11OY — router · wires. Open Space."><img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJiZyIgY3g9IjUwJSIgY3k9IjQyJSIgcj0iNzAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTAxNTFmIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDYwOTBmIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjI1NiIgaGVpZ2h0PSIyNTYiIHJ4PSIxMiIgZmlsbD0idXJsKCNiZykiLz48ZyBzdHJva2U9IiNkNGE0NDQiIHN0cm9rZS13aWR0aD0iMi4yIiBvcGFjaXR5PSIwLjg1Ij48bGluZSB4MT0iMTI4IiB5MT0iNzIiIHgyPSIxODYiIHkyPSIxMDQiLz48bGluZSB4MT0iMTg2IiB5MT0iMTA0IiB4Mj0iMTk2IiB5Mj0iMTY4Ii8+PGxpbmUgeDE9IjE5NiIgeTE9IjE2OCIgeDI9IjE0MCIgeTI9IjE5NiIvPjxsaW5lIHgxPSIxNDAiIHkxPSIxOTYiIHgyPSI4NiIgeTI9IjE4NCIvPjxsaW5lIHgxPSI4NiIgeTE9IjE4NCIgeDI9IjY0IiB5Mj0iMTI4Ii8+PGxpbmUgeDE9IjY0IiB5MT0iMTI4IiB4Mj0iMTI4IiB5Mj0iNzIiLz48bGluZSB4MT0iMTI4IiB5MT0iNzIiIHgyPSI5NiIgeTI9Ijk2Ii8+PGxpbmUgeDE9Ijk2IiB5MT0iOTYiIHgyPSIxNTAiIHkyPSIxNDAiLz48bGluZSB4MT0iMTUwIiB5MT0iMTQwIiB4Mj0iMTg2IiB5Mj0iMTA0Ii8+PGxpbmUgeDE9IjE1MCIgeTE9IjE0MCIgeDI9IjE0MCIgeTI9IjE5NiIvPjxsaW5lIHgxPSI5NiIgeTE9Ijk2IiB4Mj0iNjQiIHkyPSIxMjgiLz48bGluZSB4MT0iMTUwIiB5MT0iMTQwIiB4Mj0iMTk2IiB5Mj0iMTY4Ii8+PC9nPjxjaXJjbGUgY3g9IjEyOCIgY3k9IjcyIiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTg2IiBjeT0iMTA0IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTk2IiBjeT0iMTY4IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iMTQwIiBjeT0iMTk2IiByPSI3IiBmaWxsPSIjZDRhNDQ0Ii8+PGNpcmNsZSBjeD0iODYiIGN5PSIxODQiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSI2NCIgY3k9IjEyOCIgcj0iNyIgZmlsbD0iI2Q0YTQ0NCIvPjxjaXJjbGUgY3g9Ijk2IiBjeT0iOTYiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSIxNTAiIGN5PSIxNDAiIHI9IjciIGZpbGw9IiNkNGE0NDQiLz48Y2lyY2xlIGN4PSI5NiIgY3k9Ijk2IiByPSI3IiBmaWxsPSIjZjRlZWRlIi8+PC9zdmc+" alt="a11oy emblem"/><span class="fb-name">A11OY</span><span class="fb-role">router · wires</span></a>
39
+ </div>
40
+ </section>
41
+ </main>
42
+
43
+ <p class="tagline">Governed Agentic Mesh
44
+ <small>Provable by math · signed by receipts · runs on your hardware · UDS Edition</small>
45
+ </p>
46
+
47
+ <footer class="roster-footer">
48
+ Doctrine v11 LOCKED · <b>749 / 14 / 163</b> · &Lambda; Conjecture 1 · SLSA L1 honest · UDS Edition
49
+ </footer>
50
+
51
+ <script type="importmap">
52
+ { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } }
53
+ </script>
54
+ <script type="module" src="js/main.js"></script>
55
+ <noscript>
56
+ <p style="text-align:center;color:#a090c0;font-family:monospace;padding:1rem">
57
+ JavaScript is required for the live 3D roster. Visit each organ:
58
+ <a href="https://huggingface.co/SZLHOLDINGS">SZLHOLDINGS</a>.
59
+ </p>
60
+ </noscript>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  </body>
62
  </html>
js/canvas_decals.js ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // canvas_decals.js
2
+ // Procedural canvas-painted textures for UDS swag — zero external assets.
3
+ // Every texture is generated with the 2D Canvas API and wrapped in a THREE.CanvasTexture.
4
+ // This keeps the scene asset-free (fast load, no licensing risk, clearly readable).
5
+ import * as THREE from 'three';
6
+
7
+ const DPR = 2; // supersample the decal canvases for crisp text
8
+
9
+ function makeCanvas(w, h) {
10
+ const c = document.createElement('canvas');
11
+ c.width = w * DPR;
12
+ c.height = h * DPR;
13
+ const ctx = c.getContext('2d');
14
+ ctx.scale(DPR, DPR);
15
+ return { c, ctx, w, h };
16
+ }
17
+
18
+ function texFrom(c) {
19
+ const t = new THREE.CanvasTexture(c);
20
+ t.anisotropy = 4;
21
+ t.colorSpace = THREE.SRGBColorSpace;
22
+ t.needsUpdate = true;
23
+ return t;
24
+ }
25
+
26
+ // Draw a small emblem glyph for a hero. Keeps shapes simple + iconic.
27
+ function drawEmblem(ctx, kind, cx, cy, r, color) {
28
+ ctx.save();
29
+ ctx.translate(cx, cy);
30
+ ctx.strokeStyle = color;
31
+ ctx.fillStyle = color;
32
+ ctx.lineWidth = Math.max(2, r * 0.14);
33
+ ctx.lineCap = 'round';
34
+ ctx.lineJoin = 'round';
35
+ switch (kind) {
36
+ case 'serpent': { // AMARU — golden serpent S-curve
37
+ ctx.beginPath();
38
+ for (let i = 0; i <= 40; i++) {
39
+ const t = i / 40;
40
+ const a = t * Math.PI * 2.6;
41
+ const x = Math.sin(a) * r * 0.62;
42
+ const y = (t - 0.5) * r * 1.9;
43
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
44
+ }
45
+ ctx.stroke();
46
+ ctx.beginPath(); ctx.arc(Math.sin(0) * r * 0.62, -0.5 * r * 1.9, r * 0.16, 0, 7); ctx.fill();
47
+ break;
48
+ }
49
+ case 'shield': { // SENTRA — gate-glyph shield
50
+ ctx.beginPath();
51
+ ctx.moveTo(0, -r);
52
+ ctx.lineTo(r * 0.82, -r * 0.45);
53
+ ctx.lineTo(r * 0.62, r * 0.85);
54
+ ctx.lineTo(0, r);
55
+ ctx.lineTo(-r * 0.62, r * 0.85);
56
+ ctx.lineTo(-r * 0.82, -r * 0.45);
57
+ ctx.closePath(); ctx.stroke();
58
+ // gate bars inside
59
+ for (let i = -1; i <= 1; i++) {
60
+ ctx.beginPath();
61
+ ctx.moveTo(-r * 0.4, i * r * 0.32);
62
+ ctx.lineTo(r * 0.4, i * r * 0.32);
63
+ ctx.stroke();
64
+ }
65
+ break;
66
+ }
67
+ case 'console': { // ROSIE — console rune (terminal prompt)
68
+ ctx.strokeRect(-r * 0.9, -r * 0.7, r * 1.8, r * 1.4);
69
+ ctx.beginPath();
70
+ ctx.moveTo(-r * 0.5, -r * 0.2);
71
+ ctx.lineTo(-r * 0.1, r * 0.05);
72
+ ctx.lineTo(-r * 0.5, r * 0.3);
73
+ ctx.stroke();
74
+ ctx.beginPath();
75
+ ctx.moveTo(r * 0.05, r * 0.3);
76
+ ctx.lineTo(r * 0.55, r * 0.3);
77
+ ctx.stroke();
78
+ break;
79
+ }
80
+ case 'falcon': { // KILLINCHU — falcon / kestrel in flight
81
+ ctx.beginPath();
82
+ ctx.moveTo(-r, -r * 0.1);
83
+ ctx.quadraticCurveTo(-r * 0.35, -r * 0.65, 0, -r * 0.15);
84
+ ctx.quadraticCurveTo(r * 0.35, -r * 0.65, r, -r * 0.1);
85
+ ctx.quadraticCurveTo(r * 0.35, r * 0.1, 0, r * 0.55);
86
+ ctx.quadraticCurveTo(-r * 0.35, r * 0.1, -r, -r * 0.1);
87
+ ctx.closePath(); ctx.fill();
88
+ break;
89
+ }
90
+ case 'router': { // A11OY — neural-net / multi-arrow router node
91
+ ctx.beginPath(); ctx.arc(0, 0, r * 0.28, 0, 7); ctx.stroke();
92
+ const dirs = [[0, -1], [0.87, 0.5], [-0.87, 0.5]];
93
+ for (const [dx, dy] of dirs) {
94
+ ctx.beginPath();
95
+ ctx.moveTo(dx * r * 0.3, dy * r * 0.3);
96
+ ctx.lineTo(dx * r, dy * r);
97
+ ctx.stroke();
98
+ // arrowhead
99
+ ctx.beginPath();
100
+ ctx.arc(dx * r, dy * r, r * 0.12, 0, 7);
101
+ ctx.fill();
102
+ }
103
+ break;
104
+ }
105
+ }
106
+ ctx.restore();
107
+ }
108
+
109
+ // Build the wrap-around torso texture: dark fabric, name stencil on chest,
110
+ // UDS monogram patch on right shoulder, emblem on chest center.
111
+ // The texture wraps a cylinder; chest faces +Z, so we paint chest at U≈0.5.
112
+ export function makeTorsoTexture({ name, stencil, emblem, color, swatch }) {
113
+ const W = 512, H = 512;
114
+ const { c, ctx } = makeCanvas(W, H);
115
+ // base fabric
116
+ const g = ctx.createLinearGradient(0, 0, 0, H);
117
+ g.addColorStop(0, shade(swatch, -0.45));
118
+ g.addColorStop(1, shade(swatch, -0.7));
119
+ ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
120
+
121
+ // subtle vertical seams
122
+ ctx.strokeStyle = 'rgba(0,0,0,0.25)'; ctx.lineWidth = 2;
123
+ for (let x = 64; x < W; x += 128) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
124
+
125
+ // chest panel (centered at U=0.5 -> x=W/2)
126
+ const cx = W / 2;
127
+ // emblem on chest center
128
+ drawEmblem(ctx, emblem, cx, H * 0.42, 56, color);
129
+
130
+ // name stencil under emblem
131
+ ctx.fillStyle = '#f4f4f6';
132
+ ctx.font = 'bold 30px "JetBrains Mono", monospace';
133
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
134
+ ctx.fillText(stencil, cx, H * 0.66);
135
+ // colored underline
136
+ ctx.fillStyle = color; ctx.fillRect(cx - 120, H * 0.72, 240, 4);
137
+
138
+ // UDS shoulder patch — right shoulder sits near U≈0.78 -> x≈0.78*W
139
+ drawUDSPatch(ctx, W * 0.80, H * 0.16, 70, color);
140
+
141
+ return texFrom(c);
142
+ }
143
+
144
+ // A reusable UDS monogram patch (also used by grey NPCs).
145
+ export function drawUDSPatch(ctx, cx, cy, size, accent = '#9aa3ad') {
146
+ ctx.save();
147
+ ctx.translate(cx, cy);
148
+ // patch backing
149
+ ctx.fillStyle = '#1b2128';
150
+ roundRect(ctx, -size * 0.7, -size * 0.55, size * 1.4, size * 1.1, 8); ctx.fill();
151
+ ctx.strokeStyle = accent; ctx.lineWidth = 3;
152
+ roundRect(ctx, -size * 0.7, -size * 0.55, size * 1.4, size * 1.1, 8); ctx.stroke();
153
+ // UDS text
154
+ ctx.fillStyle = '#e8edf2';
155
+ ctx.font = `bold ${Math.round(size * 0.5)}px "JetBrains Mono", monospace`;
156
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
157
+ ctx.fillText('UDS', 0, -size * 0.05);
158
+ ctx.fillStyle = accent;
159
+ ctx.font = `${Math.round(size * 0.16)}px "JetBrains Mono", monospace`;
160
+ ctx.fillText('UNITED DEFENSE', 0, size * 0.32);
161
+ ctx.restore();
162
+ }
163
+
164
+ // Plain grey UDS NPC torso texture.
165
+ export function makeNPCTexture() {
166
+ const W = 256, H = 256;
167
+ const { c, ctx } = makeCanvas(W, H);
168
+ const g = ctx.createLinearGradient(0, 0, 0, H);
169
+ g.addColorStop(0, '#3a4048'); g.addColorStop(1, '#262b31');
170
+ ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
171
+ drawUDSPatch(ctx, W / 2, H * 0.42, 48, '#7f8a95');
172
+ ctx.fillStyle = '#b9c2cb';
173
+ ctx.font = '600 16px "JetBrains Mono", monospace';
174
+ ctx.textAlign = 'center';
175
+ ctx.fillText('PARADE REST', W / 2, H * 0.78);
176
+ return texFrom(c);
177
+ }
178
+
179
+ function roundRect(ctx, x, y, w, h, r) {
180
+ ctx.beginPath();
181
+ ctx.moveTo(x + r, y);
182
+ ctx.arcTo(x + w, y, x + w, y + h, r);
183
+ ctx.arcTo(x + w, y + h, x, y + h, r);
184
+ ctx.arcTo(x, y + h, x, y, r);
185
+ ctx.arcTo(x, y, x + w, y, r);
186
+ ctx.closePath();
187
+ }
188
+
189
+ function shade(hex, amt) {
190
+ const c = new THREE.Color(hex);
191
+ const hsl = {}; c.getHSL(hsl);
192
+ hsl.l = Math.max(0, Math.min(1, hsl.l + amt));
193
+ c.setHSL(hsl.h, hsl.s * 0.8, hsl.l);
194
+ return '#' + c.getHexString();
195
+ }
js/heroes.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // heroes.js
2
+ // The 5 SZL heroes: stylized low-poly humanoids built from primitives
3
+ // (capsule torso/limbs, sphere head, cone stance) — deliberately PS1-era,
4
+ // readable, fast, fully procedural. UDS swag painted via canvas_decals.
5
+ import * as THREE from 'three';
6
+ import { makeTorsoTexture } from './canvas_decals.js';
7
+
8
+ export const HEROES = [
9
+ {
10
+ id: 'amaru', name: 'AMARU', role: 'Cognitive Cortex — the brain',
11
+ color: '#f5c518', stencil: 'AMARU · CORTEX', emblem: 'serpent',
12
+ swag: 'Gold UDS shoulder patch · golden serpent emblem',
13
+ url: 'https://huggingface.co/spaces/SZLHOLDINGS/amaru',
14
+ prop: 'brain',
15
+ powers: [
16
+ { label: 'Dual-stream routing', icon: 'amaru_dualstream' },
17
+ { label: '7-chakra memory', icon: 'amaru_memory' },
18
+ { label: 'Forward-model prediction', icon: 'amaru_forward' },
19
+ { label: 'Lineage DAG', icon: 'amaru_lineage' },
20
+ ],
21
+ },
22
+ {
23
+ id: 'sentra', name: 'SENTRA', role: 'Immune Sentinel — the gates',
24
+ color: '#c8324e', stencil: 'SENTRA · SHIELD', emblem: 'shield',
25
+ swag: 'Crimson UDS tactical vest · gate-glyph shield emblem',
26
+ url: 'https://huggingface.co/spaces/SZLHOLDINGS/sentra',
27
+ prop: 'runes',
28
+ powers: [
29
+ { label: '8 fail-CLOSED gates', icon: 'sentra_gates' },
30
+ { label: 'DSSE signing', icon: 'sentra_dsse' },
31
+ { label: 'Conduction-aphasia detector', icon: 'sentra_conduction' },
32
+ { label: 'Cosign verify', icon: 'sentra_cosign' },
33
+ ],
34
+ },
35
+ {
36
+ id: 'rosie', name: 'ROSIE', role: 'Executive Operator — multi-organ orchestrator',
37
+ color: '#2ea66c', stencil: 'ROSIE · CONSOLE', emblem: 'console',
38
+ swag: 'Emerald UDS command jacket · console-rune emblem',
39
+ url: 'https://huggingface.co/spaces/SZLHOLDINGS/rosie',
40
+ prop: 'console',
41
+ powers: [
42
+ { label: 'Multi-LLM ensemble', icon: 'rosie_ensemble' },
43
+ { label: 'Adaptive routing', icon: 'rosie_routing' },
44
+ { label: 'Step-3.7-Flash voter', icon: 'rosie_step' },
45
+ { label: 'Executive UI', icon: 'rosie_exec' },
46
+ ],
47
+ },
48
+ {
49
+ id: 'killinchu', name: 'KILLINCHU', role: 'Kestrel Drone Health — UDS Edition',
50
+ color: '#4ac4d9', stencil: 'KILLINCHU · KESTREL', emblem: 'falcon',
51
+ swag: 'Full UDS field uniform · falcon emblem — the UDS face',
52
+ url: 'https://huggingface.co/spaces/SZLHOLDINGS/killinchu',
53
+ prop: 'hud', isUDSFace: true,
54
+ powers: [
55
+ { label: 'NOAA space-weather', icon: 'kil_noaa' },
56
+ { label: 'USGS seismic', icon: 'kil_usgs' },
57
+ { label: 'MQ-9 drone health', icon: 'kil_mq9' },
58
+ { label: 'Phase prediction', icon: 'kil_phase' },
59
+ ],
60
+ },
61
+ {
62
+ id: 'a11oy', name: 'A11OY', role: 'Sovereign Router — the wires',
63
+ color: '#8b6cf2', stencil: 'A11OY · ROUTER', emblem: 'router',
64
+ swag: 'Violet UDS sash · neural-net circuit pattern',
65
+ url: 'https://huggingface.co/spaces/SZLHOLDINGS/a11oy',
66
+ prop: 'streams',
67
+ powers: [
68
+ { label: 'PAC-Bayes governance', icon: 'a11_governance' },
69
+ { label: 'Multi-LLM router', icon: 'a11_router' },
70
+ { label: '35 anchor formulas', icon: 'a11_formulas' },
71
+ { label: 'Khipu chain', icon: 'a11_khipu' },
72
+ ],
73
+ },
74
+ ];
75
+
76
+ // Build one stylized low-poly humanoid. Returns a THREE.Group with a `.userData`
77
+ // describing animatable parts (torso for breathing, prop group, etc.).
78
+ export function buildHero(hero) {
79
+ const g = new THREE.Group();
80
+ const col = new THREE.Color(hero.color);
81
+ const skin = new THREE.Color('#caa98c');
82
+
83
+ const matBody = new THREE.MeshStandardMaterial({
84
+ map: makeTorsoTexture({
85
+ name: hero.name, stencil: hero.stencil, emblem: hero.emblem,
86
+ color: hero.color, swatch: hero.color,
87
+ }),
88
+ roughness: 0.62, metalness: 0.12, flatShading: true,
89
+ });
90
+ const matLimb = new THREE.MeshStandardMaterial({
91
+ color: col.clone().multiplyScalar(0.45), roughness: 0.7, metalness: 0.1, flatShading: true,
92
+ });
93
+ const matSkin = new THREE.MeshStandardMaterial({ color: skin, roughness: 0.8, flatShading: true });
94
+ const matAccent = new THREE.MeshStandardMaterial({
95
+ color: col, emissive: col, emissiveIntensity: 0.35, roughness: 0.4, metalness: 0.3, flatShading: true,
96
+ });
97
+
98
+ // Torso — capsule. Pivot so breathing scales from the waist.
99
+ const torsoGeo = new THREE.CapsuleGeometry(0.34, 0.7, 4, 10);
100
+ const torso = new THREE.Mesh(torsoGeo, matBody);
101
+ torso.castShadow = true;
102
+ const torsoPivot = new THREE.Group();
103
+ torso.position.y = 0.55;
104
+ torsoPivot.add(torso);
105
+ torsoPivot.position.y = 0.9;
106
+ g.add(torsoPivot);
107
+
108
+ // Head — sphere + a small accent visor band.
109
+ const head = new THREE.Mesh(new THREE.SphereGeometry(0.27, 16, 12), matSkin);
110
+ head.position.y = 0.55 + 0.62;
111
+ head.castShadow = true;
112
+ torsoPivot.add(head);
113
+ const visor = new THREE.Mesh(new THREE.TorusGeometry(0.24, 0.05, 6, 16), matAccent);
114
+ visor.rotation.x = Math.PI / 2.1;
115
+ visor.position.y = 0.55 + 0.66;
116
+ torsoPivot.add(visor);
117
+
118
+ // Shoulders / arms — capsules angled outward (parade-ready stance).
119
+ for (const side of [-1, 1]) {
120
+ const arm = new THREE.Mesh(new THREE.CapsuleGeometry(0.1, 0.6, 4, 8), matLimb);
121
+ arm.position.set(side * 0.46, 0.42, 0);
122
+ arm.rotation.z = side * 0.18;
123
+ arm.castShadow = true;
124
+ torsoPivot.add(arm);
125
+ const shoulder = new THREE.Mesh(new THREE.SphereGeometry(0.14, 10, 8), matAccent);
126
+ shoulder.position.set(side * 0.44, 0.78, 0);
127
+ torsoPivot.add(shoulder);
128
+ }
129
+
130
+ // Legs — capsules.
131
+ for (const side of [-1, 1]) {
132
+ const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.13, 0.7, 4, 8), matLimb);
133
+ leg.position.set(side * 0.17, 0.45, 0);
134
+ leg.castShadow = true;
135
+ g.add(leg);
136
+ }
137
+
138
+ // Stance cone — anchors the figure to the dais (the "base").
139
+ const base = new THREE.Mesh(new THREE.ConeGeometry(0.5, 0.3, 16), matLimb);
140
+ base.position.y = 0.12; base.rotation.x = Math.PI;
141
+ g.add(base);
142
+
143
+ // Prop group (weapon/tool) — hidden until hover.
144
+ const prop = buildProp(hero, col);
145
+ prop.visible = false;
146
+ g.add(prop);
147
+
148
+ g.userData = { hero, torsoPivot, prop, baseScale: 1 };
149
+ return g;
150
+ }
151
+
152
+ // Per-hero activated prop. Returns a group positioned around the hero.
153
+ function buildProp(hero, col) {
154
+ const grp = new THREE.Group();
155
+ const mat = new THREE.MeshStandardMaterial({
156
+ color: col, emissive: col, emissiveIntensity: 0.9, transparent: true,
157
+ opacity: 0.85, roughness: 0.3, metalness: 0.4,
158
+ });
159
+ switch (hero.prop) {
160
+ case 'brain': { // glowing brain hemispheres halo above head
161
+ for (const side of [-1, 1]) {
162
+ const hemi = new THREE.Mesh(new THREE.SphereGeometry(0.18, 12, 10, 0, Math.PI), mat);
163
+ hemi.position.set(side * 0.16, 2.05, 0);
164
+ hemi.rotation.y = side > 0 ? 0 : Math.PI;
165
+ grp.add(hemi);
166
+ }
167
+ const halo = new THREE.Mesh(new THREE.TorusGeometry(0.34, 0.02, 6, 24), mat);
168
+ halo.position.y = 2.05; halo.rotation.x = Math.PI / 2;
169
+ grp.add(halo);
170
+ grp.userData.spin = halo;
171
+ break;
172
+ }
173
+ case 'runes': { // 8 floating shield runes orbit
174
+ const runes = [];
175
+ for (let i = 0; i < 8; i++) {
176
+ const r = new THREE.Mesh(new THREE.BoxGeometry(0.12, 0.16, 0.03), mat);
177
+ const a = (i / 8) * Math.PI * 2;
178
+ r.position.set(Math.cos(a) * 0.7, 1.0, Math.sin(a) * 0.7);
179
+ grp.add(r); runes.push(r);
180
+ }
181
+ grp.userData.orbit = runes;
182
+ break;
183
+ }
184
+ case 'console': { // holographic console panel materializes in front
185
+ const panel = new THREE.Mesh(new THREE.PlaneGeometry(0.8, 0.5), mat);
186
+ panel.position.set(0, 1.1, 0.55);
187
+ grp.add(panel);
188
+ for (let i = 0; i < 3; i++) {
189
+ const bar = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.04, 0.01),
190
+ new THREE.MeshStandardMaterial({ color: '#0a160f', emissive: col, emissiveIntensity: 0.6 }));
191
+ bar.position.set(0, 1.2 - i * 0.12, 0.56);
192
+ grp.add(bar);
193
+ }
194
+ break;
195
+ }
196
+ case 'hud': { // holographic drone HUD reticle locks on
197
+ const reticle = new THREE.Mesh(new THREE.RingGeometry(0.28, 0.34, 24), mat);
198
+ reticle.position.set(0, 1.4, 0.5);
199
+ grp.add(reticle);
200
+ const inner = new THREE.Mesh(new THREE.RingGeometry(0.1, 0.12, 16), mat);
201
+ inner.position.set(0, 1.4, 0.5);
202
+ grp.add(inner);
203
+ grp.userData.spin = reticle;
204
+ break;
205
+ }
206
+ case 'streams': { // glowing routing-arrow streams flow from hand
207
+ const streams = [];
208
+ for (let i = 0; i < 5; i++) {
209
+ const s = new THREE.Mesh(new THREE.ConeGeometry(0.05, 0.18, 6), mat);
210
+ s.position.set(0.5, 0.7 + i * 0.02, 0.2);
211
+ s.rotation.z = -Math.PI / 2;
212
+ grp.add(s); streams.push(s);
213
+ }
214
+ grp.userData.stream = streams;
215
+ break;
216
+ }
217
+ }
218
+ return grp;
219
+ }
js/hover_panel.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // hover_panel.js
2
+ // Floating holographic superpower panel. Rendered as a DOM overlay anchored to
3
+ // the hero's screen-projected head position (rises ~60cm above the head in world
4
+ // space). Name in hero color (monospace), SUPERPOWERS header, 4 lines that type
5
+ // out sequentially (30ms/char) then pulse, each with an animated SVG icon.
6
+ import { buildIcon } from './superpower_icons.js';
7
+
8
+ export class HoverPanel {
9
+ constructor(container, reduced) {
10
+ this.container = container;
11
+ this.reduced = reduced;
12
+ this.el = document.createElement('div');
13
+ this.el.className = 'hover-panel';
14
+ this.el.setAttribute('role', 'tooltip');
15
+ this.el.setAttribute('aria-hidden', 'true');
16
+ container.appendChild(this.el);
17
+ this.activeId = null;
18
+ this._timers = [];
19
+ }
20
+
21
+ _clearTimers() { this._timers.forEach(clearTimeout); this._timers = []; }
22
+
23
+ show(hero) {
24
+ if (this.activeId === hero.id) return;
25
+ this.activeId = hero.id;
26
+ this._clearTimers();
27
+ const c = hero.color;
28
+ this.el.style.setProperty('--hc', c);
29
+ this.el.innerHTML = `
30
+ <div class="hp-name" style="color:${c}">${hero.name}</div>
31
+ <div class="hp-role">${hero.role}</div>
32
+ <div class="hp-head">SUPERPOWERS</div>
33
+ <ul class="hp-list"></ul>`;
34
+ const list = this.el.querySelector('.hp-list');
35
+ hero.powers.forEach((p, i) => {
36
+ const li = document.createElement('li');
37
+ li.className = 'hp-line';
38
+ const icon = buildIcon(p.icon, c, this.reduced);
39
+ const txt = document.createElement('span');
40
+ txt.className = 'hp-text';
41
+ li.appendChild(icon);
42
+ li.appendChild(txt);
43
+ list.appendChild(li);
44
+ if (this.reduced) {
45
+ txt.textContent = p.label;
46
+ li.classList.add('hp-shown');
47
+ } else {
48
+ // sequential type-out: 30ms/char, staggered per line
49
+ const startDelay = i * 520;
50
+ this._timers.push(setTimeout(() => this._type(li, txt, p.label), startDelay));
51
+ }
52
+ });
53
+ this.el.classList.add('hp-visible');
54
+ this.el.setAttribute('aria-hidden', 'false');
55
+ }
56
+
57
+ _type(li, txt, label) {
58
+ li.classList.add('hp-shown');
59
+ let n = 0;
60
+ const step = () => {
61
+ txt.textContent = label.slice(0, n);
62
+ n++;
63
+ if (n <= label.length) {
64
+ this._timers.push(setTimeout(step, 30));
65
+ } else {
66
+ li.classList.add('hp-pulse'); // pulse once typed
67
+ }
68
+ };
69
+ step();
70
+ }
71
+
72
+ hide() {
73
+ this.activeId = null;
74
+ this._clearTimers();
75
+ this.el.classList.remove('hp-visible');
76
+ this.el.setAttribute('aria-hidden', 'true');
77
+ }
78
+
79
+ // Position the panel at a screen-space point (px), centered horizontally above it.
80
+ place(x, y) {
81
+ this.el.style.left = x + 'px';
82
+ this.el.style.top = y + 'px';
83
+ }
84
+ }
js/main.js ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // main.js — Three.js bootstrap + character-select scene for the SZL org card.
2
+ // Five procedural low-poly heroes on lit daises in a slight arc, slow camera
3
+ // carousel orbit, KHIPU-style particle motes, UDS NPCs in the background.
4
+ // Hover reveals animated superpowers; click opens the hero's HF Space.
5
+ import * as THREE from 'three';
6
+ import { HEROES, buildHero } from './heroes.js';
7
+ import { buildUDSNPCs, animateNPCs } from './uds_npcs.js';
8
+ import { HoverPanel } from './hover_panel.js';
9
+
10
+ const REDUCED = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
11
+
12
+ // ---- WebGL capability gate (graceful degradation to SVG grid) ----
13
+ function webglOK() {
14
+ try {
15
+ if (!window.WebGLRenderingContext) return false;
16
+ const c = document.createElement('canvas');
17
+ return !!(c.getContext('webgl2') || c.getContext('webgl'));
18
+ } catch (e) { return false; }
19
+ }
20
+
21
+ const stage = document.getElementById('stage');
22
+ const canvas = document.getElementById('roster');
23
+ const fallback = document.getElementById('fallback');
24
+
25
+ if (!webglOK()) {
26
+ // Show the static SVG 5-tile grid fallback, hide the 3D canvas.
27
+ canvas.style.display = 'none';
28
+ if (fallback) fallback.hidden = false;
29
+ document.getElementById('loading')?.remove();
30
+ } else {
31
+ if (fallback) fallback.hidden = true;
32
+ init();
33
+ }
34
+
35
+ function init() {
36
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
37
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
38
+ renderer.shadowMap.enabled = true;
39
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
40
+
41
+ const scene = new THREE.Scene();
42
+ scene.fog = new THREE.FogExp2(0x12091f, 0.045);
43
+
44
+ const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
45
+ const CAM_RADIUS = 9.2, CAM_HEIGHT = 2.4;
46
+ camera.position.set(0, CAM_HEIGHT, CAM_RADIUS);
47
+
48
+ // ---- Lights ----
49
+ scene.add(new THREE.AmbientLight(0x5a4a7a, 0.55));
50
+ const key = new THREE.DirectionalLight(0xffffff, 0.85);
51
+ key.position.set(4, 9, 6); key.castShadow = true;
52
+ key.shadow.mapSize.set(1024, 1024);
53
+ key.shadow.camera.near = 1; key.shadow.camera.far = 30;
54
+ key.shadow.camera.left = -10; key.shadow.camera.right = 10;
55
+ key.shadow.camera.top = 10; key.shadow.camera.bottom = -10;
56
+ scene.add(key);
57
+ const fill = new THREE.DirectionalLight(0x8b6cf2, 0.3);
58
+ fill.position.set(-6, 4, -4); scene.add(fill);
59
+
60
+ // ---- Ground plane (receives shadows, dark) ----
61
+ const ground = new THREE.Mesh(
62
+ new THREE.CircleGeometry(16, 48),
63
+ new THREE.MeshStandardMaterial({ color: 0x0c0718, roughness: 1, metalness: 0 })
64
+ );
65
+ ground.rotation.x = -Math.PI / 2; ground.position.y = -0.02; ground.receiveShadow = true;
66
+ scene.add(ground);
67
+
68
+ // ---- Heroes on daises in a slight arc, slightly elevated ----
69
+ const heroRoot = new THREE.Group();
70
+ scene.add(heroRoot);
71
+ const heroEntries = [];
72
+ const N = HEROES.length;
73
+ const arcSpan = 2.0; // radians across which heroes are spread
74
+ const arcRadius = 5.0; // distance from arc center
75
+ HEROES.forEach((hero, i) => {
76
+ const t = N === 1 ? 0.5 : i / (N - 1);
77
+ const a = -arcSpan / 2 + t * arcSpan;
78
+ const x = Math.sin(a) * arcRadius;
79
+ const z = -arcRadius + Math.cos(a) * arcRadius * 0.55; // gentle forward arc
80
+ const slot = new THREE.Group();
81
+ slot.position.set(x, 0.25, z); // slightly elevated
82
+ slot.rotation.y = -a * 0.45; // face toward camera center
83
+
84
+ // dais
85
+ const daisMat = new THREE.MeshStandardMaterial({ color: 0x171022, roughness: 0.5, metalness: 0.4 });
86
+ const dais = new THREE.Mesh(new THREE.CylinderGeometry(0.95, 1.05, 0.18, 32), daisMat);
87
+ dais.position.y = -0.09; dais.receiveShadow = true; dais.castShadow = true;
88
+ slot.add(dais);
89
+ // colored rim-light ring on the dais edge
90
+ const rimMat = new THREE.MeshStandardMaterial({
91
+ color: hero.color, emissive: hero.color, emissiveIntensity: 1.0,
92
+ roughness: 0.3, transparent: true, opacity: 0.95,
93
+ });
94
+ const rim = new THREE.Mesh(new THREE.TorusGeometry(0.98, 0.045, 8, 48), rimMat);
95
+ rim.rotation.x = Math.PI / 2; rim.position.y = 0.02;
96
+ slot.add(rim);
97
+ // a point light per dais (cheap; 5 total) for the colored glow
98
+ const rimLight = new THREE.PointLight(new THREE.Color(hero.color), 1.4, 4.5, 2);
99
+ rimLight.position.set(0, 0.6, 0.4);
100
+ slot.add(rimLight);
101
+
102
+ const figure = buildHero(hero);
103
+ slot.add(figure);
104
+
105
+ heroRoot.add(slot);
106
+ heroEntries.push({ hero, slot, figure, rim, rimMat, rimLight, baseRim: 1.0,
107
+ bodyMats: collectBodyMats(figure), hovered: false, scale: 1 });
108
+ });
109
+
110
+ // ---- UDS NPCs in the background ----
111
+ const { group: npcGroup, npcs } = buildUDSNPCs(4);
112
+ scene.add(npcGroup);
113
+
114
+ // ---- KHIPU DAG particle motes ----
115
+ const PCOUNT = REDUCED ? 0 : 420;
116
+ let particles = null, pPos = null, pHome = null, pVel = null;
117
+ if (PCOUNT > 0) {
118
+ pPos = new Float32Array(PCOUNT * 3);
119
+ pHome = new Float32Array(PCOUNT * 3);
120
+ pVel = new Float32Array(PCOUNT * 3);
121
+ for (let i = 0; i < PCOUNT; i++) {
122
+ const r = 3 + Math.random() * 7;
123
+ const a = Math.random() * Math.PI * 2;
124
+ const x = Math.cos(a) * r, y = Math.random() * 6 + 0.2, z = Math.sin(a) * r - 2;
125
+ pPos[i * 3] = pHome[i * 3] = x;
126
+ pPos[i * 3 + 1] = pHome[i * 3 + 1] = y;
127
+ pPos[i * 3 + 2] = pHome[i * 3 + 2] = z;
128
+ }
129
+ const pg = new THREE.BufferGeometry();
130
+ pg.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
131
+ const pm = new THREE.PointsMaterial({ color: 0xcda8ff, size: 0.06, transparent: true, opacity: 0.7,
132
+ depthWrite: false, blending: THREE.AdditiveBlending });
133
+ particles = new THREE.Points(pg, pm);
134
+ scene.add(particles);
135
+ }
136
+
137
+ // ---- Hover panel (DOM overlay) ----
138
+ const panel = new HoverPanel(stage, REDUCED);
139
+
140
+ // ---- Raycaster + invisible hit cylinders per hero ----
141
+ const raycaster = new THREE.Raycaster();
142
+ const pointer = new THREE.Vector2();
143
+ let pointerInside = false;
144
+ const hitMeshes = heroEntries.map((e, i) => {
145
+ const hit = new THREE.Mesh(
146
+ new THREE.CylinderGeometry(0.9, 0.9, 2.6, 8),
147
+ new THREE.MeshBasicMaterial({ visible: false })
148
+ );
149
+ hit.position.y = 1.1;
150
+ hit.userData.index = i;
151
+ e.slot.add(hit);
152
+ return hit;
153
+ });
154
+
155
+ let activeIndex = -1;
156
+ function setActive(idx) {
157
+ if (idx === activeIndex) return;
158
+ activeIndex = idx;
159
+ heroEntries.forEach((e, i) => { e.hovered = (i === idx); });
160
+ if (idx >= 0) {
161
+ panel.show(heroEntries[idx].hero);
162
+ burst(idx);
163
+ stage.style.cursor = 'pointer';
164
+ stage.setAttribute('data-active', heroEntries[idx].hero.id);
165
+ } else {
166
+ panel.hide();
167
+ stage.style.cursor = 'default';
168
+ stage.removeAttribute('data-active');
169
+ }
170
+ }
171
+
172
+ // particle energy burst toward a hero then radiate
173
+ let burstUntil = 0, burstTarget = new THREE.Vector3();
174
+ function burst(idx) {
175
+ if (!particles) return;
176
+ burstTarget.copy(heroEntries[idx].slot.position).add(new THREE.Vector3(0, 1.2, 0));
177
+ burstUntil = performance.now() + 900;
178
+ }
179
+
180
+ // ---- Pointer events ----
181
+ function updatePointer(ev) {
182
+ const rect = canvas.getBoundingClientRect();
183
+ pointer.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
184
+ pointer.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
185
+ pointerInside = true;
186
+ }
187
+ canvas.addEventListener('pointermove', updatePointer);
188
+ canvas.addEventListener('pointerleave', () => { pointerInside = false; setActive(-1); });
189
+ canvas.addEventListener('click', () => {
190
+ if (activeIndex >= 0) window.open(heroEntries[activeIndex].hero.url, '_blank', 'noopener');
191
+ });
192
+
193
+ // ---- Keyboard accessibility: Tab cycles, Enter activates ----
194
+ // Hidden focusable proxies announce each hero and drive the 3D highlight.
195
+ const a11yRoot = document.getElementById('a11y-heroes');
196
+ heroEntries.forEach((e, i) => {
197
+ const b = document.createElement('button');
198
+ b.className = 'a11y-hero';
199
+ const labels = e.hero.powers.map(p => p.label.toLowerCase()).join(', ');
200
+ b.setAttribute('aria-label',
201
+ `${e.hero.name} — ${e.hero.role}. Superpowers: ${labels}. Click to open Space.`);
202
+ b.addEventListener('focus', () => setKeyActive(i));
203
+ b.addEventListener('blur', () => { if (kbActive === i) setKeyActive(-1); });
204
+ b.addEventListener('click', () => window.open(e.hero.url, '_blank', 'noopener'));
205
+ a11yRoot.appendChild(b);
206
+ });
207
+ let kbActive = -1;
208
+ function setKeyActive(i) { kbActive = i; setActive(i); }
209
+
210
+ // ---- Resize ----
211
+ function resize() {
212
+ const w = stage.clientWidth, h = stage.clientHeight;
213
+ renderer.setSize(w, h, false);
214
+ camera.aspect = w / h; camera.updateProjectionMatrix();
215
+ }
216
+ window.addEventListener('resize', resize);
217
+ resize();
218
+
219
+ // ---- Animation loop ----
220
+ const clock = new THREE.Clock();
221
+ let camAngle = 0;
222
+ const _proj = new THREE.Vector3();
223
+
224
+ function frame() {
225
+ const dt = clock.getDelta();
226
+ const t = clock.elapsedTime * 1000;
227
+
228
+ // camera slow orbit (~60s period) unless reduced motion
229
+ if (!REDUCED) camAngle = (clock.elapsedTime / 60) * Math.PI * 2;
230
+ const orbitR = CAM_RADIUS;
231
+ camera.position.x = Math.sin(camAngle) * orbitR;
232
+ camera.position.z = Math.cos(camAngle) * orbitR;
233
+ camera.position.y = CAM_HEIGHT;
234
+ camera.lookAt(0, 1.0, -1.2);
235
+
236
+ // hover hit-test (only when pointer is in canvas)
237
+ if (pointerInside) {
238
+ raycaster.setFromCamera(pointer, camera);
239
+ const hits = raycaster.intersectObjects(hitMeshes, false);
240
+ setActive(hits.length ? hits[0].object.userData.index : -1);
241
+ }
242
+
243
+ // per-hero animation
244
+ heroEntries.forEach((e, i) => {
245
+ const ud = e.figure.userData;
246
+ // breathing: torso scale y ±2% over 3s
247
+ if (!REDUCED) {
248
+ const breathe = 1 + Math.sin(t * 0.00209 + i) * 0.02;
249
+ ud.torsoPivot.scale.y = breathe;
250
+ }
251
+ // hover scale-up to 1.15x over ~200ms (ease toward target)
252
+ const target = e.hovered ? 1.15 : 1.0;
253
+ const lerpK = REDUCED ? 1 : Math.min(1, dt / 0.2);
254
+ e.scale += (target - e.scale) * lerpK;
255
+ e.slot.scale.setScalar(e.scale);
256
+
257
+ // dais rim pulse (HSL lightness) + hover brighten +50%
258
+ const baseI = e.hovered ? 1.5 : 1.0;
259
+ const pulse = REDUCED ? 0 : Math.sin(t * 0.0016 + i * 1.3) * 0.25;
260
+ e.rimMat.emissiveIntensity = baseI + pulse;
261
+ e.rimLight.intensity = (e.hovered ? 2.6 : 1.4) + pulse;
262
+
263
+ // dim non-hovered heroes to 50% when one is active
264
+ const dim = (activeIndex >= 0 && !e.hovered) ? 0.5 : 1.0;
265
+ e.bodyMats.forEach(m => {
266
+ m.transparent = dim < 1;
267
+ m.opacity += (dim - m.opacity) * (REDUCED ? 1 : Math.min(1, dt * 6));
268
+ });
269
+
270
+ // prop activation on hover
271
+ ud.prop.visible = e.hovered;
272
+ if (e.hovered && !REDUCED) animateProp(ud.prop, t);
273
+ });
274
+
275
+ // NPC idle
276
+ animateNPCs(npcs, t, REDUCED);
277
+
278
+ // particle motes drift + burst/converge
279
+ if (particles) {
280
+ const now = performance.now();
281
+ const bursting = now < burstUntil;
282
+ for (let i = 0; i < PCOUNT; i++) {
283
+ const ix = i * 3;
284
+ if (bursting) {
285
+ // converge toward target then radiate (second half)
286
+ const half = burstUntil - 450;
287
+ const k = now < half ? 0.06 : -0.05;
288
+ pPos[ix] += (burstTarget.x - pPos[ix]) * k;
289
+ pPos[ix + 1] += (burstTarget.y - pPos[ix + 1]) * k;
290
+ pPos[ix + 2] += (burstTarget.z - pPos[ix + 2]) * k;
291
+ } else {
292
+ // ease home + gentle float
293
+ pPos[ix] += (pHome[ix] - pPos[ix]) * 0.02;
294
+ pPos[ix + 1] += (pHome[ix + 1] - pPos[ix + 1]) * 0.02 + Math.sin(t * 0.0006 + i) * 0.0009;
295
+ pPos[ix + 2] += (pHome[ix + 2] - pPos[ix + 2]) * 0.02;
296
+ }
297
+ }
298
+ particles.geometry.attributes.position.needsUpdate = true;
299
+ if (!REDUCED) particles.rotation.y = clock.elapsedTime * 0.01;
300
+ }
301
+
302
+ // place hover panel above active hero's head (screen projection)
303
+ if (activeIndex >= 0) {
304
+ const e = heroEntries[activeIndex];
305
+ _proj.copy(e.slot.position); _proj.y += 2.6; // ~60cm above head
306
+ _proj.project(camera);
307
+ const rect = canvas.getBoundingClientRect();
308
+ const sx = (_proj.x * 0.5 + 0.5) * rect.width;
309
+ const sy = (-_proj.y * 0.5 + 0.5) * rect.height;
310
+ panel.place(sx, sy);
311
+ }
312
+
313
+ renderer.render(scene, camera);
314
+ requestAnimationFrame(frame);
315
+ }
316
+
317
+ document.getElementById('loading')?.remove();
318
+ requestAnimationFrame(frame);
319
+
320
+ // expose for smoke tests
321
+ window.__SZL_ROSTER__ = { setActive, heroEntries, HEROES };
322
+ }
323
+
324
+ function collectBodyMats(figure) {
325
+ const mats = [];
326
+ figure.traverse(o => { if (o.isMesh && o.material && o.material.map) mats.push(o.material); });
327
+ return mats;
328
+ }
329
+
330
+ function animateProp(prop, t) {
331
+ if (prop.userData.spin) prop.userData.spin.rotation.z = t * 0.003;
332
+ if (prop.userData.orbit) {
333
+ prop.userData.orbit.forEach((r, k) => {
334
+ const a = (k / 8) * Math.PI * 2 + t * 0.001;
335
+ r.position.set(Math.cos(a) * 0.7, 1.0 + Math.sin(t * 0.002 + k) * 0.05, Math.sin(a) * 0.7);
336
+ });
337
+ }
338
+ if (prop.userData.stream) {
339
+ prop.userData.stream.forEach((s, k) => {
340
+ s.position.x = 0.5 + ((t * 0.001 + k * 0.2) % 0.6);
341
+ s.material.opacity = 0.3 + 0.6 * Math.abs(Math.sin(t * 0.003 + k));
342
+ });
343
+ }
344
+ }
js/superpower_icons.js ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // superpower_icons.js
2
+ // Animated SVG icon set, one per superpower. Each factory returns an <svg>
3
+ // element (24x24 viewBox) with embedded CSS/SMIL animation. Color is injected.
4
+ // Animations respect prefers-reduced-motion via the `reduced` flag (freezes).
5
+
6
+ const NS = 'http://www.w3.org/2000/svg';
7
+
8
+ function svg(color, reduced, inner) {
9
+ const s = `<svg xmlns="${NS}" viewBox="0 0 24 24" width="22" height="22" fill="none"
10
+ stroke="${color}" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"
11
+ class="${reduced ? 'sp-static' : 'sp-anim'}">${inner}</svg>`;
12
+ const wrap = document.createElement('span');
13
+ wrap.className = 'sp-icon';
14
+ wrap.innerHTML = s;
15
+ return wrap;
16
+ }
17
+
18
+ // Each builder takes (color, reduced) and returns an element.
19
+ const ICONS = {
20
+ // ---- AMARU ----
21
+ amaru_dualstream: (c, r) => svg(c, r, `
22
+ <path d="M3 9 H18"><animate attributeName="stroke-dashoffset" values="0;-12" dur="1s" repeatCount="indefinite"/></path>
23
+ <path d="M14 6 L18 9 L14 12" stroke-dasharray="0"/>
24
+ <path d="M21 15 H6"><animate attributeName="stroke-dashoffset" values="0;12" dur="1s" repeatCount="indefinite"/></path>
25
+ <path d="M10 18 L6 15 L10 12"/>`),
26
+ amaru_memory: (c, r) => {
27
+ let dots = '';
28
+ for (let i = 0; i < 7; i++) { const a = (i / 7) * Math.PI * 2; dots += `<circle cx="${(12 + Math.cos(a) * 8).toFixed(1)}" cy="${(12 + Math.sin(a) * 8).toFixed(1)}" r="1.4" fill="${c}" stroke="none"/>`; }
29
+ return svg(c, r, `<g>${dots}<animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="6s" repeatCount="indefinite"/></g>`);
30
+ },
31
+ amaru_forward: (c, r) => svg(c, r, `
32
+ <circle cx="18" cy="12" r="3"/><circle cx="18" cy="12" r="0.8" fill="${c}" stroke="none"/>
33
+ <path d="M3 12 q3 -4 6 0 q3 4 6 0"><animate attributeName="stroke-dashoffset" values="0;-12" dur="1.4s" repeatCount="indefinite"/></path>`),
34
+ amaru_lineage: (c, r) => svg(c, r, `
35
+ <path d="M12 21 V13" stroke-dasharray="14"><animate attributeName="stroke-dashoffset" values="14;0" dur="1.6s" repeatCount="indefinite"/></path>
36
+ <path d="M12 13 L6 6 M12 13 L18 6" stroke-dasharray="11"><animate attributeName="stroke-dashoffset" values="11;0" dur="1.6s" begin="0.4s" repeatCount="indefinite"/></path>
37
+ <circle cx="12" cy="21" r="1.4" fill="${c}" stroke="none"/><circle cx="6" cy="6" r="1.4" fill="${c}" stroke="none"/><circle cx="18" cy="6" r="1.4" fill="${c}" stroke="none"/>`),
38
+ // ---- SENTRA ----
39
+ sentra_gates: (c, r) => {
40
+ let locks = '';
41
+ for (let i = 0; i < 8; i++) { const x = 3 + (i % 4) * 6; const y = i < 4 ? 7 : 16;
42
+ locks += `<g><rect x="${x - 1.5}" y="${y}" width="3" height="3" rx="0.5"/><path d="M${x - 1} ${y} v-1.5 a1 1 0 0 1 2 0 V${y}"><animate attributeName="d" values="M${x - 1} ${y} v-1.5 a1 1 0 0 1 2 0 V${y}; M${x - 1} ${y} v-0.5 a1 1 0 0 1 2 0 V${y}" dur="0.9s" begin="${i * 0.1}s" repeatCount="indefinite"/></path></g>`; }
43
+ return svg(c, r, locks);
44
+ },
45
+ sentra_dsse: (c, r) => svg(c, r, `
46
+ <path d="M4 17 q4 -8 8 -4 q4 4 8 -4" />
47
+ <g opacity="0"><rect x="14" y="13" width="7" height="5" rx="1"/><text x="17.5" y="17" font-size="3" fill="${c}" stroke="none" text-anchor="middle">SIG</text>
48
+ <animate attributeName="opacity" values="0;1;1;0" dur="2s" repeatCount="indefinite"/>
49
+ <animateTransform attributeName="transform" type="scale" values="1.4;1" dur="0.4s" repeatCount="indefinite" additive="sum"/></g>`),
50
+ sentra_conduction: (c, r) => svg(c, r, `
51
+ <path d="M2 12 H7 L9 6 L12 18 L14 12 H22" stroke-dasharray="40"><animate attributeName="stroke-dashoffset" values="40;0;-40" dur="1.6s" repeatCount="indefinite"/></path>`),
52
+ sentra_cosign: (c, r) => svg(c, r, `
53
+ <circle cx="8" cy="12" r="3"/><path d="M11 12 H20 M17 12 V15 M20 12 V16"/>
54
+ <g><animateTransform attributeName="transform" type="rotate" values="0 8 12;90 8 12;0 8 12" dur="1.8s" repeatCount="indefinite"/><line x1="8" y1="12" x2="8" y2="9"/></g>`),
55
+ // ---- ROSIE ----
56
+ rosie_ensemble: (c, r) => {
57
+ let tally = '';
58
+ for (let i = 0; i < 5; i++) tally += `<line x1="${4 + i * 3.5}" y1="6" x2="${4 + i * 3.5}" y2="18" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.3s" begin="${i * 0.25}s" fill="freeze" repeatCount="1"/><animate attributeName="opacity" values="1;1;0" dur="2.5s" begin="2.5s" repeatCount="indefinite"/></line>`;
59
+ return svg(c, r, tally);
60
+ },
61
+ rosie_routing: (c, r) => svg(c, r, `
62
+ <path d="M3 18 H8 L12 6 H16 L21 12"/>
63
+ <circle cx="3" cy="18" r="1.6" fill="${c}" stroke="none"><animateMotion dur="1.8s" repeatCount="indefinite" path="M0 0 H5 L9 -12 H13 L18 -6"/></circle>`),
64
+ rosie_step: (c, r) => svg(c, r, `
65
+ <path d="M13 2 L4 14 H11 L9 22 L20 9 H12 Z" fill="${c}" stroke="none" opacity="0.9"><animate attributeName="opacity" values="0.4;1;0.4" dur="0.9s" repeatCount="indefinite"/></path>`),
66
+ rosie_exec: (c, r) => svg(c, r, `
67
+ <rect x="4" y="14" width="3.5" height="6" rx="0.5"><animate attributeName="height" values="2;6;2" dur="1.6s" repeatCount="indefinite"/><animate attributeName="y" values="18;14;18" dur="1.6s" repeatCount="indefinite"/></rect>
68
+ <rect x="10" y="10" width="3.5" height="10" rx="0.5"><animate attributeName="height" values="4;10;4" dur="1.6s" begin="0.2s" repeatCount="indefinite"/><animate attributeName="y" values="16;10;16" dur="1.6s" begin="0.2s" repeatCount="indefinite"/></rect>
69
+ <rect x="16" y="7" width="3.5" height="13" rx="0.5"><animate attributeName="height" values="6;13;6" dur="1.6s" begin="0.4s" repeatCount="indefinite"/><animate attributeName="y" values="14;7;14" dur="1.6s" begin="0.4s" repeatCount="indefinite"/></rect>`),
70
+ // ---- KILLINCHU ----
71
+ kil_noaa: (c, r) => svg(c, r, `
72
+ <circle cx="5" cy="12" r="2.5" fill="${c}" stroke="none"/>
73
+ <path d="M7 10 L21 6 M7 12 H21 M7 14 L21 18"><animate attributeName="stroke-dashoffset" values="0;-10" dur="1.2s" repeatCount="indefinite"/></path>`),
74
+ kil_usgs: (c, r) => svg(c, r, `
75
+ <path d="M2 12 H6 L8 5 L11 19 L13 9 L15 15 L17 12 H22" stroke-dasharray="42"><animate attributeName="stroke-dashoffset" values="42;0;-42" dur="2s" repeatCount="indefinite"/></path>`),
76
+ kil_mq9: (c, r) => svg(c, r, `
77
+ <g><path d="M2 13 q5 -3 9 -1 q5 -4 11 -2 q-5 2 -9 2 q-5 2 -11 1 Z" fill="${c}" stroke="none" opacity="0.85"/>
78
+ <animateTransform attributeName="transform" type="translate" values="-2 1;2 -1;-2 1" dur="2.4s" repeatCount="indefinite"/></g>`),
79
+ kil_phase: (c, r) => svg(c, r, `
80
+ <path d="M2 12 q3 -7 6 0 q3 7 6 0 q3 -7 6 0" opacity="0.4"/>
81
+ <path d="M2 12 q3 -7 6 0 q3 7 6 0 q3 -7 6 0"><animateTransform attributeName="transform" type="translate" values="0 0;6 0;0 0" dur="2s" repeatCount="indefinite"/></path>`),
82
+ // ---- A11OY ----
83
+ a11_governance: (c, r) => svg(c, r, `
84
+ <g><line x1="4" y1="8" x2="20" y2="8"/><line x1="12" y1="4" x2="12" y2="8"/>
85
+ <path d="M4 8 l-2 5 h4 Z"/><path d="M20 8 l-2 5 h4 Z"/>
86
+ <animateTransform attributeName="transform" type="rotate" values="-8 12 8;6 12 8;0 12 8" dur="2.4s" repeatCount="indefinite"/></g>
87
+ <path d="M9 20 H15"/>`),
88
+ a11_router: (c, r) => svg(c, r, `
89
+ <circle cx="12" cy="12" r="3"/><path d="M12 9 V3 M12 15 V21 M9 12 H3 M15 12 H21 M14 10 l4 -4 M10 14 l-4 4 M14 14 l4 4 M10 10 l-4 -4"><animate attributeName="opacity" values="0.3;1;0.3" dur="1.5s" repeatCount="indefinite"/></path>`),
90
+ a11_formulas: (c, r) => {
91
+ let dots = '';
92
+ for (let i = 0; i < 35; i++) { const x = 3 + (i % 7) * 3; const y = 4 + Math.floor(i / 7) * 4;
93
+ dots += `<circle cx="${x}" cy="${y}" r="0.9" fill="${c}" stroke="none" opacity="0"><animate attributeName="opacity" values="0;1" dur="0.2s" begin="${(i * 0.04).toFixed(2)}s" fill="freeze" repeatCount="1"/></circle>`; }
94
+ return svg(c, r, `<g>${dots}<animate attributeName="opacity" values="1;0.3;1" dur="3s" begin="1.6s" repeatCount="indefinite"/></g>`);
95
+ },
96
+ a11_khipu: (c, r) => svg(c, r, `
97
+ <path d="M3 6 H21" /><path d="M6 6 V18 M12 6 V20 M18 6 V16" stroke-dasharray="16"><animate attributeName="stroke-dashoffset" values="16;0" dur="1.4s" repeatCount="indefinite"/></path>
98
+ <circle cx="6" cy="14" r="1.6" fill="${c}" stroke="none"/><circle cx="12" cy="16" r="1.6" fill="${c}" stroke="none"/><circle cx="18" cy="12" r="1.6" fill="${c}" stroke="none"/>`),
99
+ };
100
+
101
+ export function buildIcon(name, color, reduced) {
102
+ const f = ICONS[name];
103
+ if (f) return f(color, reduced);
104
+ return svg(color, reduced, `<circle cx="12" cy="12" r="6" fill="${color}" stroke="none"/>`);
105
+ }
js/uds_npcs.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // uds_npcs.js
2
+ // Background UDS NPC silhouettes — same humanoid template, neutral grey,
3
+ // "UDS" monogram decal, standing at parade rest. They orbit slowly + idle-sway.
4
+ import * as THREE from 'three';
5
+ import { makeNPCTexture } from './canvas_decals.js';
6
+
7
+ let sharedTexture = null;
8
+
9
+ function npcMaterials() {
10
+ if (!sharedTexture) sharedTexture = makeNPCTexture();
11
+ return {
12
+ body: new THREE.MeshStandardMaterial({ map: sharedTexture, roughness: 0.85, metalness: 0.05, flatShading: true }),
13
+ limb: new THREE.MeshStandardMaterial({ color: 0x2c3138, roughness: 0.9, flatShading: true }),
14
+ head: new THREE.MeshStandardMaterial({ color: 0x3c4249, roughness: 0.9, flatShading: true }),
15
+ };
16
+ }
17
+
18
+ function buildNPC() {
19
+ const g = new THREE.Group();
20
+ const m = npcMaterials();
21
+ const torso = new THREE.Mesh(new THREE.CapsuleGeometry(0.3, 0.6, 4, 8), m.body);
22
+ torso.position.y = 1.05; g.add(torso);
23
+ const head = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 10), m.head);
24
+ head.position.y = 1.62; g.add(head);
25
+ for (const side of [-1, 1]) {
26
+ // arms at parade rest — hands clasped front, slight inward angle
27
+ const arm = new THREE.Mesh(new THREE.CapsuleGeometry(0.08, 0.5, 4, 6), m.limb);
28
+ arm.position.set(side * 0.36, 0.92, 0.08);
29
+ arm.rotation.z = side * 0.12; arm.rotation.x = -0.25;
30
+ g.add(arm);
31
+ const leg = new THREE.Mesh(new THREE.CapsuleGeometry(0.11, 0.6, 4, 6), m.limb);
32
+ leg.position.set(side * 0.15, 0.5, 0);
33
+ g.add(leg);
34
+ }
35
+ return g;
36
+ }
37
+
38
+ // Create N NPCs arranged in a back arc. Returns { group, npcs[] } for animation.
39
+ export function buildUDSNPCs(count = 4) {
40
+ const group = new THREE.Group();
41
+ const npcs = [];
42
+ for (let i = 0; i < count; i++) {
43
+ const npc = buildNPC();
44
+ const a = -0.9 + (i / Math.max(1, count - 1)) * 1.8; // spread across back arc
45
+ const radius = 6.5;
46
+ npc.position.set(Math.sin(a) * radius, 0, -radius * 0.55 + Math.cos(a) * 0.4);
47
+ npc.scale.setScalar(0.85);
48
+ npc.rotation.y = -a * 0.5; // face roughly toward center/camera
49
+ npc.userData = { baseAngle: a, radius, phase: Math.random() * Math.PI * 2, baseX: npc.position.x };
50
+ group.add(npc); npcs.push(npc);
51
+ }
52
+ return { group, npcs };
53
+ }
54
+
55
+ // Slow orbit + idle sway. orbit is subtle so NPCs stay readable in-frame.
56
+ export function animateNPCs(npcs, t, reduced) {
57
+ if (reduced) return;
58
+ for (const npc of npcs) {
59
+ const u = npc.userData;
60
+ npc.position.y = Math.sin(t * 0.0009 + u.phase) * 0.04; // idle bob
61
+ npc.rotation.z = Math.sin(t * 0.0011 + u.phase) * 0.02; // idle sway
62
+ npc.position.x = u.baseX + Math.sin(t * 0.00012 + u.phase) * 0.6; // slow lateral orbit
63
+ }
64
+ }