function pinPageHtml() { return '' + '' + '' + '' + '' + '' + 'Clay Studio' + '
' + '

' + '
Enter your PIN to continue
' + pinBoxesHtml + '
' + '
'; } function setupPageHtml(httpsUrl, httpUrl, hasCert, lanMode) { return ` Setup - Clay Studio

Clay

Setup your device for the best experience

Step 1 of 4
Connect via Tailscale
Tailscale creates a private VPN so you can access Clay from anywhere. It needs to be installed on both the server (the machine running Clay) and this device.
1
Server: Install Tailscale on the machine running Clay.
If you are viewing this page, the server likely already has Tailscale. You can verify by checking its 100.x.x.x IP.
2
This device: Install Tailscale here and sign in with the same account.
3
Once both devices are on Tailscale, open the relay using the server's Tailscale IP.
Checking connection...
Step 1 of 3
Install certificate
Encrypt all traffic between this device and the relay. The certificate is generated locally and does not grant any additional access.
1
Download the certificate.
Download Certificate
2
Open Settings and tap the Profile Downloaded banner to install.
If the banner is gone: Settings > General > VPN & Device Management
3
Go to Settings > General > About > Certificate Trust Settings and enable full trust.
2
Open the downloaded file, or go to Settings > Security > Install a certificate > CA certificate.
Path may vary by device. Search "certificate" in Settings if needed.
2
The certificate should be trusted automatically via mkcert. If your browser still shows a warning, run mkcert -install on the host machine.
Checking HTTPS connection...
Step 2 of 3
Add to Home Screen
Install Clay as an app for quick access and a full-screen experience.
On iOS, push notifications only work from the installed app. This step is required.
1
Tap the Share button at the bottom of the Safari toolbar.
2
Scroll down in the share sheet and tap Add to Home Screen
3
Tap Add in the top right corner to confirm.
1
Tap the three dots menu in the top right corner of Chrome.
2
Tap Install app or Add to Home screen.
If you don't see it, try Open in Chrome first if using another browser.
3
Tap Install in the confirmation dialog.
1
Look for the install icon in the address bar (a monitor with a down arrow).
2
Click it and then click Install to confirm.
If there is no icon, go to Menu > Install Clay or Menu > Save and Share > Install.
After installing, open Clay from your home screen to continue setup.
Step 3 of 3
Enable notifications
Get alerted on your phone when Claude finishes a response, even when the app is in the background.
✓
All set!
Your device is configured. You can change these settings anytime from the app.
Open Clay
`; } function escapeHtml(s) { return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } // --- Build auth page CSS variables from the Clay Studio theme pair --- var path = require("path"); var _authTheme = require(path.join(__dirname, "themes", "clay-light.json")); var _authDarkTheme = require(path.join(__dirname, "themes", "clay-dark.json")); function _hexToRgb(hex) { hex = hex.replace("#", ""); return { r: parseInt(hex.substring(0, 2), 16), g: parseInt(hex.substring(2, 4), 16), b: parseInt(hex.substring(4, 6), 16) }; } function _hexToRgba(hex, alpha) { var c = _hexToRgb(hex); return "rgba(" + c.r + "," + c.g + "," + c.b + "," + alpha + ")"; } function _mixColors(hex1, hex2, weight) { var c1 = _hexToRgb(hex1), c2 = _hexToRgb(hex2); var w = weight; var r = Math.round(c1.r * w + c2.r * (1 - w)); var g = Math.round(c1.g * w + c2.g * (1 - w)); var b = Math.round(c1.b * w + c2.b * (1 - w)); return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } var _keys = ["base00","base01","base02","base03","base04","base05","base06","base07", "base08","base09","base0A","base0B","base0C","base0D","base0E","base0F"]; function _buildAuthVarDeclarations(theme) { var t = {}; for (var ki = 0; ki < _keys.length; ki++) { t[_keys[ki]] = "#" + theme[_keys[ki]]; } var vars = { "--bg": t.base00, "--bg-alt": t.base01, "--text": t.base06, "--text-muted": t.base04, "--text-dimmer": t.base03, "--accent": t.base09, "--accent-15": _hexToRgba(t.base09, 0.15), "--accent-20": _hexToRgba(t.base09, 0.20), "--border": t.base02, "--input-bg": _mixColors(t.base01, t.base02, 0.5), "--error": t.base08, }; var declarations = ""; var names = Object.keys(vars); for (var vi = 0; vi < names.length; vi++) { declarations += names[vi] + ":" + vars[names[vi]] + ";"; } return declarations; } var _authVarsStr = ":root{" + _buildAuthVarDeclarations(_authTheme) + "}" + "@media(prefers-color-scheme:dark){:root{" + _buildAuthVarDeclarations(_authDarkTheme) + "}}"; // --- Shared CSS for auth pages --- var authPageStyles = _authVarsStr + // Reset & layout '*{margin:0;padding:0;box-sizing:border-box}' + 'body{background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,sans-serif;' + 'min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:20px}' + '.c{max-width:380px;width:100%;text-align:center}' + 'h1{color:var(--text);font-size:24px;font-weight:700;margin-bottom:6px}' + '.sub{color:var(--text-muted);font-size:14px;margin-bottom:28px}' + '.field{margin-bottom:16px;text-align:left}' + '.field label{display:block;font-size:12px;color:var(--text-muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px}' + 'input{width:100%;background:var(--input-bg);border:1px solid var(--border);border-radius:12px;' + 'color:var(--text);font-size:16px;padding:12px 14px;outline:none;font-family:inherit}' + 'input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-15)}' + 'input::placeholder{font-size:14px;color:var(--text-dimmer)}' + // PIN digit boxes '.pin-wrap{display:flex;gap:8px;justify-content:center}' + '.pin-digit{width:44px;height:56px;background:var(--input-bg);border:1.5px solid var(--border);border-radius:8px;' + 'color:var(--text);font-family:"Courier New",Courier,"Roboto Mono",monospace;font-size:28px;font-weight:700;' + 'text-align:center;line-height:56px;outline:none;caret-color:transparent;' + 'transition:border-color 0.15s,box-shadow 0.15s}' + '.pin-digit:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-20)}' + '.pin-digit.filled{color:var(--text)}' + // Legacy single-input fallback '.pin-input{font-size:24px;letter-spacing:12px;text-align:center;-webkit-text-security:disc;' + 'font-family:"Courier New",Courier,"Roboto Mono",monospace}' + '.btn{width:100%;background:var(--accent);color:#fff;border:none;border-radius:12px;' + 'padding:14px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit;' + 'transition:opacity 0.15s;margin-top:8px}' + '.btn:hover{opacity:0.9}' + '.btn:disabled{opacity:0.4;cursor:default}' + '.err{color:var(--error);font-size:13px;margin-top:12px;min-height:1.3em}' + '.info{color:var(--text-dimmer);font-size:12px;margin-top:16px}' + // Step wizard '.step{display:none}.step.active{display:block}' + '.steps-bar{display:flex;gap:6px;justify-content:center;margin-bottom:28px}' + '.steps-dot{width:8px;height:8px;border-radius:50%;background:var(--border);transition:background 0.2s}' + '.steps-dot.done{background:var(--accent)}.steps-dot.current{background:var(--accent)}'; // --- Shared JS for PIN digit boxes --- // initPinBoxes(containerId, hiddenInputId, onComplete) — wires up 6 individual digit inputs var pinBoxScript = 'function initPinBoxes(cId,hId,onComplete){' + 'var wrap=document.getElementById(cId),hidden=document.getElementById(hId);' + 'var boxes=wrap.querySelectorAll(".pin-digit");' + 'var digits=["","","","","",""];' + 'boxes[0].focus();' + 'function setDigit(idx,v){digits[idx]=v;boxes[idx].value=v?"\\u2022":"";boxes[idx].classList.toggle("filled",v.length>0);syncHidden()}' + 'for(var i=0;i0){setDigit(idx-1,"");boxes[idx-1].focus()}else{setDigit(idx,"")}syncHidden();return}' + 'if(e.key==="ArrowLeft"&&idx>0)boxes[idx-1].focus();' + 'if(e.key==="ArrowRight"&&idx<5)boxes[idx+1].focus();' + 'if(e.key==="Enter"&&hidden.value.length===6&&onComplete){e.preventDefault();onComplete()}});' + 'boxes[idx].addEventListener("paste",function(e){' + 'e.preventDefault();var d=(e.clipboardData||window.clipboardData).getData("text").replace(/[^0-9]/g,"").slice(0,6);' + 'for(var j=0;j=6){boxes[5].focus();if(onComplete)onComplete()}else if(d.length>0)boxes[d.length].focus()});' + 'boxes[idx].addEventListener("focus",function(){this.select()});' + '})(i)}' + 'function syncHidden(){var v="";for(var j=0;j