cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

feat: refresh Portal and goq.sh presentation

Dami committed d4a1ae8
commit
d4a1ae8a8ef773be0572ee2395fdc76ce6100cb4
parent
23cb438492bf630ab8332c9905b082096254fcd8
author
Dami <dami.etoile@gmail.com>
61 files changed, +327 -68
M.gitignore1+
Msrc-tauri/icons/128x128.pngbin
Msrc-tauri/icons/128x128@2x.pngbin
Msrc-tauri/icons/32x32.pngbin
Asrc-tauri/icons/64x64.pngbin
Msrc-tauri/icons/Square107x107Logo.pngbin
Msrc-tauri/icons/Square142x142Logo.pngbin
Msrc-tauri/icons/Square150x150Logo.pngbin
Msrc-tauri/icons/Square284x284Logo.pngbin
Msrc-tauri/icons/Square30x30Logo.pngbin
Msrc-tauri/icons/Square310x310Logo.pngbin
Msrc-tauri/icons/Square44x44Logo.pngbin
Msrc-tauri/icons/Square71x71Logo.pngbin
Msrc-tauri/icons/Square89x89Logo.pngbin
Msrc-tauri/icons/StoreLogo.pngbin
Asrc-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml5+
Asrc-tauri/icons/android/mipmap-hdpi/ic_launcher.pngbin
Asrc-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.pngbin
Asrc-tauri/icons/android/mipmap-hdpi/ic_launcher_round.pngbin
Asrc-tauri/icons/android/mipmap-mdpi/ic_launcher.pngbin
Asrc-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.pngbin
Asrc-tauri/icons/android/mipmap-mdpi/ic_launcher_round.pngbin
Asrc-tauri/icons/android/mipmap-xhdpi/ic_launcher.pngbin
Asrc-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.pngbin
Asrc-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.pngbin
Asrc-tauri/icons/android/mipmap-xxhdpi/ic_launcher.pngbin
Asrc-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.pngbin
Asrc-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.pngbin
Asrc-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.pngbin
Asrc-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.pngbin
Asrc-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.pngbin
Asrc-tauri/icons/android/values/ic_launcher_background.xml4+
Msrc-tauri/icons/icon.icnsbin
Msrc-tauri/icons/icon.icobin
Msrc-tauri/icons/icon.pngbin
Asrc-tauri/icons/ios/AppIcon-20x20@1x.pngbin
Asrc-tauri/icons/ios/AppIcon-20x20@2x-1.pngbin
Asrc-tauri/icons/ios/AppIcon-20x20@2x.pngbin
Asrc-tauri/icons/ios/AppIcon-20x20@3x.pngbin
Asrc-tauri/icons/ios/AppIcon-29x29@1x.pngbin
Asrc-tauri/icons/ios/AppIcon-29x29@2x-1.pngbin
Asrc-tauri/icons/ios/AppIcon-29x29@2x.pngbin
Asrc-tauri/icons/ios/AppIcon-29x29@3x.pngbin
Asrc-tauri/icons/ios/AppIcon-40x40@1x.pngbin
Asrc-tauri/icons/ios/AppIcon-40x40@2x-1.pngbin
Asrc-tauri/icons/ios/AppIcon-40x40@2x.pngbin
Asrc-tauri/icons/ios/AppIcon-40x40@3x.pngbin
Asrc-tauri/icons/ios/AppIcon-512@2x.pngbin
Asrc-tauri/icons/ios/AppIcon-60x60@2x.pngbin
Asrc-tauri/icons/ios/AppIcon-60x60@3x.pngbin
Asrc-tauri/icons/ios/AppIcon-76x76@1x.pngbin
Asrc-tauri/icons/ios/AppIcon-76x76@2x.pngbin
Asrc-tauri/icons/ios/AppIcon-83.5x83.5@2x.pngbin
Msrc-tauri/tauri.conf.json4+-
Msrc/audio-ui.mjs2+-
Msrc/audio-ui.test.mjs8+-
Msrc/index.html31+++-
Msrc/session-controls-layout.test.mjs2+
Msrc/style.css243++++++++++++++++++++++++------
Mwebsite/dither.js93+++++++++++-
Mwebsite/index.html2+-
M .gitignore +1 -0
M src-tauri/icons/128x128.png +0 -0

Binary file.

M src-tauri/icons/128x128@2x.png +0 -0

Binary file.

M src-tauri/icons/32x32.png +0 -0

Binary file.

A src-tauri/icons/64x64.png +0 -0

Binary file.

M src-tauri/icons/Square107x107Logo.png +0 -0

Binary file.

M src-tauri/icons/Square142x142Logo.png +0 -0

Binary file.

M src-tauri/icons/Square150x150Logo.png +0 -0

