cozyclay 3.1.3 → 3.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/INSTALL_FOR_AGENTS.md +3 -0
  3. package/README.md +5 -0
  4. package/bin/agent/agent-failure-codes.mjs +31 -0
  5. package/bin/agent/agent-routes.mjs +5 -10
  6. package/bin/agent/agent-runner.mjs +5 -3
  7. package/bin/cozyclay.mjs +18 -112
  8. package/bin/star-prompt.mjs +123 -0
  9. package/bin/studio-routes.mjs +69 -0
  10. package/bin/update-check.mjs +53 -5
  11. package/bin/update-install.mjs +136 -0
  12. package/bin/update-routes.mjs +80 -0
  13. package/dist/ai-camera-control/index.html +1 -1
  14. package/dist/app/index.html +5 -6
  15. package/dist/assets/{TaskBoard-U1Yl-4F1.js → TaskBoard-DwXhsJXz.js} +1 -1
  16. package/dist/assets/app-CEP7jzXB.css +1 -0
  17. package/dist/assets/app-CTKcyPM-.js +5144 -0
  18. package/dist/assets/compile-worker-or7djI2v.js +6 -0
  19. package/dist/assets/{motion3-compile-DDG3wklD.js → motion3-compile-Du7UXrWn.js} +1 -1
  20. package/dist/assets/scene-asset-sync-t45pWCyq.js +13 -0
  21. package/dist/assets/task-board-CkLZGPkv.js +15 -0
  22. package/dist/assets/workflow-DJwOUzqJ.js +179 -0
  23. package/dist/cozyclay-package.json +1 -1
  24. package/dist/greybox-to-video/index.html +3 -3
  25. package/dist/index.html +533 -264
  26. package/dist/media/cozyclay-mark.png +0 -0
  27. package/dist/media/cozyclay-truck-demo-poster.jpg +0 -0
  28. package/dist/media/step-move-poster.jpg +0 -0
  29. package/dist/media/step-shoot-poster.jpg +0 -0
  30. package/dist/media/step-stage-poster.jpg +0 -0
  31. package/dist/media/truck-greybox-poster.jpg +0 -0
  32. package/dist/media/truck-seedance-poster.jpg +0 -0
  33. package/dist/previs-software/index.html +4 -4
  34. package/dist/privacy/index.html +46 -19
  35. package/dist/seedance-camera-control/index.html +3 -3
  36. package/dist/sitemap.xml +6 -6
  37. package/dist/workflow/index.html +3 -4
  38. package/package.json +1 -1
  39. package/src/App.jsx +59 -70
  40. package/src/analytics-controls.js +17 -0
  41. package/src/analytics-friction.js +124 -0
  42. package/src/analytics-strict.js +139 -0
  43. package/src/analytics-values.js +49 -0
  44. package/src/analytics.js +37 -133
  45. package/src/asset-pane.jsx +4 -5
  46. package/src/click-friction.js +101 -0
  47. package/src/command-bus-telemetry.js +155 -0
  48. package/src/command-bus.js +10 -8
  49. package/src/commands/motion3.js +7 -2
  50. package/src/controls.jsx +3 -2
  51. package/src/crash-telemetry.js +45 -0
  52. package/src/domains/cast.js +2 -11
  53. package/src/domains/motion.js +49 -38
  54. package/src/domains/objects.js +3 -0
  55. package/src/domains/scene-undo.js +27 -0
  56. package/src/domains/scenes.js +1 -1
  57. package/src/error-boundary.jsx +2 -0
  58. package/src/export-attempt.js +100 -0
  59. package/src/export-workspace.js +12 -1
  60. package/src/first-success-guide.jsx +11 -7
  61. package/src/guide-telemetry.js +60 -0
  62. package/src/hierarchy-panel.jsx +2 -1
  63. package/src/live-control.js +2 -9
  64. package/src/locale-zh/domains.js +1 -0
  65. package/src/main.jsx +5 -0
  66. package/src/motion/timeline.jsx +9 -5
  67. package/src/motion-draft-telemetry.js +62 -0
  68. package/src/motion3/compiler/channels.js +7 -5
  69. package/src/motion3/compiler/compile.js +6 -2
  70. package/src/motion3/compiler/finger-close.js +84 -7
  71. package/src/motion3/compiler/head-lead.js +70 -0
  72. package/src/motion3/compiler/head-steady.js +1 -1
  73. package/src/motion3/compiler/limbs.js +10 -3
  74. package/src/motion3/compiler/pose.js +1 -1
  75. package/src/motion3/compiler/validate.js +5 -3
  76. package/src/motion3/pipeline.js +4 -0
  77. package/src/motion3/qa/explain.js +2 -1
  78. package/src/motion3/qa/finger-checks.js +80 -0
  79. package/src/motion3/qa/gait-observe.js +88 -0
  80. package/src/motion3/qa/grasp-checks.js +3 -0
  81. package/src/motion3/qa/hints-grasp.js +8 -0
  82. package/src/motion3/qa/index.js +2 -1
  83. package/src/motion3/qa/motion-life.js +26 -3
  84. package/src/motion3/schema/topics.js +1 -1
  85. package/src/playback-telemetry.js +107 -0
  86. package/src/post-turn-edits.js +62 -0
  87. package/src/search-friction.js +49 -0
  88. package/src/semantic-edit.js +19 -6
  89. package/src/shell/BoardSlot.jsx +3 -3
  90. package/src/shell/BottomDock.jsx +1 -1
  91. package/src/shell/DetailsSlot.jsx +1 -1
  92. package/src/shell/MenuBar.jsx +13 -13
  93. package/src/shell/StudioShell.jsx +7 -1
  94. package/src/shell/TopBar.jsx +2 -0
  95. package/src/shell/ViewportToolbar.jsx +2 -2
  96. package/src/shortcut-friction.js +122 -0
  97. package/src/stage-canvas-rigs.jsx +3 -4
  98. package/src/studio-telemetry.js +97 -0
  99. package/src/thrash-friction.js +90 -0
  100. package/src/update/UpdateChip.jsx +86 -0
  101. package/src/update/UpdateDialog.jsx +147 -0
  102. package/src/update/update-client.js +188 -0
  103. package/src/update/update-telemetry.js +78 -0
  104. package/src/update/update.css +94 -0
  105. package/src/use-webgl-available.js +35 -0
  106. package/src/workflow/AgentPanel.jsx +29 -30
  107. package/src/workflow/CozySceneNode.jsx +1 -1
  108. package/src/workflow/LoginPanel.jsx +15 -5
  109. package/src/workflow/agent-client.js +10 -91
  110. package/src/workflow/agent-failure-codes.js +47 -0
  111. package/src/workflow/agent-turn-telemetry.js +222 -0
  112. package/src/workflow/connect-telemetry.js +86 -0
  113. package/src/workflow/prompt-telemetry.js +105 -0
  114. package/tools/dev/pages/privacy.html +44 -17
  115. package/tools/loc-baseline.json +10 -10
  116. package/tools/run-tests.mjs +8 -8
  117. package/dist/assets/app-BdRlMf4J.css +0 -1
  118. package/dist/assets/app-DwETmoQZ.js +0 -5144
  119. package/dist/assets/compile-worker-oy_P9HHh.js +0 -6
  120. package/dist/assets/landing-y3EqDgSZ.js +0 -1
  121. package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +0 -1
  122. package/dist/assets/scene-asset-sync-Bz59Ghdz.js +0 -13
  123. package/dist/assets/task-board-DFfNa0xg.js +0 -15
  124. package/dist/assets/workflow-BMJGd_gZ.js +0 -179
  125. package/dist/media/playground-poster.jpg +0 -0
package/dist/index.html CHANGED
@@ -16,18 +16,18 @@
16
16
  <meta property="og:title" content="CozyClay — open source 3D previs software, in your browser" />
17
17
  <meta property="og:description" content="Block a scene, pose characters, author camera moves and cuts, and preview generated motion. Open source, runs in your browser." />
18
18
  <meta property="og:url" content="https://cozyclay.org/" />
19
- <meta property="og:image" content="https://cozyclay.org/media/cozyclay-demo-poster.jpg" />
19
+ <meta property="og:image" content="https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg" />
20
20
  <meta property="og:image:width" content="1920" />
21
21
  <meta property="og:image:height" content="1080" />
22
- <meta property="og:image:alt" content="A CozyClay shot mid-wipe: the greybox previs on one half, the generated result on the other." />
22
+ <meta property="og:image:alt" content="A CozyClay shot mid-wipe: the clay greybox of a truck driver on one half, the Seedance 2.5 generation on the other." />
23
23
  <meta name="twitter:card" content="summary_large_image" />
24
24
  <meta name="twitter:title" content="CozyClay — open source 3D previs software, in your browser" />