Binary file.

M src-tauri/icons/Square284x284Logo.png +0 -0

Binary file.

M src-tauri/icons/Square30x30Logo.png +0 -0

Binary file.

M src-tauri/icons/Square310x310Logo.png +0 -0

Binary file.

M src-tauri/icons/Square44x44Logo.png +0 -0

Binary file.

M src-tauri/icons/Square71x71Logo.png +0 -0

Binary file.

M src-tauri/icons/Square89x89Logo.png +0 -0

Binary file.

M src-tauri/icons/StoreLogo.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
A src-tauri/icons/android/mipmap-hdpi/ic_launcher.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-mdpi/ic_launcher.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0

Binary file.

A src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png +0 -0

Binary file.

A src-tauri/icons/android/values/ic_launcher_background.xml +4 -0
M src-tauri/icons/icon.icns +0 -0

Binary file.

M src-tauri/icons/icon.ico +0 -0

Binary file.

M src-tauri/icons/icon.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-20x20@1x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-20x20@2x-1.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-20x20@2x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-20x20@3x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-29x29@1x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-29x29@2x-1.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-29x29@2x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-29x29@3x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-40x40@1x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-40x40@2x-1.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-40x40@2x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-40x40@3x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-512@2x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-60x60@2x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-60x60@3x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-76x76@1x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-76x76@2x.png +0 -0

Binary file.

A src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png +0 -0

Binary file.