25
25
  <meta name="twitter:description" content="Block a scene, pose characters, author camera moves and cuts, and preview generated motion. Open source, runs in your browser." />
26
- <meta name="twitter:image" content="https://cozyclay.org/media/cozyclay-demo-poster.jpg" />
26
+ <meta name="twitter:image" content="https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg" />
27
27
 
28
28
  <title>CozyClay — open source 3D previs software, in your browser</title>
29
29
 
30
- <meta property="og:video" content="https://cozyclay.org/media/cozyclay-demo.mp4" />
30
+ <meta property="og:video" content="https://cozyclay.org/media/truck-seedance.mp4" />
31
31
  <meta property="og:video:type" content="video/mp4" />
32
32
  <meta property="og:video:width" content="1920" />
33
33
  <meta property="og:video:height" content="1080" />
@@ -46,9 +46,10 @@
46
46
  "applicationCategory": "DesignApplication",
47
47
  "applicationSubCategory": "3D previsualization",
48
48
  "operatingSystem": "Any (Chromium-based browser)",
49
+ "softwareVersion": "3.1.5",
49
50
  "description": "CozyClay is an open-source, browser-based 3D staging studio. Block a scene, pose characters, sequence motion prompts on a timeline, and preview generated motion in one local workspace that handles like the Unity Editor.",
50
- "image": "https://cozyclay.org/media/cozyclay-demo-poster.jpg",
51
- "screenshot": "https://cozyclay.org/media/cozyclay-demo-poster.jpg",
51
+ "image": "https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg",
52
+ "screenshot": "https://cozyclay.org/media/cozyclay-truck-demo-poster.jpg",
52
53
  "logo": "https://cozyclay.org/media/cozyclay-logo.png",
53
54
  "license": "https://github.com/NomaDamas/CozyClay/blob/main/LICENSE",
54
55
  "codeRepository": "https://github.com/NomaDamas/CozyClay",
@@ -58,333 +59,465 @@
58
59
  </script>
59
60
 
60
61
  <style>
62
+ @font-face { font-family: "Instrument Serif"; font-weight: 400; font-display: swap; src: url("/fonts/instrument-serif-latin.woff2") format("woff2"); }
63
+ @font-face { font-family: "Instrument Serif"; font-style: italic; font-weight: 400; font-display: swap; src: url("/fonts/instrument-serif-italic-latin.woff2") format("woff2"); }
64
+ @font-face { font-family: "Inter"; font-weight: 100 900; font-display: swap; src: url("/fonts/inter-latin.woff2") format("woff2"); }
65
+ @font-face { font-family: "JetBrains Mono"; font-weight: 400; font-display: swap; src: url("/fonts/jetbrains-mono-400-latin.woff2") format("woff2"); }
61
66
  :root {
62
- --bg: #f5f5f7;
63
- --alt: #ececf0;
67
+ --paper: #f5f5f7;
68
+ --paper-2: #ececf0;
64
69
  --panel: #ffffff;
65
70
  --line: #d2d2d7;
66
71
  --line-strong: #c7c7cc;
67
- --ink: #1d1d1f;
72
+ --ink: #111111;
68
73
  --muted: #636366;
69
- --accent: #0066cc;
74
+ --brand: #111111;
75
+ --brand-deep: #000000;
76
+ --brand-tint: #e8e8ea;
70
77
  --ok: #2f6b3a;
71
- --shadow-card: 0 1px 2px rgba(0, 0, 0, .04), 0 8px 24px rgba(0, 0, 0, .06);
78
+ --serif: "Instrument Serif", "Iowan Old Style", Georgia, serif;
79
+ --sans: "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", system-ui, sans-serif;
80
+ --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
81
+ --radius: 16px;
82
+ --shadow-clay: 0 1px 2px rgba(0, 0, 0, .04), 0 8px 24px rgba(0, 0, 0, .06);
72
83
  --shadow-window: 0 2px 6px rgba(0, 0, 0, .06), 0 30px 60px rgba(0, 0, 0, .18);
73
- --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
84
+ --header-h: 64px;
74
85
  }
75
86
  * { box-sizing: border-box; }
76
- html { scroll-behavior: smooth; }
87
+ html { scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + 12px); }
77
88
  body {
78
89
  margin: 0;
79
- background: var(--bg);
90
+ background: var(--paper);
80
91
  color: var(--ink);
81
- font: 17px/1.5 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", system-ui, sans-serif;
92
+ font: 17px/1.5 var(--sans);
82
93
  -webkit-font-smoothing: antialiased;
94
+ font-feature-settings: "cv11", "ss01", "ss03";
95
+ letter-spacing: -.011em;
83
96
  }
84
- a { color: var(--accent); }
97
+ a { color: var(--brand); }
85
98
  button { font-family: inherit; }
86
- :focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
99
+ img, video { max-width: 100%; }
100
+ :focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 6px; }
87
101
  .wrap { max-width: 1160px; margin: 0 auto; padding: 0 24px; }
88
102
  .wrap.narrow { max-width: 820px; }
89
103
 