M src-tauri/tauri.conf.json +3 -1
M src/audio-ui.mjs +1 -1
M src/audio-ui.test.mjs +4 -4
M src/index.html +24 -7
@@ -3,12 +3,24 @@
33 <head>44 <meta charset="UTF-8" />55 <meta name="viewport" content="width=device-width, initial-scale=1.0" />6- <title>Sigil Spark</title>6+ <title>Portal</title>77 <link rel="stylesheet" href="./style.css" />88 </head>99 <body>1010 <div class="overlay-screen hidden" id="tap-overlay" style="z-index: 200;">1111 <div class="overlay-content">12+ <div class="flow-steps" aria-hidden="true">13+ <span class="flow-step">01 pin</span>14+ <span class="flow-step active">02 tap</span>15+ <span class="flow-step">03 play</span>16+ </div>17+ <svg class="pixel-key" viewBox="0 0 26 12" width="78" height="36" aria-hidden="true">18+ <rect x="0" y="1" width="8" height="10" fill="var(--terracotta)" />19+ <rect x="3" y="5" width="2" height="2" fill="var(--paper)" />20+ <rect x="8" y="5" width="18" height="2" fill="var(--ochre)" />21+ <rect x="14" y="7" width="2" height="3" fill="var(--moss)" />22+ <rect x="19" y="7" width="2" height="2" fill="var(--moss)" />23+ </svg>1224 <h1 id="tap-overlay-title">tap key</h1>1325 <p id="tap-overlay-desc">Touch the sensor on your key when it blinks.</p>1426 <div class="overlay-status pending" id="tap-status">waiting...</div>
@@ -30,7 +42,7 @@
3042 <button class="pin-pad-key" data-controller-focus data-pin-digit="7" onclick="pinPadDigit('7')">7</button>3143 <button class="pin-pad-key" data-controller-focus data-pin-digit="8" onclick="pinPadDigit('8')">8</button>3244 <button class="pin-pad-key" data-controller-focus data-pin-digit="9" onclick="pinPadDigit('9')">9</button>33- <button class="pin-pad-key pin-pad-action" data-controller-focus onclick="pinPadBackspace()" aria-label="Backspace">⌫</button>45+ <button class="pin-pad-key pin-pad-action" data-controller-focus onclick="pinPadBackspace()" aria-label="Backspace">del</button>3446 <button class="pin-pad-key" data-controller-focus data-pin-digit="0" onclick="pinPadDigit('0')">0</button>3547 <button class="pin-pad-key pin-pad-action" data-controller-focus onclick="closePinPad()">done</button>3648 </div>
@@ -40,7 +52,12 @@
4052 4153 <div class="overlay-screen hidden" id="intro">4254 <div class="overlay-content">43- <h1>sigil spark</h1>55+ <div class="flow-steps" aria-hidden="true">56+ <span class="flow-step active">01 pin</span>57+ <span class="flow-step">02 tap</span>58+ <span class="flow-step">03 play</span>59+ </div>60+ <h1>portal</h1>4461 <p id="intro-desc">enter the host security-key pin</p>4562 <div class="dev-warning hidden" id="dev-connect-warning" role="alert">4663 DEVELOPMENT DIRECT NODE<br />PASSKEY LOOKUP BYPASSED · NOT CLIENT AUTHORIZATION
@@ -58,7 +75,7 @@
5875 </div>5976 6077 <div class="topbar">61- <span class="brand">sigil spark</span>78+ <span class="brand">portal</span>6279 <span class="dev-badge hidden" id="dev-connect-badge" title="Skips passkey identity lookup. This is not client authorization.">dev direct-node · not auth</span>6380 <span class="sep">·</span>6481 <div class="status-line" id="status-line">
@@ -88,7 +105,7 @@
88105 <button class="control-toggle" id="control-toggle" data-controller-focus onclick="toggleControl()">89106 view only · input unavailable90107 </button>91- <button class="control-toggle disabled" id="audio-toggle" data-controller-focus onclick="toggleAudioMute()" aria-disabled="true" aria-label="Audio unavailable. Not connected" title="Audio unavailable: not connected">108+ <button type="button" class="control-toggle disabled" id="audio-toggle" data-controller-focus onclick="toggleAudioMute()" aria-disabled="true" aria-label="Audio unavailable. Not connected" title="Audio unavailable: not connected">92109 🔇93110 </button>94111 </div>
@@ -109,7 +126,7 @@
109126 </div>110127 <div class="panel-body">111128 <div class="panel-section">112- <h3># security key</h3>129+ <h3><span class="hash" aria-hidden="true">#</span> security key</h3>113130 <div class="info-row"><span class="info-label">status</span><span class="info-value" id="fido-status">—</span></div>114131 <div class="info-row"><span class="info-label">product</span><span class="info-value" id="fido-product">—</span></div>115132 <div class="info-row"><span class="info-label">vid:pid</span><span class="info-value" id="fido-vidpid">—</span></div>
@@ -119,7 +136,7 @@
119136 <button class="link-btn panel-action" data-controller-focus onclick="scanFido()">rescan</button>120137 </div>121138 <div class="panel-section" id="stream-section">122- <h3># stream</h3>139+ <h3><span class="hash" aria-hidden="true">#</span> stream</h3>123140 <div class="info-row"><span class="info-label">codec</span><span class="info-value" id="stream-codec">—</span></div>124141 <div class="info-row"><span class="info-label">transport</span><span class="info-value" id="stream-transport">—</span></div>125142 <div class="info-row"><span class="info-label">path</span><span class="info-value" id="stream-path">—</span></div>
M src/session-controls-layout.test.mjs +2 -0
M src/style.css +191 -52
@@ -1,14 +1,39 @@
1+/* ─────────────────────────────────────────────────────────────2+ portal webview — goq.sh visual language, dark ramp.3+ The app frames a video stream, so it commits to the landing4+ page's dark palette as a single theme.5+ ───────────────────────────────────────────────────────────── */6+17 :root {2- --background: #1a1a1a;3- --foreground: #c4c4c4;4- --muted: #2a2a2a;5- --muted-fg: #808080;6- --accent: #2a2a2a;7- --border: rgba(255,255,255,0.1);8- --border-strong: rgba(255,255,255,0.15);9- --destructive: #ff6568;10- --green: #4ade80;11- --yellow: #fbbf24;8+ /* paper + ink (goq.sh dark ramp) */9+ --paper: #201913;10+ --paper-2: #271f17;11+ --stage: #16110c;12+ --ink: #ece0cd;13+ --ink-muted: #a3927a;14+ --hair: rgba(236, 224, 205, 0.12);15+ --hair-strong: rgba(236, 224, 205, 0.24);16+ --panel-shadow: rgba(0, 0, 0, 0.45);17+18+ /* earth-tone splashes */19+ --terracotta: #cf7048;20+ --ochre: #d99a47;21+ --moss: #8a9155;22+ --clay: #bd6c52;23+ --sand: #cbb488;24+25+ /* semantic aliases (state colors ride the palette) */26+ --background: var(--paper);27+ --foreground: var(--ink);28+ --muted: var(--paper-2);29+ --muted-fg: var(--ink-muted);30+ --accent: var(--terracotta);31+ --border: var(--hair);32+ --border-strong: var(--hair-strong);33+ --destructive: var(--clay);34+ --green: var(--moss);35+ --yellow: var(--ochre);36+1237 --mono: "SFMono-Regular", "Cascadia Mono", "Liberation Mono", ui-monospace, monospace;1338 --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;1439 }
@@ -32,6 +57,8 @@ body {
3257 -webkit-font-smoothing: antialiased;3358 }3459 60+::selection { background: var(--ochre); color: var(--paper); }61+3562 /* ─── Top bar ─────────────────────────────────────────────────────── */3663 .topbar {3764 display: flex;
@@ -44,12 +71,13 @@ body {
4471 .brand {4572 font-family: var(--mono);4673 font-size: 13px;47- font-weight: 500;48- color: var(--foreground);74+ font-weight: 600;75+ letter-spacing: 0.02em;76+ text-transform: lowercase;77+ color: var(--terracotta);4978 }5079 .dev-badge {51- border: 1px solid rgba(251, 191, 36, 0.65);52- border-radius: 3px;80+ border: 1px solid color-mix(in srgb, var(--ochre) 65%, transparent);5381 color: var(--yellow);5482 font-family: var(--mono);5583 font-size: 10px;
@@ -63,7 +91,7 @@ body {
6391 margin: -10px 0 20px;6492 border: 1px solid var(--yellow);6593 color: var(--yellow);66- background: rgba(251, 191, 36, 0.08);94+ background: color-mix(in srgb, var(--ochre) 8%, transparent);6795 padding: 10px;6896 font-family: var(--mono);6997 font-size: 11px;
@@ -78,10 +106,11 @@ body {
78106 color: var(--muted-fg);79107 display: flex;80108 align-items: center;81- gap: 6px;109+ gap: 7px;82110 }111+/* pixel-square state dot, matching the site's pixel bullets */83112 .status-line .dot {84- width: 6px; height: 6px; border-radius: 50%;113+ width: 7px; height: 7px;85114 background: var(--muted-fg);86115 }87116 .status-line .dot.ok { background: var(--green); }
@@ -115,6 +144,7 @@ body {
115144 .link-btn {116145 font-family: var(--mono);117146 font-size: 12px;147+ text-transform: lowercase;118148 color: var(--muted-fg);119149 cursor: pointer;120150 text-decoration: underline;
@@ -131,7 +161,7 @@ body {
131161 .link-btn:focus-visible,132162 .link-btn.controller-focus {133163 color: var(--foreground);134- outline: 2px solid var(--green);164+ outline: 2px solid var(--moss);135165 outline-offset: 4px;136166 }137167
@@ -143,7 +173,7 @@ body {
143173 justify-content: center;144174 position: relative;145175 min-height: 0;146- background: #0d0d0d;176+ background: var(--stage);147177 }148178 #frame-canvas {149179 max-width: 100%;
@@ -179,6 +209,14 @@ body.native-pointer-control * { cursor: none !important; }
179209 font-size: 13px;180210 padding: 40px;181211 }212+.placeholder::before {213+ content: "";214+ display: inline-block;215+ width: 8px; height: 8px;216+ margin-right: 9px;217+ background: var(--ochre);218+ vertical-align: baseline;219+}182220 .placeholder.hidden { display: none; }183221 184222 /* Session controls */
@@ -218,7 +256,7 @@ body.native-pointer-control * { cursor: none !important; }
218256 }219257 .control-toggle:focus-visible,220258 .control-toggle.controller-focus {221- outline: 2px solid var(--green);259+ outline: 2px solid var(--moss);222260 outline-offset: 1px;223261 }224262 .control-toggle.disabled {
@@ -231,11 +269,17 @@ body.native-pointer-control * { cursor: none !important; }
231269 text-decoration: none;232270 }233271 #audio-toggle {234- min-width: 32px;235- padding-inline: 6px;272+ display: inline-flex;273+ align-items: center;274+ justify-content: center;275+ min-width: 40px;276+ min-height: 32px;277+ padding: 6px 10px;278+ font-family: system-ui, sans-serif;236279 font-size: 16px;237280 line-height: 1;238- text-decoration: none;281+ white-space: nowrap;282+ touch-action: manipulation;239283 }240284 241285 /* ─── Bottom bar ──────────────────────────────────────────────────── */
@@ -264,11 +308,13 @@ body.native-pointer-control * { cursor: none !important; }
264308 .bottombar .prompt { color: var(--foreground); }265309 .bottombar .sep { color: var(--muted-fg); }266310 .bottombar .action {311+ font-family: var(--mono);312+ text-transform: lowercase;267313 color: var(--muted-fg);268314 cursor: pointer;269315 text-decoration: underline;270316 text-decoration-color: transparent;271- transition: color 0.1s, text-decoration-color 0.1s;317+ transition: color 0.1s, text-decoration-color 0.1s, background 0.1s;272318 background: none;273319 border: none;274320 }
@@ -276,13 +322,24 @@ body.native-pointer-control * { cursor: none !important; }
276322 color: var(--foreground);277323 text-decoration-color: currentColor;278324 }279-.bottombar .action.primary { color: var(--foreground); }280-.bottombar .action.danger { color: var(--destructive); opacity: 0.7; }325+.bottombar .action.primary {326+ color: var(--foreground);327+ border: 1px solid var(--border-strong);328+ padding: 4px 12px;329+ letter-spacing: 0.04em;330+ text-decoration: none;331+ box-shadow: 2px 2px 0 var(--hair);332+}333+.bottombar .action.primary:hover {334+ background: var(--ink);335+ color: var(--paper);336+}337+.bottombar .action.danger { color: var(--destructive); opacity: 0.8; }281338 .bottombar .action.danger:hover { opacity: 1; }282339 .bottombar .action:focus-visible,283340 .bottombar .action.controller-focus {284341 color: var(--foreground);285- outline: 2px solid var(--green);342+ outline: 2px solid var(--moss);286343 outline-offset: 3px;287344 }288345 .bottombar .hidden { display: none; }
@@ -304,9 +361,9 @@ body.native-pointer-control * { cursor: none !important; }
304361 }305362 .pin-input:focus {306363 outline: none;307- border-bottom-color: var(--foreground);364+ border-bottom-color: var(--sand);308365 }309-.pin-input.controller-focus { outline: 2px solid var(--green); outline-offset: 3px; }366+.pin-input.controller-focus { outline: 2px solid var(--moss); outline-offset: 3px; }310367 .pin-input.hidden { display: none; }311368 .pin-label {312369 font-size: 11px;
@@ -329,7 +386,7 @@ body.native-pointer-control * { cursor: none !important; }
329386 #control-toggle { max-width: 40vw; }330387 }331388 332-/* ─── Intro / overlay screen ──────────────────────────────────────── */389+/* ─── Intro / overlay screens ─────────────────────────────────────── */333390 .overlay-screen {334391 position: fixed;335392 inset: 0;
@@ -341,16 +398,37 @@ body.native-pointer-control * { cursor: none !important; }
341398 }342399 .overlay-screen.hidden { display: none; }343400 .overlay-content {401+ position: relative;344402 text-align: center;345- width: 320px;346- padding: 32px;403+ width: 360px;404+ max-width: calc(100vw - 48px);405+ padding: 36px 32px 32px;347406 border: 1px solid var(--border-strong);348407 background: var(--muted);408+ box-shadow: 12px 12px 0 var(--panel-shadow);409+}410+/* dithered strip along the panel's top edge */411+.overlay-content::before {412+ content: "";413+ position: absolute;414+ top: 0; left: 0; right: 0;415+ height: 6px;416+ background-image:417+ repeating-linear-gradient(90deg,418+ var(--terracotta) 0 6px, transparent 6px 18px),419+ repeating-linear-gradient(90deg,420+ var(--moss) 9px 12px, transparent 12px 30px),421+ repeating-linear-gradient(90deg,422+ var(--ochre) 21px 24px, transparent 24px 42px);423+ opacity: 0.7;424+ pointer-events: none;349425 }350426 .overlay-content h1 {351427 font-family: var(--mono);352428 font-size: 16px;353- font-weight: 500;429+ font-weight: 700;430+ text-transform: lowercase;431+ letter-spacing: -0.01em;354432 color: var(--foreground);355433 margin-bottom: 12px;356434 }
@@ -358,6 +436,7 @@ body.native-pointer-control * { cursor: none !important; }
358436 font-size: 26px;359437 letter-spacing: -0.5px;360438 margin-bottom: 8px;439+ color: var(--terracotta);361440 }362441 .overlay-content p {363442 font-size: 12px;
@@ -367,10 +446,40 @@ body.native-pointer-control * { cursor: none !important; }
367446 font-family: var(--mono);368447 letter-spacing: 0.02em;369448 }449+/* pixel security-key mark (tap overlay) */450+.pixel-key {451+ display: block;452+ margin: 0 auto 16px;453+ image-rendering: pixelated;454+ image-rendering: crisp-edges;455+}456+/* three-step flow eyebrow: 01 pin · 02 tap · 03 play */457+.flow-steps {458+ display: flex;459+ justify-content: center;460+ gap: 16px;461+ margin-bottom: 20px;462+ font-family: var(--mono);463+ font-size: 10px;464+ font-weight: 500;465+ letter-spacing: 0.12em;466+ text-transform: uppercase;467+ color: var(--muted-fg);468+}469+.flow-step { opacity: 0.55; }470+.flow-step.active {471+ opacity: 1;472+ font-weight: 700;473+ padding-bottom: 3px;474+ border-bottom: 2px solid currentColor;475+}476+#intro .flow-step.active { color: var(--terracotta); }477+#tap-overlay .flow-step.active { color: var(--ochre); }370478 .overlay-actions {371479 display: flex;372480 gap: 16px;373481 justify-content: center;482+ align-items: center;374483 margin-top: 14px;375484 }376485 .shortcut-key {
@@ -395,15 +504,16 @@ body.native-pointer-control * { cursor: none !important; }
395504 }396505 .overlay-input:focus {397506 outline: none;398- border-bottom-color: var(--foreground);507+ border-bottom-color: var(--sand);399508 }400509 .overlay-input.controller-focus {401- outline: 2px solid var(--green);510+ outline: 2px solid var(--moss);402511 outline-offset: 4px;403512 }404513 .overlay-btn {405514 font-family: var(--mono);406515 font-size: 13px;516+ text-transform: lowercase;407517 color: var(--muted-fg);408518 cursor: pointer;409519 background: none;
@@ -411,20 +521,35 @@ body.native-pointer-control * { cursor: none !important; }
411521 padding: 6px 12px;412522 text-decoration: underline;413523 text-decoration-color: transparent;414- transition: color 0.1s, text-decoration-color 0.1s;524+ transition: color 0.1s, text-decoration-color 0.1s, background 0.1s;415525 }416526 .overlay-btn:hover {417527 color: var(--foreground);418528 text-decoration-color: currentColor;419529 }420-.overlay-btn.primary { color: var(--foreground); }530+.overlay-btn.primary {531+ color: var(--foreground);532+ border: 1px solid var(--border-strong);533+ padding: 7px 20px;534+ letter-spacing: 0.06em;535+ text-decoration: none;536+ box-shadow: 3px 3px 0 var(--hair);537+}538+.overlay-btn.primary:hover {539+ background: var(--ink);540+ color: var(--paper);541+}421542 .overlay-btn:focus-visible,422543 .overlay-btn.controller-focus {423544 color: var(--foreground);424- outline: 2px solid var(--green);545+ outline: 2px solid var(--moss);425546 outline-offset: 2px;426547 text-decoration-color: currentColor;427548 }549+.overlay-btn.primary:focus-visible,550+.overlay-btn.primary.controller-focus {551+ text-decoration: none;552+}428553 .overlay-btn:disabled {429554 opacity: 0.4;430555 cursor: not-allowed;
@@ -448,7 +573,7 @@ body.native-pointer-control * { cursor: none !important; }
448573 449574 /* ─── Controller PIN pad ─────────────────────────────────────────── */450575 #pin-pad-overlay { z-index: 160; }451-.pin-pad-content { width: 340px; }576+.pin-pad-content { width: 380px; }452577 .pin-pad-display {453578 min-height: 36px;454579 margin: 8px 0 4px;
@@ -459,29 +584,34 @@ body.native-pointer-control * { cursor: none !important; }
459584 }460585 .pin-pad-grid {461586 display: grid;462- grid-template-columns: repeat(3, 64px);587+ grid-template-columns: repeat(3, 72px);463588 justify-content: center;464589 gap: 10px;465590 margin-top: 20px;466591 }467592 .pin-pad-key {468- height: 52px;593+ height: 56px;469594 border: 1px solid var(--border-strong);470- border-radius: 4px;471595 color: var(--foreground);472596 background: var(--background);473597 font-family: var(--mono);474598 font-size: 18px;475599 cursor: pointer;600+ box-shadow: 3px 3px 0 var(--hair);601+ transition: background 0.1s, color 0.1s;602+}603+.pin-pad-key:hover { border-color: var(--sand); }604+.pin-pad-key:active {605+ transform: translate(2px, 2px);606+ box-shadow: 1px 1px 0 var(--hair);476607 }477-.pin-pad-key:hover { border-color: var(--foreground); }478608 .pin-pad-key:focus-visible,479609 .pin-pad-key.controller-focus {480- outline: 3px solid var(--green);610+ outline: 3px solid var(--moss);481611 outline-offset: 2px;482- border-color: var(--green);612+ border-color: var(--moss);483613 }484-.pin-pad-key.pin-pad-action { font-size: 12px; color: var(--muted-fg); }614+.pin-pad-key.pin-pad-action { font-size: 12px; color: var(--muted-fg); text-transform: lowercase; }485615 .pin-pad-clear { display: block; margin: 12px auto 0; }486616 .panel-action { margin-top: 10px; }487617
@@ -489,7 +619,7 @@ body.native-pointer-control * { cursor: none !important; }
489619 .panel-overlay {490620 position: fixed;491621 inset: 0;492- background: rgba(0,0,0,0.4);622+ background: rgba(0, 0, 0, 0.5);493623 z-index: 90;494624 display: none;495625 }
@@ -499,7 +629,7 @@ body.native-pointer-control * { cursor: none !important; }
499629 top: 0; right: 0; bottom: 0;500630 width: 300px;501631 background: var(--background);502- border-left: 1px solid var(--border);632+ border-left: 1px solid var(--border-strong);503633 transform: translateX(100%);504634 transition: transform 0.15s;505635 z-index: 91;
@@ -517,7 +647,8 @@ body.native-pointer-control * { cursor: none !important; }
517647 .panel-header h2 {518648 font-family: var(--mono);519649 font-size: 13px;520- font-weight: 500;650+ font-weight: 600;651+ text-transform: lowercase;521652 }522653 .panel-body {523654 flex: 1;
@@ -530,10 +661,12 @@ body.native-pointer-control * { cursor: none !important; }
530661 .panel-section h3 {531662 font-family: var(--mono);532663 font-size: 11px;533- font-weight: 500;534- color: var(--muted-fg);664+ font-weight: 700;665+ text-transform: lowercase;666+ color: var(--foreground);535667 margin-bottom: 8px;536668 }669+.panel-section h3 .hash { color: var(--ochre); font-weight: 500; }537670 .info-row {538671 display: flex;539672 justify-content: space-between;
@@ -558,6 +691,7 @@ body.native-pointer-control * { cursor: none !important; }
558691 .info-value {559692 font-family: var(--mono);560693 font-size: 11px;694+ font-variant-numeric: tabular-nums;561695 color: var(--foreground);562696 }563697 .config-select {
@@ -580,5 +714,10 @@ body.native-pointer-control * { cursor: none !important; }
580714 }581715 .config-input:focus {582716 outline: none;583- border-color: var(--foreground);717+ border-color: var(--sand);718+}719+720+@media (prefers-reduced-motion: reduce) {721+ .panel { transition: none; }722+ .pin-pad-key:active { transform: none; }584723 }
M website/dither.js +91 -2
@@ -3,6 +3,8 @@
33 Vanilla, no dependencies. Ordered (Bayer 8x8) dithering of a44 slowly drifting earth-tone gradient, rendered at low resolution55 and scaled up (CSS image-rendering: pixelated) into chunky pixels.6+ The pointer dents a damped wave field on the same grid, so moving7+ the mouse leaves a rippling wake dithered into the terrain.68 ───────────────────────────────────────────────────────────── */79 (function () {810 "use strict";
@@ -133,8 +135,82 @@
133135 dctx.imageSmoothingEnabled = false;134136 palette = readPalette();135137 img = bctx.createImageData(W, H);138+ waveA = new Float32Array(W * H);139+ waveB = new Float32Array(W * H);140+ lastCellX = -1;136141 }137142 143+ /* ── Pointer ripples: a damped wave automaton on the dither grid.144+ The pointer dents the height field; the dent propagates as145+ rings that get dithered into the same ramps as the terrain. */146+ var waveA, waveB; // waveA = current heights, waveB = previous147+ var WAVE_DAMP = 0.975;148+ var WAVE_WARP = 14; // wave gradient displaces the noise-sampling coords (refraction)149+ var WAVE_SHADE = 0.1; // faint crest/trough shading on top of the refraction150+ var lastCellX = -1, lastCellY = -1;151+152+ // Soft 5x5 dent: a wider impulse makes longer-wavelength rings153+ // that survive the busy terrain instead of vanishing into it.154+ function splash(cx, cy, strength) {155+ var x = cx | 0, y = cy | 0;156+ if (x < 2 || y < 2 || x >= W - 2 || y >= H - 2) return;157+ for (var dy = -2; dy <= 2; dy++) {158+ var row = (y + dy) * W + x;159+ for (var dx = -2; dx <= 2; dx++) {160+ var w = 1 - (dx * dx + dy * dy) / 6;161+ if (w > 0) waveA[row + dx] -= strength * w;162+ }163+ }164+ }165+166+ function stepWaves() {167+ var a = waveA, b = waveB;168+ for (var y = 1; y < H - 1; y++) {169+ var row = y * W;170+ for (var x = 1; x < W - 1; x++) {171+ var i = row + x;172+ b[i] = ((a[i - 1] + a[i + 1] + a[i - W] + a[i + W]) * 0.5 - b[i]) * WAVE_DAMP;173+ }174+ }175+ waveA = b;176+ waveB = a;177+ }178+179+ function pointerCell(e) {180+ var r = display.getBoundingClientRect();181+ return [182+ ((e.clientX - r.left) / r.width) * W,183+ ((e.clientY - r.top) / r.height) * H,184+ ];185+ }186+187+ // Listen on the hero (not the canvas) so the trail continues188+ // under the floating panel instead of dying at its edge.189+ var hero = display.parentElement;190+ hero.addEventListener("pointermove", function (e) {191+ if (reduced.matches || !raf) return;192+ var c = pointerCell(e);193+ if (lastCellX >= 0) {194+ var dx = c[0] - lastCellX;195+ var dy = c[1] - lastCellY;196+ var steps = Math.min(48, Math.ceil(Math.max(Math.abs(dx), Math.abs(dy))) || 1);197+ for (var s = 1; s <= steps; s++) {198+ splash(lastCellX + (dx * s) / steps, lastCellY + (dy * s) / steps, 0.4);199+ }200+ } else {201+ splash(c[0], c[1], 0.4);202+ }203+ lastCellX = c[0];204+ lastCellY = c[1];205+ });206+ hero.addEventListener("pointerdown", function (e) {207+ if (reduced.matches || !raf) return;208+ var c = pointerCell(e);209+ splash(c[0], c[1], 2);210+ });211+ hero.addEventListener("pointerleave", function () { lastCellX = -1; });212+ hero.addEventListener("pointercancel", function () { lastCellX = -1; });213+138214 // Per-load random seed: shifts sampling into a different region of139215 // the infinite noise field so the background doesn't start the same140216 // place every visit. (Browser Math.random — fine here.)
@@ -182,7 +258,18 @@
182258 var brow = BAYER[y & 7];183259 var brow2 = BAYER[(y + 4) & 7];184260 for (var x = 0; x < W; x++) {185- var v = field(x, y, t);261+ // refract the terrain through the wave: the height-field262+ // gradient bends where this cell samples the noise, so the263+ // marbling itself churns inside the rings.264+ var wi = i >> 2;265+ var wx = x, wy = y;266+ if (x > 0 && x < W - 1 && y > 0 && y < H - 1) {267+ wx += (waveA[wi + 1] - waveA[wi - 1]) * WAVE_WARP;268+ wy += (waveA[wi + W] - waveA[wi - W]) * WAVE_WARP;269+ }270+ var v = field(wx, wy, t) + waveA[wi] * WAVE_SHADE;271+ if (v < 0) v = 0;272+ else if (v > 1) v = 1;186273 // ordered-dither the luminance ramp between neighbouring stops187274 var scaled = v * last;188275 var lo = Math.floor(scaled);
@@ -191,7 +278,7 @@
191278 var frac = scaled - lo;192279 var idx = frac > brow[x & 7] ? lo + 1 : lo;193280 // ordered-dither the region to pick which ramp this pixel uses194- var region = regionField(x, y, t);281+ var region = regionField(wx, wy, t);195282 var arr = region > brow2[(x + 4) & 7] ? warm : green;196283 var c = arr[idx];197284 data[i] = c[0];
@@ -214,6 +301,8 @@
214301 function loop(now) {215302 if (start === null) start = now;216303 if (now - lastDraw >= FRAME_MS) {304+ stepWaves();305+ stepWaves(); // two sim ticks per drawn frame: rings spread faster217306 render((now - start) / 1000);218307 lastDraw = now;219308 }
M website/index.html +1 -1
@@ -5,7 +5,7 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1" />66 <title>goq.sh: one-to-one Steam streaming over iroh</title>77 <meta name="description" content="goq.sh pairs Portal, a native client, with Sigil, a headless Gamescope host, for games over QUIC. One-to-one, low-latency Steam streaming over iroh. Your key is the address." />8- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%23ece3d2'/%3E%3Crect x='1' y='4' width='6' height='8' fill='%23b5623b'/%3E%3Crect x='3' y='7' width='2' height='2' fill='%23ece3d2'/%3E%3Crect x='7' y='7' width='8' height='2' fill='%23c98a3a'/%3E%3Crect x='10' y='9' width='2' height='2' fill='%236b7145'/%3E%3Crect x='13' y='9' width='2' height='3' fill='%236b7145'/%3E%3C/svg%3E" />8+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024' shape-rendering='crispEdges'%3E%3Crect x='100' y='100' width='824' height='824' rx='185' fill='%23ece3d2'/%3E%3Cg transform='translate(224,356) scale(44)' fill='%232b2119' fill-opacity='0.18'%3E%3Crect width='6' height='8'/%3E%3Crect x='6' y='3' width='8' height='2'/%3E%3Crect x='9' y='5' width='2' height='2'/%3E%3Crect x='12' y='5' width='2' height='3'/%3E%3C/g%3E%3Cg transform='translate(204,336) scale(44)'%3E%3Crect width='6' height='8' fill='%23b5623b'/%3E%3Crect x='2' y='3' width='2' height='2' fill='%23ece3d2'/%3E%3Crect x='6' y='3' width='8' height='2' fill='%23c98a3a'/%3E%3Crect x='9' y='5' width='2' height='2' fill='%236b7145'/%3E%3Crect x='12' y='5' width='2' height='3' fill='%236b7145'/%3E%3C/g%3E%3C/svg%3E" />99 <link rel="stylesheet" href="./styles.css" />1010 </head>1111 <body>