90
- /* Type scale: 64 / 36 / 19 / 17 / 14. Nothing else. */
91
- h1 { font-size: clamp(40px, 6vw, 64px); line-height: 1.05; letter-spacing: -.02em; font-weight: 700; margin: 0; max-width: 820px; }
92
- h1 em { font-style: normal; color: var(--accent); }
93
- h2 { font-size: clamp(28px, 3.4vw, 36px); line-height: 1.15; letter-spacing: -.01em; font-weight: 700; margin: 0 0 32px; }
94
- h3 { font-size: 19px; font-weight: 600; margin: 0; }
104
+ h1 { font-family: var(--serif); font-size: clamp(40px, 6vw, 68px); line-height: 1.02; letter-spacing: -.02em; font-weight: 400; margin: 0; max-width: 900px; }
105
+ h1 em { font-style: italic; color: inherit; }
106
+ h2 { font-family: var(--serif); font-size: clamp(34px, 4.2vw, 52px); line-height: 1.05; letter-spacing: -.015em; font-weight: 400; margin: 0 0 16px; }
107
+ h3 { font-size: 19px; line-height: 1.3; font-weight: 600; margin: 0; letter-spacing: -.02em; }
108
+ .kicker { display: inline-block; margin: 0 0 14px; font: 400 12px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
109
+ .sub { margin: 0 0 40px; max-width: 40em; color: var(--muted); font-size: 19px; }
95
110
  .small, figcaption { font-size: 14px; color: var(--muted); }
96
- code, pre, kbd { font-family: var(--mono); font-size: 14px; }
111
+ code, pre, kbd { font-family: var(--mono); font-size: 13.5px; letter-spacing: 0; }
97
112
 
113
+ /* Header: one row at every width. On phones only the brand and the two
114
+ actions stay; the section links are a scroll away. */
98
115
  header.site {
99
116
  position: sticky; top: 0; z-index: 10;
100
117
  background: rgba(245, 245, 247, .8);
101
118
  backdrop-filter: saturate(180%) blur(20px); -webkit-backdrop-filter: saturate(180%) blur(20px);
102
119
  border-bottom: 1px solid var(--line);
103
120
  }
104
- header.site .wrap { display: flex; align-items: center; justify-content: space-between; gap: 8px 16px; flex-wrap: wrap; padding-top: 10px; padding-bottom: 10px; }
105
- .brand { display: flex; align-items: center; gap: 10px; min-height: 44px; font-weight: 600; color: var(--ink); text-decoration: none; }
106
- .brand img { width: 32px; height: auto; border-radius: 8px; background: #fff; padding: 3px 2px; }
107
- header.site nav { display: flex; align-items: center; gap: 4px 24px; flex-wrap: wrap; font-size: 14px; }
121
+ header.site .wrap { display: flex; align-items: center; gap: 16px; height: var(--header-h); }
122
+ .brand { display: flex; align-items: center; gap: 10px; min-height: 44px; font-weight: 600; font-size: 18px; letter-spacing: -.02em; color: var(--ink); text-decoration: none; margin-right: auto; }
123
+ .brand img { width: 36px; height: auto; }
124
+ header.site nav { display: flex; align-items: center; gap: 28px; font-size: 15px; }
108
125
  header.site nav a { display: flex; align-items: center; min-height: 44px; color: var(--muted); text-decoration: none; }
109
126
  header.site nav a:hover { color: var(--ink); }
110
- header.site nav a.btn, header.site nav a.btn:hover { color: #fff; font-size: 14px; min-height: 44px; padding: 0 16px; }
127
+ .actions { display: flex; align-items: center; gap: 8px; }
128
+ .icon-link { display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; border-radius: 999px; color: var(--ink); }
129
+ .icon-link:hover { background: var(--paper-2); }
111
130
 
112
131
  .btn {
113
- display: inline-flex; align-items: center; justify-content: center;
132
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
114
133
  min-height: 48px; padding: 0 24px; border-radius: 8px; border: 1px solid transparent;
115
- font-size: 17px; font-weight: 600; text-decoration: none; cursor: pointer;
116
- transition: filter .15s, transform .15s;
134
+ font-size: 17px; font-weight: 600; text-decoration: none; cursor: pointer; white-space: nowrap;
135
+ transition: background-color .15s, transform .15s, box-shadow .15s;
117
136
  }
118
- .btn:hover { filter: brightness(1.08); }
119
- .btn:active { transform: scale(.98); }
120
- .btn.primary { background: var(--accent); color: #fff; }
137
+ .btn:active { transform: translateY(1px) scale(.99); }
138
+ .btn.primary { background: var(--brand); color: #fff; }
139
+ .btn.primary:hover { background: var(--brand-deep); }
121
140
  .btn.ghost { background: var(--panel); color: var(--ink); border-color: var(--line-strong); }
141
+ .btn.ghost:hover { border-color: var(--ink); }
142
+ .btn.sm { min-height: 40px; padding: 0 18px; font-size: 15px; }
122
143
  .cta { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; }
123
144
 
124
- .hero { padding: 80px 0 48px; }
125
- .hero .wrap { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 24px; }
126
- .lede { margin: 0; color: var(--muted); }
145
+ /* Hero: headline left, the pitch and the two ways in on the right. */
146
+ /* First screen: black stage, the headline and the reel. Scrolling plays
147
+ it out: the reel rises over the headline and grows to the full width,
148
+ then the page continues. --p (0..1) is set by the script below. */
149
+ .stage {
150
+ position: relative; height: 210vh; color: #f2efe8; --p: 0;
151
+ background:
152
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .05 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"),
153
+ radial-gradient(120% 90% at 50% 35%, #1f1e1c 0%, #161615 55%, #111110 100%);
154
+ }
155
+ .stage-pin {
156
+ position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden;
157
+ display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
158
+ padding-top: calc(var(--header-h) + 2vh);
159
+ }
160
+ .stage h1 {
161
+ margin: 0; padding: 0 4vw; width: 100%; max-width: none; text-align: center;
162
+ font-family: var(--sans); font-size: clamp(44px, 8.6vw, 140px); line-height: 1; letter-spacing: -.06em; font-weight: 600;
163
+ font-feature-settings: "ss01", "cv11";
164
+ opacity: calc(.88 * (1 - var(--p) * 1.4));
165
+ transform: translateY(calc(var(--p) * -12vh)) scale(calc(1 - var(--p) * .12));
166
+ transform-origin: 50% 0;
167
+ }
168
+ .stage-reel {
169
+ position: relative; z-index: 1; width: min(1280px, 94vw); margin-top: 7vh;
170
+ transform: translateY(calc(var(--p) * var(--rise, 0px))) scale(calc(.94 + var(--p) * var(--grow, .06)));
171
+ transform-origin: 50% 0;
172
+ }
173
+ .stage .window { border-color: #2c2b29; background: #1a1a19; box-shadow: 0 30px 80px rgba(0, 0, 0, .45); }
174
+ .stage .titlebar { background: #22211f; border-bottom-color: #2c2b29; }
175
+ .stage .titlebar span { color: #8a8a90; }
176
+ .stage-cue {
177
+ position: absolute; bottom: 3vh; left: 50%; margin-left: -11px; color: #8a8a90;
178
+ opacity: calc(1 - var(--p) * 5); animation: cue 1.8s ease-in-out infinite;
179
+ }
180
+ @keyframes cue { 50% { transform: translateY(6px); } }
181
+ body.on-stage header.site { background: #151514; border-bottom-color: #262624; }
182
+ body.on-stage header.site .brand, body.on-stage header.site .icon-link { color: #f2efe8; }
183
+ body.on-stage header.site nav a { color: #9a9aa0; }
184
+ body.on-stage header.site nav a:hover { color: #f2efe8; }
185
+ body.on-stage header.site .btn.primary { background: #f2efe8; color: #0b0b0c; }
186
+ .hero-grid { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 28px; }
187
+
188
+ .stage h1 .line { display: inline-block; white-space: nowrap; }
189
+ .curved .ch { display: inline-block; white-space: pre; transform-origin: 50% 55%; will-change: transform; }
190
+ .hero-side { display: flex; flex-direction: column; align-items: center; gap: 22px; max-width: 640px; }
191
+ .lede { margin: 0; color: var(--muted); font-size: 19px; }
192
+ .hero-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; }
127
193
  .cmd {
128
- display: inline-flex; align-items: center; gap: 4px; padding: 0 4px 0 14px;
129
- background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
194
+ display: inline-flex; align-items: center; gap: 4px; padding: 0 4px 0 14px; min-height: 48px;
195
+ background: var(--panel); border: 1px solid var(--line-strong); border-radius: 8px;
130
196
  }
131
- .cmd .copy { width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; background: none; border: 0; color: var(--muted); cursor: pointer; }
197
+ .cmd .copy { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: none; border: 0; color: var(--muted); cursor: pointer; border-radius: 6px; }
198
+ .cmd .copy:hover { color: var(--ink); background: var(--paper); }
132
199
  .cmd .copy .ic-done { display: none; color: var(--ok); }
133
200
  .cmd .copy[data-state="copied"] .ic-copy { display: none; }
134
201
  .cmd .copy[data-state="copied"] .ic-done { display: block; }
202
+ .meta-line { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 16px; margin: 0; color: var(--muted); font-size: 14px; }
203
+ .meta-line a { color: var(--muted); }
204
+ .meta-line > * + *::before { content: "·"; margin-right: 8px; }
205
+ .meta-line { gap: 4px 8px; }
135
206
 
136
207
  .window { border: 1px solid var(--line-strong); border-radius: 12px; overflow: hidden; background: var(--panel); box-shadow: var(--shadow-window); }
137
208
  .titlebar { display: flex; align-items: center; gap: 8px; height: 40px; padding: 0 14px; background: #e8e8ed; border-bottom: 1px solid var(--line); }
138
209
  .titlebar i { width: 12px; height: 12px; border-radius: 50%; background: #ff5f57; }
139
210
  .titlebar i:nth-child(2) { background: #febc2e; }
140
211
  .titlebar i:nth-child(3) { background: #28c840; }
141
- .titlebar span { flex: 1; text-align: center; margin-right: 52px; font-size: 14px; font-weight: 600; color: var(--muted); }
212
+ .titlebar span { flex: 1; text-align: center; margin-right: 52px; font-size: 13px; font-weight: 500; color: var(--muted); }
142
213
 
143
- .proof { padding: 0 0 96px; }
144
- .proof figure { margin: 0; }
145
- .proof video { display: block; width: 100%; height: auto; background: #000; }
146
- .proof figcaption { margin-top: 16px; text-align: center; }
147
214
 
148
- section { padding: 88px 0; border-top: 1px solid var(--line); }
149
- section.alt { background: var(--alt); }
150
- section.hero, section.proof { border-top: 0; }
151
215
 
152
- .cards { display: flex; flex-wrap: wrap; gap: 20px; }
153
- .card {
154
- flex: 1 1 240px; min-width: 0; padding: 24px;
155
- background: var(--panel); border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow-card);
216
+ /* Before/after compare. --pos is the divider, set by the script below. */
217
+ .compare { position: relative; aspect-ratio: 16 / 9; background: #000; cursor: ew-resize; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
218
+ .compare video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; pointer-events: none; }
219
+ .compare .cmp-b { clip-path: inset(0 0 0 var(--pos)); }
220
+ .cmp-line { position: absolute; top: 0; bottom: 0; left: var(--pos); width: 2px; margin-left: -1px; background: #fff; pointer-events: none; }
221
+ .cmp-knob {
222
+ position: absolute; top: 50%; left: 50%; width: 44px; height: 44px; margin: -22px 0 0 -22px;
223
+ display: flex; align-items: center; justify-content: center; border-radius: 50%;
224
+ background: #fff; color: var(--ink); box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
156
225
  }
157
- .card p { margin: 4px 0 0; color: var(--muted); }
158
- .card .n { display: block; margin-bottom: 16px; font-size: clamp(28px, 3.4vw, 36px); font-weight: 700; line-height: 1; color: var(--accent); }
159
- a.card { color: var(--ink); text-decoration: none; font-size: 19px; font-weight: 600; min-height: 96px; transition: transform .2s; }
160
- a.card:hover { transform: translateY(-2px); }
161
-
162
- /* Playground. The ids and data-state values belong to the script below. */
163
- .demo { scroll-margin-top: 76px; }
164
- .demo .window { min-width: 0; box-shadow: 0 2px 6px rgba(0, 0, 0, .06), 0 24px 48px rgba(0, 0, 0, .14); }
165
- .playground {
166
- position: relative;
167
- /* Header 64 + margin 12 + title bar 40 + bottom bar 52 + 12. */
168
- height: clamp(480px, calc(100vh - 180px), 900px);
169
- overflow: hidden;
170
- background: #1b1b1b center / cover no-repeat;
226
+ .cmp-range { position: absolute; width: 1px; height: 1px; margin: 0; padding: 0; opacity: 0; pointer-events: none; }
227
+ .compare:has(.cmp-range:focus-visible) .cmp-knob { outline: 3px solid var(--brand); outline-offset: 2px; }
228
+ .compare > span {
229
+ position: absolute; bottom: 14px; padding: 5px 10px; border-radius: 6px; pointer-events: none;
230
+ background: rgba(0, 0, 0, .55); color: #fff; font: 400 11px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase;
231
+ backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
171
232
  }
172
- .playground iframe { display: block; width: 100%; height: 100%; border: 0; background: #1b1b1b; }
173
- .playground-cover {
174
- position: absolute; inset: 0; display: flex; flex-direction: column;
175
- align-items: center; justify-content: center; gap: 12px;
176
- background: rgba(245, 245, 247, .55); cursor: pointer; border: 0; color: var(--ink);
177
- font: inherit; padding: 0; width: 100%;
178
- }
179
- .playground-cover:hover { background: rgba(245, 245, 247, .4); }
180
- .playground-cover[hidden] { display: none; }
181
- .playground-cover .btn { pointer-events: none; }
182
- .playground-cover small { font-size: 14px; font-weight: 600; color: var(--ink); }
183
- .playground[data-state="loading"] iframe { opacity: 0; }
184
- .playground-loading {
185
- position: absolute; inset: 0; display: none; align-items: center; justify-content: center;
186
- color: #fff; font-size: 14px; background: rgba(0, 0, 0, .45); pointer-events: none;
233
+ .compare .tag-l { left: 14px; }
234
+ .compare .tag-r { right: 14px; }
235
+
236
+ section { padding: 88px 0; border-top: 1px solid var(--line); }
237
+ section.alt { background: var(--paper-2); }
238
+ section.stage { padding: 0; border-top: 0; }
239
+
240
+ .card {
241
+ min-width: 0; padding: 28px; background: var(--panel);
242
+ border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-clay);
187
243
  }
188
- .playground[data-state="loading"] .playground-loading { display: flex; }
189
- .playground-phone { display: none; }
190
- .playground-bar {
191
- display: none; gap: 8px 12px; align-items: center; flex-wrap: wrap;
192
- padding: 10px 14px; border-top: 1px solid var(--line); background: #e8e8ed;
193
- font-size: 14px; color: var(--muted);
244
+ .card p { margin: 8px 0 0; color: var(--muted); }
245
+
246
+ /* Why */
247
+ .why { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 32px 64px; align-items: start; }
248
+ .why h2 { margin: 0; }
249
+ .why p { margin: 0 0 16px; color: var(--muted); font-size: 19px; }
250
+ .why p strong { color: var(--ink); font-weight: 600; }
251
+
252
+ /* Workflow: pick a step, see it. */
253
+ .flow { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); gap: 24px 40px; align-items: start; }
254
+ .flow-tabs, .ai-tabs { display: flex; flex-direction: column; gap: 4px; }
255
+ .flow-tabs button, .ai-tabs button {
256
+ display: flex; gap: 16px; align-items: flex-start; width: 100%; padding: 16px; text-align: left;
257
+ background: none; border: 1px solid transparent; border-radius: 12px; color: var(--muted); font-size: 15px; cursor: pointer;
258
+ transition: background-color .15s, border-color .15s;
194
259
  }
195
- .playground[data-state="live"] + .playground-bar,
196
- .playground[data-state="loading"] + .playground-bar { display: flex; }
197
- .playground-bar a { color: var(--muted); }
198
- .playground-bar button, button.copy {
199
- min-height: 32px; padding: 0 12px; border-radius: 6px; cursor: pointer;
200
- background: var(--panel); border: 1px solid var(--line-strong); color: var(--ink); font-size: 14px;
260
+ .ai-tabs button { display: block; }
261
+ .flow-tabs button:hover, .ai-tabs button:hover { background: var(--paper-2); }
262
+ .flow-tabs button[aria-selected="true"], .ai-tabs button[aria-selected="true"] { background: var(--panel); border-color: var(--line); box-shadow: var(--shadow-clay); }
263
+ .flow-tabs b {
264
+ flex: none; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px;
265
+ background: var(--paper-2); color: var(--muted); font-size: 15px;
201
266
  }
202
- .playground-bar .spacer { flex: 1; }
203
- button.copy[data-state="copied"] { border-color: var(--ok); color: var(--ok); }
267
+ .flow-tabs button[aria-selected="true"] b { background: var(--brand); color: #fff; }
268
+ .flow-tabs em { font-style: normal; }
269
+ .flow-tabs strong, .ai-tabs strong { display: block; margin-bottom: 4px; color: var(--ink); font-size: 17px; font-weight: 600; letter-spacing: -.02em; }
270
+ .ai-tabs span { display: block; }
271
+ .flow-view figure { margin: 0; }
272
+ .flow-view video { display: block; width: 100%; height: auto; aspect-ratio: 1440 / 600; border-radius: var(--radius); border: 1px solid var(--line); background: var(--paper-2); }
273
+ .flow-view figcaption { margin-top: 12px; }
274
+ .flow-exports { display: flex; align-items: center; padding: 8px 28px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); }
275
+ .flow-exports[hidden] { display: none; }
276
+ .flow-exports .exports { width: 100%; }
277
+ .exports { margin: 0; padding: 0; list-style: none; display: grid; }
278
+ .exports li { display: flex; gap: 14px; align-items: baseline; padding: 14px 0; border-bottom: 1px solid var(--line); }
279
+ .exports li:last-child { border-bottom: 0; }
280
+ .exports b { flex: 0 0 9.5em; font-weight: 550; }
281
+ .exports span { color: var(--muted); font-size: 15px; }
282
+
283
+ /* Direct it with AI: pick a surface, get its setup. */
284
+ .ai { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); gap: 24px 40px; align-items: start; }
285
+ .ai-view p { margin: 0 0 16px; color: var(--muted); }
286
+ .ai-view .shortcut { margin: 0; padding: 18px 22px; background: var(--panel); border: 1px solid var(--line); border-radius: 16px; color: var(--ink); line-height: 2; }
287
+ .ai-view .keys { white-space: nowrap; }
288
+ .ai-view kbd { padding: 2px 8px; border: 1px solid var(--line-strong); border-bottom-width: 2px; border-radius: 6px; background: var(--paper); }
204
289
 
205
- .demo-note { margin: 24px 0 0; }
290
+ .code {
291
+ position: relative; padding: 18px 92px 18px 22px;
292
+ background: var(--panel); border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow-clay);
293
+ }
294
+ .code[hidden] { display: none; }
295
+ .code pre { margin: 0; overflow-x: auto; line-height: 1.7; }
296
+ .code .copy { position: absolute; right: 12px; top: 12px; min-height: 36px; padding: 0 14px; border: 0; background: none; color: var(--brand); font-weight: 600; }
297
+ .code .copy[data-state="copied"] { color: var(--ok); }
206
298
 
207
299
  /* Quick start */
208
- .start { display: flex; flex-wrap: wrap; gap: 32px 40px; }
209
- .start-head { flex: 1 1 280px; }
300
+ .start { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr); gap: 32px 56px; align-items: start; }
210
301
  .start-head h2 { margin-bottom: 8px; }
211
302
  .start-head p { margin: 0 0 8px; color: var(--muted); }
212
- .start-head a { display: inline-flex; align-items: center; min-height: 44px; font-size: 14px; text-decoration: none; }
213
- .start-body { flex: 999 1 480px; min-width: 0; display: flex; flex-direction: column; gap: 16px; }
303
+ .start-head a { display: inline-flex; align-items: center; min-height: 44px; font-size: 15px; font-weight: 500; text-decoration: none; }
304
+ .start-body { min-width: 0; display: flex; flex-direction: column; gap: 14px; }
214
305
  .seg { align-self: flex-start; display: flex; gap: 2px; padding: 2px; background: #dcdce1; border-radius: 10px; }
215
306
  .seg button {
216
307
  min-height: 44px; padding: 0 18px; border: 0; border-radius: 8px; background: transparent;
217
308
  color: var(--ink); font-size: 14px; font-weight: 600; cursor: pointer; transition: background .15s;
218
309
  }
219
- .seg button[aria-pressed="true"] { background: var(--panel); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); }
220
- .code {
221
- position: relative; min-height: 140px; padding: 16px 96px 16px 20px;
222
- background: var(--panel); border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow-card);
223
- }
224
- .code[hidden] { display: none; }
225
- .code pre { margin: 0; overflow-x: auto; line-height: 1.7; }
226
- .code .copy { position: absolute; right: 12px; top: 12px; min-height: 44px; padding: 0 14px; border: 0; background: none; color: var(--accent); font-weight: 600; }
310
+ .seg button[aria-pressed="true"] { background: var(--panel); color: var(--ink); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); }
227
311
 
228
312
  table { width: 100%; border-collapse: collapse; }
229
313
  th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); }
230
314
  th { font-weight: 600; white-space: nowrap; }
231
315
  td { color: var(--muted); }
316
+ td:first-child { font-family: var(--mono); font-size: 14px; color: var(--ink); white-space: nowrap; }
232
317
  .scroller { overflow-x: auto; }
233
318
 
234
319
  details { border-bottom: 1px solid var(--line); padding: 6px 0; }
235
- details summary { display: flex; align-items: center; min-height: 44px; cursor: pointer; font-weight: 600; }
236
- details p { color: var(--muted); margin: 0 0 12px; }
237
- #controls { padding: 0 0 88px; border-top: 0; }
320
+ details summary { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 52px; cursor: pointer; font-weight: 600; list-style: none; }
321
+ details summary::-webkit-details-marker { display: none; }
322
+ details summary::after { content: "+"; flex: none; font: 400 22px/1 var(--mono); color: var(--muted); transition: transform .2s; }
323
+ details[open] summary::after { transform: rotate(45deg); }
324
+ details p { color: var(--muted); margin: 0 0 14px; max-width: 40em; }
325
+ #controls { padding: 0 0 104px; }
238
326
  #controls .scroller { margin: 12px 0; }
239
- #faq h2 { margin-bottom: 16px; }
240
327
 
241
- .closing .wrap { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 24px; }
328
+ /* Read */
329
+ .reads { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; }
330
+ a.card { display: flex; flex-direction: column; color: var(--ink); text-decoration: none; transition: transform .2s, border-color .2s; }
331
+ a.card:hover { transform: translateY(-3px); border-color: var(--line-strong); }
332
+ a.card p { flex: 1; font-size: 15px; }
333
+ a.card .more { margin-top: 20px; color: var(--brand); font-size: 15px; font-weight: 600; }
334
+
335
+ .closing { text-align: center; }
336
+ .closing .wrap { display: flex; flex-direction: column; align-items: center; gap: 24px; }
337
+ .closing img { width: 88px; height: auto; }
242
338
  .closing h2 { margin: 0; }
339
+ .closing .sub { margin: 0; }
243
340
 
244
- footer.site { border-top: 1px solid var(--line); padding: 20px 0 32px; color: var(--muted); font-size: 14px; }
245
- footer.site .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px 24px; }
341
+ footer.site { border-top: 1px solid var(--line); padding: 32px 0 40px; color: var(--muted); font-size: 14px; }
342
+ footer.site .wrap { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px 24px; }
246
343
  footer.site p { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 20px; }
247
344
  footer.site a { display: inline-flex; align-items: center; min-height: 44px; color: var(--muted); text-decoration: none; }
248
345
  footer.site a:hover { color: var(--ink); }
346
+ footer.site p.legal { display: block; }
347
+ footer.site .legal a { display: inline; min-height: 0; text-decoration: underline; text-underline-offset: 2px; }
249
348
 
250
- @media (max-width: 720px), (hover: none) {
251
- .playground-cover .btn, .playground-cover small { display: none; }
252
- .playground-phone { display: block; font-size: 14px; padding: 0 24px; text-align: center; }
253
- .playground-cover { cursor: default; }
349
+ @media (max-width: 960px) {
350
+ .steps, .reads { grid-template-columns: repeat(2, minmax(0, 1fr)); }
351
+ .ai, .why, .start, .flow, .hero-grid { grid-template-columns: minmax(0, 1fr); }
352
+ }
353
+ @media (max-width: 760px) {
354
+ header.site nav { display: none; }
254
355
  }
255
356
  @media (max-width: 720px) {
256
- section { padding: 64px 0; }
257
- .hero { padding: 56px 0 40px; }
258
- .proof { padding-bottom: 64px; }
259
- .cta .btn { width: 100%; }
260
- .code { padding-right: 20px; padding-top: 60px; }
357
+ :root { --header-h: 56px; }
358
+ .stage { height: 170vh; }
359
+ .stage-pin { justify-content: center; padding-top: var(--header-h); }
360
+ .stage-reel { margin-top: 6vh; }
361
+ body { font-size: 16px; }
362
+ .brand img { width: 32px; }
363
+ section { padding: 72px 0; }
364
+ #controls { padding-bottom: 72px; }
365
+ .hero-actions .btn, .hero-actions .cmd { width: 100%; }
366
+ .hero-actions .cmd { justify-content: space-between; }
367
+ .flow-tabs, .ai-tabs { display: grid; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); gap: 4px; }
368
+ .flow-tabs button, .ai-tabs button { width: auto; padding: 10px 6px; flex-direction: column; align-items: center; gap: 6px; text-align: center; }
369
+ .flow-tabs em, .ai-tabs span { display: none; }
370
+ .flow-tabs strong, .ai-tabs strong { margin: 0; font-size: 14px; }
371
+ .flow-tabs b { width: 26px; height: 26px; font-size: 13px; }
372
+ .flow-exports { aspect-ratio: auto; padding: 4px 20px; }
261
373
  }
262
374
  @media (prefers-reduced-motion: reduce) {
375
+ .stage { height: auto; --p: 0 !important; }
376
+ .stage-pin { position: relative; height: auto; padding-bottom: 72px; }
377
+ .stage-reel { transform: none; }
378
+ .stage-cue { display: none; }
263
379
  html { scroll-behavior: auto; }
264
- .btn, a.card, .seg button { transition: none; }
380
+ .btn, a.card, .seg button, details summary::after { transition: none; }
265
381
  }
266
382
  </style>
267
- <script type="module" crossorigin src="/assets/landing-y3EqDgSZ.js"></script>
268
- <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-P2Xu9kJm.js">
269
383
  </head>
270
384
  <body>
271
385
  <header class="site">
272
386
  <div class="wrap">
273
- <a class="brand" href="/"><img src="/media/cozyclay-logo.png" width="880" height="714" alt="" />CozyClay</a>
274
- <nav>
387
+ <a class="brand" href="/"><img src="/media/cozyclay-mark.png" width="144" height="98" alt="" />CozyClay</a>
388
+ <nav aria-label="Sections">
275
389
  <a href="#how">How it works</a>
276
- <a href="#try">Demo</a>
390
+ <a href="#ai">AI agents</a>
277
391
  <a href="#start">Quick start</a>
278
- <a href="https://github.com/NomaDamas/CozyClay">GitHub</a>
279
- <a class="btn primary" href="#try">Try it</a>
280
392
  </nav>
393
+ <div class="actions">
394
+ <a class="icon-link" href="https://github.com/NomaDamas/CozyClay" aria-label="CozyClay on GitHub">
395
+ <svg width="22" height="22" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
396
+ </a>
397
+ <a class="btn primary sm" href="/app/">Try it</a>
398
+ </div>
281
399
  </div>
282
400
  </header>
283
401
 
284
402
  <main>
285
- <section class="hero">
286
- <div class="wrap">
287
- <h1>Block the shot before you <em>pay to render it</em></h1>
288
- <p class="lede">Free, open source previs in your browser.</p>
289
- <div class="cta">
290
- <a class="btn primary" href="#try">Try it in your browser</a>
291
- <a class="btn ghost" href="https://github.com/NomaDamas/CozyClay">View on GitHub</a>
403
+ <section class="stage" id="stage">
404
+ <div class="stage-pin">
405
+ <h1 class="curved" aria-label="Previs the shot"><span class="line">Previs the shot</span></h1>
406
+ <div class="stage-reel">
407
+ <div class="window">
408
+ <div class="compare" id="compare" style="--pos: 50%">
409
+ <video class="cmp-a" src="/media/truck-greybox.mp4" poster="/media/truck-greybox-poster.jpg" width="1920" height="1080" autoplay muted loop playsinline preload="metadata"></video>
410
+ <video class="cmp-b" src="/media/truck-seedance.mp4" poster="/media/truck-seedance-poster.jpg" width="1920" height="1080" autoplay muted loop playsinline preload="metadata" aria-hidden="true"></video>
411
+ <div class="cmp-line" aria-hidden="true"><span class="cmp-knob"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l-6 6 6 6M15 6l6 6-6 6"/></svg></span></div>
412
+ <input class="cmp-range" type="range" min="0" max="100" step="1" value="50" aria-label="Compare the CozyClay greybox with the Seedance 2.5 take" />
413
+ <span class="tag-l" aria-hidden="true">CozyClay</span>
414
+ <span class="tag-r" aria-hidden="true">Seedance 2.5</span>
415
+ </div>
416
+ </div>
292
417
  </div>
293
- <span class="cmd">
294
- <code>npx cozyclay --via site</code>
295
- <button type="button" class="copy" data-copy="npx cozyclay --via site" aria-label="Copy command">
296
- <svg class="ic-copy" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15V6a2 2 0 0 1 2-2h9"></path></svg>
297
- <svg class="ic-done" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12l5 5 9-10"></path></svg>
298
- </button>
299
- </span>
418
+ <span class="stage-cue" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg></span>
300
419
  </div>
301
420
  </section>
302
421
 
303
- <section class="proof">
304
- <div class="wrap">
305
- <figure>
306
- <div class="window">
307
- <div class="titlebar" aria-hidden="true"><i></i><i></i><i></i><span>CozyClay</span></div>
308
- <video
309
- src="/media/cozyclay-demo.mp4"
310
- poster="/media/cozyclay-demo-poster.jpg"
311
- width="1920" height="1080"
312
- autoplay muted loop playsinline preload="metadata"
313
- ></video>
314
- </div>
315
- <figcaption>Greybox left, generated right. Same camera, every cut.</figcaption>
316
- </figure>
422
+ <section id="why" class="alt">
423
+ <div class="wrap why">
424
+ <div>
425
+ <span class="kicker">Why</span>
426
+ <h2>A text prompt cannot hold a whole scene</h2>
427
+ </div>
428
+ <div>
429
+ <p>AI video costs money per take, and one miss means paying again. A prompt cannot say where everyone stands, how they move, where the camera is and when it cuts.</p>
430
+ <p><strong>Make those decisions first, in 3D, where they are cheap to change.</strong> The video model then gets the shot as a first frame, a greybox reference clip and a prompt that describes the real framing. It follows the blocking instead of guessing.</p>
431
+ </div>
317
432
  </div>
318
433
  </section>
319
434
 
320
435
  <section id="how">
321
436
  <div class="wrap">
322
- <h2>How it works</h2>
323
- <div class="cards">
324
- <div class="card">
325
- <span class="n">1</span>
326
- <h3>Block the scene</h3>
327
- <p>Set pieces and characters.</p>
437
+ <span class="kicker">How it works</span>
438
+ <h2>Stage, move, shoot, export</h2>
439
+ <p class="sub">One local workspace that handles like a 3D editor, from the empty deck to the files a video model reads.</p>
440
+ <div class="flow">
441
+ <div class="flow-tabs" role="tablist" aria-label="Workflow steps">
442
+ <button type="button" role="tab" id="flow-t1" aria-controls="flow-p1" aria-selected="true">
443
+ <b>1</b><span><strong>Stage</strong><em>Build the set from primitives. Add rigged characters, or capsule stand-ins.</em></span>
444
+ </button>
445
+ <button type="button" role="tab" id="flow-t2" aria-controls="flow-p2" aria-selected="false" tabindex="-1">
446
+ <b>2</b><span><strong>Move</strong><em>Describe the action. The compiler bakes every frame; QA checks collisions, foot slide and reach.</em></span>
447
+ </button>
448
+ <button type="button" role="tab" id="flow-t3" aria-controls="flow-p3" aria-selected="false" tabindex="-1">
449
+ <b>3</b><span><strong>Shoot</strong><em>Shots on a timeline, dolly rails and cranes, a look through the lens.</em></span>
450
+ </button>
451
+ <button type="button" role="tab" id="flow-t4" aria-controls="flow-p4" aria-selected="false" tabindex="-1">
452
+ <b>4</b><span><strong>Export</strong><em>Everything the video model needs, in one pack. Every edit is one undo step.</em></span>
453
+ </button>
328
454
  </div>
329
- <div class="card">
330
- <span class="n">2</span>
331
- <h3>Set the camera</h3>
332
- <p>Moves and cuts on a timeline.</p>
333
- </div>
334
- <div class="card">
335
- <span class="n">3</span>
336
- <h3>Hand off the shot</h3>
337
- <p>Export the move, not a prompt.</p>
455
+ <div class="flow-view">
456
+ <figure role="tabpanel" id="flow-p1" aria-labelledby="flow-t1">
457
+ <video src="/media/step-stage.mp4" poster="/media/step-stage-poster.jpg" width="1440" height="600" muted loop playsinline preload="none" aria-label="A whole street built from cubes and cylinders in CozyClay."></video>
458
+ <figcaption>A whole street from cubes and cylinders, railings and lamps included.</figcaption>
459
+ </figure>
460
+ <figure role="tabpanel" id="flow-p2" aria-labelledby="flow-t2" hidden>
461
+ <video src="/media/step-move.mp4" poster="/media/step-move-poster.jpg" width="1440" height="600" muted loop playsinline preload="none" aria-label="A request to the agent, then the clay character walks to a ball and pushes it with both hands."></video>
462
+ <figcaption>Ask the agent; the character walks to the ball and pushes it with both hands.</figcaption>
463
+ </figure>
464
+ <figure role="tabpanel" id="flow-p3" aria-labelledby="flow-t3" hidden>
465
+ <video src="/media/step-shoot.mp4" poster="/media/step-shoot-poster.jpg" width="1440" height="600" muted loop playsinline preload="none" aria-label="The shot camera riding a four-point rail while the crane rises, with its live preview."></video>
466
+ <figcaption>The camera rides a rail while the crane takes it up, previewed live.</figcaption>
467
+ </figure>
468
+ <div role="tabpanel" id="flow-p4" aria-labelledby="flow-t4" class="flow-exports" hidden>
469
+ <ul class="exports" aria-label="Export formats">
470
+ <li><b>Keyframe pack</b><span>First and last frame, greybox clip, camera, prompt</span></li>
471
+ <li><b>Video</b><span>The shot as an mp4</span></li>
472
+ <li><b>Passes</b><span>Depth and normal</span></li>
473
+ <li><b>Contact sheet</b><span>The shots side by side</span></li>
474
+ <li><b>OTIO cut list</b><span>The edit, for your editing software</span></li>
475
+ <li><b>Storyboard</b><span>Panels with captions and an animatic</span></li>
476
+ </ul>
477
+ </div>
338
478
  </div>
339
479
  </div>
340
480
  </div>
341
481
  </section>
342
482
 
343
- <section id="try" class="alt">
483
+ <section id="ai">
344
484
  <div class="wrap">
345
- <h2>Try it</h2>
346
- <div class="demo">
347
- <div class="window">
348
- <div class="titlebar" aria-hidden="true"><i></i><i></i><i></i><span>CozyClay</span></div>
349
- <div class="playground" id="playground" style="background-image: url('/media/playground-poster.jpg')">
350
- <button class="playground-cover" id="playground-start" type="button" aria-label="Start the playground">
351
- <span class="btn primary">Open the studio here</span>
352
- <small>About 2 MB</small>
353
- <span class="playground-phone">Needs a mouse and keyboard. Come back on a computer, or run <code>npx cozyclay --via site</code>.</span>
354
- </button>
355
- <div class="playground-loading">Loading the studio…</div>
356
- </div>
357
- <div class="playground-bar">
358
- <span class="spacer"></span>
359
- <button type="button" id="playground-full">Fullscreen</button>
360
- <a href="/app/" target="_blank" rel="noopener">Open studio ↗</a>
361
- <button type="button" id="playground-close">Close</button>
362
- </div>
363
- </div>
364
- </div>
365
- <p class="small demo-note">Needs a mouse and keyboard.</p>
366
- </div>
367
- </section>
368
-
369
- <section id="what">
370
- <div class="wrap">
371
- <h2>What you can do</h2>
372
- <div class="cards">
373
- <div class="card">
374
- <h3>Stage a scene</h3>
375
- <p>Primitives, imported models, plan view.</p>
376
- </div>
377
- <div class="card">
378
- <h3>Move through the set</h3>
379
- <p>Fly, walk, orbit, frame.</p>
380
- </div>
381
- <div class="card">
382
- <h3>Author the camera</h3>
383
- <p>Lens, dolly, crane, cuts.</p>
485
+ <span class="kicker">Direct it with AI</span>
486
+ <h2>Your agent builds the scene while you watch</h2>
487
+ <p class="sub">The same scene can be driven from three places, live in the viewport. Bring ChatGPT, Anthropic, OpenAI, Gemini, MiniMax, OpenRouter or CLIProxyAPI.</p>
488
+ <div class="ai">
489
+ <div class="ai-tabs" role="tablist" aria-label="Ways to drive CozyClay">
490
+ <button type="button" role="tab" id="ai-t1" aria-controls="ai-p1" aria-selected="true"><strong>Agent panel</strong><span>In the studio</span></button>
491
+ <button type="button" role="tab" id="ai-t2" aria-controls="ai-p2" aria-selected="false" tabindex="-1"><strong>MCP server</strong><span>Claude, Cursor, any client</span></button>
492
+ <button type="button" role="tab" id="ai-t3" aria-controls="ai-p3" aria-selected="false" tabindex="-1"><strong>Terminal</strong><span>Coding agents</span></button>
384
493
  </div>
385
- <div class="card">
386
- <h3>Generate motion</h3>
387
- <p>Poses, Prompt Blocks, IK fixes.</p>
494
+ <div class="ai-view">
495
+ <div role="tabpanel" id="ai-p1" aria-labelledby="ai-t1">
496
+ <p>Chat beside the viewport and watch each change land. Sign in with ChatGPT or use your own key. Every change the agent makes is one undo step.</p>
497
+ <p class="shortcut">Open the studio, then press <span class="keys"><kbd>Cmd/Ctrl</kbd> + <kbd>B</kbd></span></p>
498
+ </div>
499
+ <div role="tabpanel" id="ai-p2" aria-labelledby="ai-t2" hidden>
500
+ <p>Add CozyClay to Claude Desktop, Claude Code, Cursor or any MCP client. The agent edits the scene in the studio you have open.</p>
501
+ <div class="code">
502
+ <pre><code>{
503
+ "mcpServers": {
504
+ "cozyclay": {
505
+ "command": "npx",
506
+ "args": ["-y", "cozyclay", "mcp"]
507
+ }
508
+ }
509
+ }</code></pre>
510
+ <button type="button" class="copy" data-copy='{ "mcpServers": { "cozyclay": { "command": "npx", "args": ["-y", "cozyclay", "mcp"] } } }'>Copy</button>
511
+ </div>
512
+ </div>
513
+ <div role="tabpanel" id="ai-p3" aria-labelledby="ai-t3" hidden>
514
+ <p>Coding agents drive a running studio from the shell, JSON in and out.</p>
515
+ <div class="code">
516
+ <pre><code>npx cozyclay
517
+ cclay live status --wait</code></pre>
518
+ <button type="button" class="copy" data-copy="npx cozyclay&#10;cclay live status --wait">Copy</button>
519
+ </div>
520
+ </div>
388
521
  </div>
389
522
  </div>
390
523
  </div>
@@ -393,9 +526,10 @@
393
526
  <section id="start" class="alt">
394
527
  <div class="wrap start">
395
528
  <div class="start-head">
396
- <h2>Quick start</h2>
397
- <p>Node.js 22.19+ and a Chromium browser.</p>
398
- <a href="#controls" id="controls-link">Full control list</a>
529
+ <span class="kicker">Quick start</span>
530
+ <h2>One command</h2>
531
+ <p>Node.js 22.19+ and a Chromium browser. The studio opens at <code>127.0.0.1:5180/app/</code>.</p>
532
+ <a href="#controls" id="controls-link">Full control list →</a>
399
533
  </div>
400
534
  <div class="start-body">
401
535
  <div class="seg" id="start-tabs">
@@ -413,6 +547,7 @@ npm install
413
547
  npm run dev</code></pre>
414
548
  <button type="button" class="copy" data-copy="git clone https://github.com/NomaDamas/CozyClay.git&#10;cd CozyClay&#10;npm install&#10;npm run dev">Copy</button>
415
549
  </div>
550
+ <p class="small">Asking an AI agent to set it up? Give it the repository link; its instructions are in <a href="https://github.com/NomaDamas/CozyClay/blob/main/INSTALL_FOR_AGENTS.md">INSTALL_FOR_AGENTS.md</a>.</p>
416
551
  </div>
417
552
  </div>
418
553
  </section>
@@ -444,48 +579,41 @@ npm run dev</code></pre>
444
579
 
445
580
  <section id="read">
446
581
  <div class="wrap">
582
+ <span class="kicker">Guides</span>
447
583
  <h2>Read</h2>
448
- <div class="cards">
449
- <a class="card" href="/greybox-to-video/">Greybox to AI video</a>
450
- <a class="card" href="/seedance-camera-control/">Seedance 2.5 camera control</a>
451
- <a class="card" href="/previs-software/">Previs software compared</a>
452
- <a class="card" href="/ai-camera-control/">Camera control for AI video</a>
584
+ <div class="reads">
585
+ <a class="card" href="/greybox-to-video/">
586
+ <h3>Greybox to AI video</h3>
587
+ <p>Why a grey blockout is the most reliable input for a video model, and the block-then-generate loop.</p>
588
+ <span class="more">Read →</span>
589
+ </a>
590
+ <a class="card" href="/seedance-camera-control/">
591
+ <h3>Seedance 2.5 camera control</h3>
592
+ <p>Getting the same camera on every take with a reference clip.</p>
593
+ <span class="more">Read →</span>
594
+ </a>
595
+ <a class="card" href="/previs-software/">
596
+ <h3>Previs software compared</h3>
597
+ <p>Free, open source and browser-based previs tools side by side.</p>
598
+ <span class="more">Read →</span>
599
+ </a>
600
+ <a class="card" href="/ai-camera-control/">
601
+ <h3>Camera control for AI video</h3>
602
+ <p>Why camera prompts drift and what the models actually accept.</p>
603
+ <span class="more">Read →</span>
604
+ </a>
453
605
  </div>
454
606
  </div>
455
607
  </section>
456
608
 
457
- <section id="faq">
458
- <div class="wrap narrow">
459
- <h2>FAQ</h2>
460
- <details>
461
- <summary>What is CozyClay?</summary>
462
- <p>Open source previs software in a browser: block a scene, pose characters, author camera moves and cuts, preview motion.</p>
463
- </details>
464
- <details>
465
- <summary>Previs or previz?</summary>
466
- <p>Same thing. Previsualization: the rough 3D pass where a shot gets blocked before anyone pays for final assets or renders.</p>
467
- </details>
468
- <details>
469
- <summary>Can I use it for camera control in AI video?</summary>
470
- <p>That is why it exists. Block the shot in 3D, then hand the camera move to the video model instead of describing it in a prompt. See <a href="/ai-camera-control/">camera control for AI video</a>.</p>
471
- </details>
472
- <details>
473
- <summary>Do I need Blender or Unity?</summary>
474
- <p>No. It runs in a Chromium browser. Controls follow Unity conventions, nothing to install.</p>
475
- </details>
476
- <details>
477
- <summary>Is it free? Does it phone home?</summary>
478
- <p>Free, AGPL-3.0, runs on your machine and works offline. Project content is never uploaded. The npm package sends anonymous usage metrics; <code>cclay telemetry off</code> disables them.</p>
479
- </details>
480
- </div>
481
- </section>
482
-
483
- <section class="closing alt">
609
+ <section class="closing">
484
610
  <div class="wrap">
611
+ <img src="/media/cozyclay-mark.png" width="144" height="98" alt="" loading="lazy" />
485
612
  <h2>Block your first shot</h2>
613
+ <p class="sub">Free, open source, and running in your browser in under a minute.</p>
486
614
  <div class="cta">
487
- <a class="btn primary" href="#try">Try it in your browser</a>
488
- <a class="btn ghost" href="https://github.com/NomaDamas/CozyClay">View on GitHub</a>
615
+ <a class="btn primary" href="/app/">Try it in your browser</a>
616
+ <a class="btn ghost" href="https://discord.gg/Hp9V5wwgjF">Join the Discord</a>
489
617
  </div>
490
618
  </div>
491
619
  </section>
@@ -505,26 +633,167 @@ npm run dev</code></pre>
505
633
  document.getElementById("controls-link").addEventListener("click", () => {
506
634
  document.querySelector("#controls details").open = true;
507
635
  });
508
- // A started playground takes the screen: steps, window and its bar in view.
509
- document.getElementById("playground-start").addEventListener("click", () => {
510
- if (!matchMedia("(min-width: 721px) and (hover: hover)").matches) return;
511
- const still = matchMedia("(prefers-reduced-motion: reduce)").matches;
512
- document.querySelector("#try .demo").scrollIntoView({ block: "start", behavior: still ? "auto" : "smooth" });
636
+ document.querySelectorAll("button.copy[data-copy]").forEach((button) => {
637
+ button.addEventListener("click", async () => {
638
+ try { await navigator.clipboard.writeText(button.dataset.copy); button.dataset.state = "copied"; setTimeout(() => { button.dataset.state = ""; }, 1400); } catch {}
639
+ });
640
+ });
641
+ // Hero compare: drag (or arrow keys on the hidden range) moves the divider;
642
+ // the generated take follows the greybox clock so both show the same frame.
643
+ const compare = document.getElementById("compare");
644
+ const greybox = compare.querySelector(".cmp-a");
645
+ const take = compare.querySelector(".cmp-b");
646
+ const range = compare.querySelector(".cmp-range");
647
+ const setPos = (pct) => {
648
+ const p = Math.min(100, Math.max(0, pct));
649
+ compare.style.setProperty("--pos", p + "%");
650
+ range.value = String(Math.round(p));
651
+ };
652
+ const fromPointer = (event) => {
653
+ const box = compare.getBoundingClientRect();
654
+ setPos(((event.clientX - box.left) / box.width) * 100);
655
+ };
656
+ compare.addEventListener("pointerdown", (event) => {
657
+ compare.setPointerCapture(event.pointerId);
658
+ fromPointer(event);
659
+ });
660
+ compare.addEventListener("pointermove", (event) => {
661
+ if (compare.hasPointerCapture(event.pointerId)) fromPointer(event);
513
662
  });
663
+ range.addEventListener("input", () => setPos(Number(range.value)));
664
+ // The greybox is the clock. Every frame the take is checked against it and
665
+ // re-seeked when it drifts more than about one frame; both files have a
666
+ // keyframe every second, so the seek lands at once. A short cooldown lets
667
+ // a seek finish before the next check. The take holds while the greybox
668
+ // buffers.
669
+ let settleUntil = 0;
670
+ const syncTake = () => {
671
+ if (greybox.paused !== take.paused) (greybox.paused ? take.pause() : take.play().catch(() => {}));
672
+ if (take.seeking || performance.now() < settleUntil) return;
673
+ if (Math.abs(take.currentTime - greybox.currentTime) > 0.045) {
674
+ take.currentTime = greybox.currentTime;
675
+ settleUntil = performance.now() + 400;
676
+ }
677
+ };
678
+ const tick = () => {
679
+ if (!greybox.paused) syncTake();
680
+ if (greybox.requestVideoFrameCallback) greybox.requestVideoFrameCallback(tick);
681
+ else requestAnimationFrame(tick);
682
+ };
683
+ tick();
684
+ greybox.addEventListener("waiting", () => take.pause());
685
+ // Tab lists (workflow steps, agent surfaces): click or arrow keys pick a panel.
686
+ document.querySelectorAll("[role=tablist]").forEach((list) => {
687
+ const tabs = [...list.querySelectorAll("[role=tab]")];
688
+ const select = (tab) => tabs.forEach((other) => {
689
+ const on = other === tab;
690
+ other.setAttribute("aria-selected", String(on));
691
+ other.tabIndex = on ? 0 : -1;
692
+ document.getElementById(other.getAttribute("aria-controls")).hidden = !on;
693
+ });
694
+ tabs.forEach((tab, i) => {
695
+ tab.addEventListener("click", () => select(tab));
696
+ tab.addEventListener("keydown", (event) => {
697
+ const move = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
698
+ if (!move) return;
699
+ event.preventDefault();
700
+ const next = tabs[(i + move + tabs.length) % tabs.length];
701
+ select(next);
702
+ next.focus();
703
+ });
704
+ });
705
+ });
706
+ // How it works: the open step's clip plays while the section is on screen.
707
+ const flowView = document.querySelector(".flow-view");
708
+ let flowSeen = false;
709
+ const playOpenStep = () => flowView.querySelectorAll("video").forEach((clip) => {
710
+ if (flowSeen && !clip.closest("[role=tabpanel]").hidden) clip.play().catch(() => {});
711
+ else clip.pause();
712
+ });
713
+ new IntersectionObserver(([entry]) => { flowSeen = entry.isIntersecting; playOpenStep(); }).observe(flowView);
714
+ document.querySelectorAll(".flow-tabs [role=tab]").forEach((tab) => tab.addEventListener("click", playOpenStep));
715
+ document.querySelector(".flow-tabs").addEventListener("keydown", () => setTimeout(playOpenStep));
716
+ // Hero headline: the letters sit on the front of a cylinder, so the
717
+ // middle bulges toward the viewer and the ends fall away.
718
+ const headline = document.querySelector("h1.curved");
719
+ headline.querySelectorAll(".line").forEach((line) => {
720
+ const text = line.textContent;
721
+ line.textContent = "";
722
+ line.setAttribute("aria-hidden", "true");
723
+ for (const char of text) {
724
+ const span = document.createElement("span");
725
+ span.className = "ch";
726
+ span.textContent = char;
727
+ line.appendChild(span);
728
+ }
729
+ });
730
+ const bend = () => {
731
+ const chars = [...headline.querySelectorAll(".ch")];
732
+ chars.forEach((c) => { c.style.transform = ""; });
733
+ const box = headline.getBoundingClientRect();
734
+ const centre = box.left + box.width / 2;
735
+ const half = Math.max(...[...headline.querySelectorAll(".line")].map((l) => l.getBoundingClientRect().width)) / 2;
736
+ chars.forEach((c) => {
737
+ const r = c.getBoundingClientRect();
738
+ const u = Math.max(-1, Math.min(1, (r.left + r.width / 2 - centre) / half));
739
+ const depth = Math.sqrt(1 - 0.75 * u * u);
740
+ c.style.transform = "scale(" + (0.84 + 0.16 * depth).toFixed(3) + ")";
741
+ });
742
+ };
743
+ bend();
744
+ document.fonts.ready.then(bend);
745
+ addEventListener("resize", bend);
746
+ // First screen: map the scroll through the stage to --p, and size the
747
+ // reel's rise and growth so it ends centred at the full width.
748
+ const stage = document.getElementById("stage");
749
+ const reel = stage.querySelector(".stage-reel");
750
+ const header = document.querySelector("header.site");
751
+ const still = matchMedia("(prefers-reduced-motion: reduce)");
752
+ const measure = () => {
753
+ const pin = stage.querySelector(".stage-pin");
754
+ const start = 0.94;
755
+ const width = reel.offsetWidth;
756
+ const full = Math.min(innerWidth * 0.98, (innerHeight - header.offsetHeight - 24) * 16 / 9 + 0);
757
+ const end = Math.max(start, full / width);
758
+ stage.style.setProperty("--grow", (end - start).toFixed(3));
759
+ const top = reel.offsetTop;
760
+ const target = header.offsetHeight + Math.max(12, (innerHeight - header.offsetHeight - reel.offsetHeight * end) / 2);
761
+ stage.style.setProperty("--rise", (target - top) + "px");
762
+ void pin;
763
+ };
764
+ let queued = false;
765
+ const onScroll = () => {
766
+ queued = false;
767
+ const box = stage.getBoundingClientRect();
768
+ const span = stage.offsetHeight - innerHeight;
769
+ const p = still.matches || span <= 0 ? 0 : Math.min(1, Math.max(0, -box.top / span));
770
+ stage.style.setProperty("--p", p.toFixed(4));
771
+ document.body.classList.toggle("on-stage", box.bottom > header.offsetHeight);
772
+ };
773
+ const queue = () => { if (!queued) { queued = true; requestAnimationFrame(onScroll); } };
774
+ measure();
775
+ onScroll();
776
+ addEventListener("scroll", queue, { passive: true });
777
+ addEventListener("resize", () => { measure(); queue(); });
778
+ greybox.addEventListener("seeked", syncTake);
779
+ greybox.addEventListener("play", syncTake);
780
+ greybox.addEventListener("pause", syncTake);
514
781
  })();
515
782
  </script>
516
783
 
517
784
  <footer class="site">
518
785
  <div class="wrap">
519
786
  <p>
520
- <a href="https://github.com/NomaDamas/CozyClay">Source (AGPL)</a>
787
+ <a href="https://github.com/NomaDamas/CozyClay">GitHub (AGPL)</a>
521
788
  <a href="https://www.npmjs.com/package/cozyclay">npm</a>
789
+ <a href="https://github.com/NomaDamas/CozyClay/blob/main/CHANGELOG.md">Changelog</a>
790
+ <a href="https://discord.gg/Hp9V5wwgjF">Discord</a>
522
791
  <a href="/privacy/">Privacy</a>
523
792
  </p>
524
- <p>
525
- CozyClay is not affiliated with or endorsed by NVIDIA or Unity Technologies.
793
+ <p class="legal">
794
+ Made by <a href="https://github.com/NomaDamas">NomaDamas</a>. Not affiliated with or endorsed by NVIDIA or Unity Technologies.
526
795
  </p>
527
796
  </div>
528
797
  </footer>
529
798
  </body>
530
- </html>
799
+ </html>