ai-agent-session-center 2.0.0 → 2.0.1

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.
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { execSync } from 'child_process';
6
+ import { scryptSync, randomBytes } from 'crypto';
6
7
 
7
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
9
  const PROJECT_ROOT = join(__dirname, '..');
@@ -28,6 +29,7 @@ try {
28
29
 
29
30
  // ── readline helper ──
30
31
  const rl = createInterface({ input: process.stdin, output: process.stdout });
32
+ let rlClosed = false;
31
33
 
32
34
  function ask(prompt) {
33
35
  return new Promise(resolve => rl.question(prompt, resolve));
@@ -54,8 +56,46 @@ async function askValue(stepNum, totalSteps, label, defaultVal) {
54
56
  return answer.trim() || String(defaultVal);
55
57
  }
56
58
 
59
+ // ── Password helper ──
60
+ function hashPassword(password) {
61
+ const salt = randomBytes(16).toString('hex');
62
+ const hash = scryptSync(password, salt, 64).toString('hex');
63
+ return `${salt}:${hash}`;
64
+ }
65
+
66
+ async function askPassword(prompt) {
67
+ return new Promise((resolve) => {
68
+ process.stdout.write(prompt);
69
+ const wasRaw = process.stdin.isRaw;
70
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
71
+ let input = '';
72
+ const onData = (ch) => {
73
+ const c = ch.toString();
74
+ if (c === '\n' || c === '\r') {
75
+ if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw || false);
76
+ process.stdin.removeListener('data', onData);
77
+ process.stdout.write('\n');
78
+ resolve(input);
79
+ } else if (c === '\u007f' || c === '\b') {
80
+ if (input.length > 0) {
81
+ input = input.slice(0, -1);
82
+ process.stdout.write('\b \b');
83
+ }
84
+ } else if (c === '\u0003') {
85
+ // Ctrl+C
86
+ process.exit(1);
87
+ } else {
88
+ input += c;
89
+ process.stdout.write('*');
90
+ }
91
+ };
92
+ process.stdin.resume();
93
+ process.stdin.on('data', onData);
94
+ });
95
+ }
96
+
57
97
  // ── Main ──
58
- const TOTAL = 5;
98
+ const TOTAL = 6;
59
99
 
60
100
  console.log(`\n${CYAN}╭──────────────────────────────────────────────╮${RESET}`);
61
101
  console.log(`${CYAN}│${RESET} ${BOLD}AI Agent Session Center — Setup Wizard${RESET} ${CYAN}│${RESET}`);
@@ -113,7 +153,61 @@ const historyOptions = [
113
153
  const currentHistIdx = historyOptions.findIndex(o => o.value === (existing.sessionHistoryHours || 24));
114
154
  const history = await choose(5, TOTAL, 'Session history retention', historyOptions, currentHistIdx >= 0 ? currentHistIdx : 1);
115
155
 
116
- rl.close();
156
+ // 6. Dashboard password
157
+ const hasExistingPassword = Boolean(existing.passwordHash);
158
+ let passwordHash = null;
159
+
160
+ if (hasExistingPassword) {
161
+ const pwOptions = [
162
+ { label: `Keep current password`, value: 'keep' },
163
+ { label: `Change password`, value: 'change' },
164
+ { label: `Remove password ${DIM}— no login required${RESET}`, value: 'remove' },
165
+ ];
166
+ const pwChoice = await choose(6, TOTAL, 'Dashboard password', pwOptions, 0);
167
+ if (pwChoice.value === 'keep') {
168
+ passwordHash = existing.passwordHash;
169
+ } else if (pwChoice.value === 'change') {
170
+ rl.close(); rlClosed = true;
171
+ const pw = await askPassword(` ${DIM}New password:${RESET} `);
172
+ if (pw.length < 4) {
173
+ console.log(` ${RED}✗ Password must be at least 4 characters${RESET}`);
174
+ process.exit(1);
175
+ }
176
+ const confirm = await askPassword(` ${DIM}Confirm password:${RESET} `);
177
+ if (pw !== confirm) {
178
+ console.log(` ${RED}✗ Passwords do not match${RESET}`);
179
+ process.exit(1);
180
+ }
181
+ passwordHash = hashPassword(pw);
182
+ ok('Password updated');
183
+ } else {
184
+ passwordHash = null;
185
+ ok('Password removed — no login required');
186
+ }
187
+ } else {
188
+ const pwOptions = [
189
+ { label: `No password ${DIM}— open access on localhost${RESET}`, value: 'none' },
190
+ { label: `Set a password ${DIM}— require login${RESET}`, value: 'set' },
191
+ ];
192
+ const pwChoice = await choose(6, TOTAL, 'Dashboard password (optional)', pwOptions, 0);
193
+ if (pwChoice.value === 'set') {
194
+ rl.close(); rlClosed = true;
195
+ const pw = await askPassword(` ${DIM}Enter password:${RESET} `);
196
+ if (pw.length < 4) {
197
+ console.log(` ${RED}✗ Password must be at least 4 characters${RESET}`);
198
+ process.exit(1);
199
+ }
200
+ const confirm = await askPassword(` ${DIM}Confirm password:${RESET} `);
201
+ if (pw !== confirm) {
202
+ console.log(` ${RED}✗ Passwords do not match${RESET}`);
203
+ process.exit(1);
204
+ }
205
+ passwordHash = hashPassword(pw);
206
+ ok('Password set — login will be required');
207
+ }
208
+ }
209
+
210
+ if (!rlClosed) { rl.close(); rlClosed = true; }
117
211
 
118
212
  // ── Save config ──
119
213
  const configData = {
@@ -122,6 +216,7 @@ const configData = {
122
216
  hookDensity: density.value,
123
217
  debug: debug.value,
124
218
  sessionHistoryHours: history.value,
219
+ ...(passwordHash ? { passwordHash } : {}),
125
220
  };
126
221
 
127
222
  const dataDir = join(PROJECT_ROOT, 'data');
@@ -136,6 +231,7 @@ info(`Enabled CLIs: ${BOLD}${configData.enabledClis.join(', ')}${RESET}`);
136
231
  info(`Hook density: ${BOLD}${configData.hookDensity}${RESET}`);
137
232
  info(`Debug: ${BOLD}${configData.debug ? 'ON' : 'OFF'}${RESET}`);
138
233
  info(`History retention: ${BOLD}${configData.sessionHistoryHours}h${RESET}`);
234
+ info(`Password: ${BOLD}${configData.passwordHash ? 'Enabled' : 'Disabled'}${RESET}`);
139
235
 
140
236
  // ── Install hooks with chosen density ──
141
237
  console.log('');
@@ -154,3 +250,6 @@ console.log(` ${GREEN}✓ Setup complete!${RESET}`);
154
250
  console.log(`${GREEN}────────────────────────────────────────────────${RESET}`);
155
251
  console.log(`\n Starting server on port ${BOLD}${configData.port}${RESET}...`);
156
252
  console.log(` Browser will open automatically.\n`);
253
+
254
+ // Explicit exit — askPassword's process.stdin.resume() keeps the event loop alive
255
+ process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agent-session-center",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "A real-time dashboard for monitoring AI agent sessions (Claude Code, Gemini CLI, Codex) with 3D visualization",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -40,6 +40,7 @@
40
40
  "node": ">=18.0.0"
41
41
  },
42
42
  "dependencies": {
43
+ "better-sqlite3": "^12.6.2",
43
44
  "express": "^5.0.0",
44
45
  "node-pty": "^1.1.0",
45
46
  "ws": "^8.18.0"
@@ -846,3 +846,321 @@ body[data-effect-ended="none"] .css-robot[data-status="ended"] {
846
846
  flex: 1;
847
847
  }
848
848
 
849
+ /* ============================================================
850
+ Responsive: Animation Mobile & Touch Enhancements
851
+ ============================================================ */
852
+
853
+ /* --- Enhanced prefers-reduced-motion support --- */
854
+ @media (prefers-reduced-motion: reduce) {
855
+ /* Override all animation effects with simpler alternatives */
856
+ .css-robot[data-status="working"] .robot-head::after,
857
+ .css-robot[data-status="working"] .robot-head::before,
858
+ .char-cat[data-status="working"] .cat-head::after,
859
+ .char-cat[data-status="working"] .cat-head::before,
860
+ .char-alien[data-status="working"] .alien-dome::after,
861
+ .char-alien[data-status="working"] .alien-dome::before,
862
+ .char-ghost[data-status="working"] .ghost-body::after,
863
+ .char-ghost[data-status="working"] .ghost-body::before,
864
+ .char-orb[data-status="working"] .orb-core::after,
865
+ .char-orb[data-status="working"] .orb-core::before,
866
+ .char-dragon[data-status="working"] .dragon-head::after,
867
+ .char-dragon[data-status="working"] .dragon-head::before {
868
+ animation: none !important;
869
+ opacity: 0 !important;
870
+ }
871
+
872
+ /* Disable triggered movement effects */
873
+ .css-robot[data-movement]::before,
874
+ .css-robot[data-movement]::after {
875
+ animation: none !important;
876
+ opacity: 0 !important;
877
+ }
878
+
879
+ .css-robot[data-movement] .robot-body-wrap {
880
+ animation: none !important;
881
+ }
882
+
883
+ /* Disable configurable status effects */
884
+ body[data-effect-working="energy-ring"] .css-robot[data-status="working"]::before,
885
+ body[data-effect-working="energy-ring"] .css-robot[data-status="working"]::after,
886
+ body[data-effect-working="sparks"] .css-robot[data-status="working"]::before,
887
+ body[data-effect-working="sparks"] .css-robot[data-status="working"]::after,
888
+ body[data-effect-working="steam"] .css-robot[data-status="working"]::before,
889
+ body[data-effect-working="steam"] .css-robot[data-status="working"]::after {
890
+ animation: none !important;
891
+ opacity: 0 !important;
892
+ }
893
+
894
+ /* Show static indicators instead of animations for key states */
895
+ .css-robot[data-status="approval"] {
896
+ outline: 2px solid rgba(255, 221, 0, 0.6);
897
+ outline-offset: 4px;
898
+ border-radius: 8px;
899
+ }
900
+
901
+ .css-robot[data-status="working"] {
902
+ outline: 2px solid rgba(255, 152, 0, 0.4);
903
+ outline-offset: 4px;
904
+ border-radius: 8px;
905
+ }
906
+
907
+ /* Suppress movement preview animations */
908
+ .movement-preview-viewport .css-robot {
909
+ animation: none !important;
910
+ }
911
+ .movement-preview-viewport .css-robot::before,
912
+ .movement-preview-viewport .css-robot::after {
913
+ animation: none !important;
914
+ }
915
+ }
916
+
917
+ /* --- :active touch feedback on animation settings controls --- */
918
+ .anim-intensity-control input[type="range"]:active {
919
+ opacity: 0.8;
920
+ }
921
+
922
+ .movement-preview-viewport:active {
923
+ opacity: 0.9;
924
+ }
925
+
926
+ /* --- Touch device enhancements (pointer: coarse) --- */
927
+ @media (pointer: coarse) {
928
+ /* Animation sliders: larger hit area on touch devices */
929
+ .anim-intensity-control input[type="range"] {
930
+ height: 44px;
931
+ cursor: pointer;
932
+ }
933
+
934
+ .anim-intensity-control {
935
+ min-height: 44px;
936
+ }
937
+
938
+ /* Movement preview: larger tap targets */
939
+ .movement-preview-viewport {
940
+ min-height: 120px;
941
+ }
942
+ }
943
+
944
+ /* --- Tablet (max-width: 768px) --- */
945
+ @media (max-width: 768px) {
946
+ /* Scale down characters proportionally on tablet */
947
+ .css-robot {
948
+ transform: scale(0.85);
949
+ transform-origin: center bottom;
950
+ }
951
+
952
+ /* Movement preview: slightly smaller */
953
+ .movement-preview-viewport {
954
+ height: 140px;
955
+ }
956
+
957
+ .movement-preview-viewport .css-robot {
958
+ transform: scale(0.75);
959
+ }
960
+ }
961
+
962
+ /* --- Small tablet / large phone (max-width: 640px) --- */
963
+ @media (max-width: 640px) {
964
+ /* Scale down characters more on mobile */
965
+ .css-robot {
966
+ transform: scale(0.75);
967
+ transform-origin: center bottom;
968
+ }
969
+
970
+ /* Reduce sweat drops box-shadow complexity for performance */
971
+ .css-robot[data-status="working"] .robot-head::after,
972
+ .char-cat[data-status="working"] .cat-head::after,
973
+ .char-alien[data-status="working"] .alien-dome::after,
974
+ .char-ghost[data-status="working"] .ghost-body::after,
975
+ .char-orb[data-status="working"] .orb-core::after,
976
+ .char-dragon[data-status="working"] .dragon-head::after,
977
+ .char-penguin[data-status="working"] .penguin-head::after,
978
+ .char-octopus[data-status="working"] .octo-head::after,
979
+ .char-mushroom[data-status="working"] .mush-cap::after,
980
+ .char-fox[data-status="working"] .fox-head::after,
981
+ .char-unicorn[data-status="working"] .unicorn-head::after,
982
+ .char-jellyfish[data-status="working"] .jelly-bell::after,
983
+ .char-owl[data-status="working"] .owl-head::after,
984
+ .char-bat[data-status="working"] .bat-head::after,
985
+ .char-cactus[data-status="working"] .cactus-body::after,
986
+ .char-slime[data-status="working"] .slime-body::after,
987
+ .char-pumpkin[data-status="working"] .pumpkin-body::after,
988
+ .char-yeti[data-status="working"] .yeti-head::after,
989
+ .char-crystal[data-status="working"] .crystal-body::after,
990
+ .char-bee[data-status="working"] .bee-head::after {
991
+ /* Reduce from 19 box-shadow drops to 6 for mobile performance */
992
+ box-shadow:
993
+ -4px 4px 0 1px rgba(100,220,255,1),
994
+ 2px 14px 0 0 rgba(80,200,255,0.9),
995
+ -2px 24px 0 1px rgba(80,200,255,0.85),
996
+ 5px 34px 0 0 rgba(60,190,255,0.8),
997
+ -3px 44px 0 1px rgba(100,220,255,0.75),
998
+ 1px 49px 0 0 rgba(80,200,255,0.7);
999
+ }
1000
+
1001
+ .css-robot[data-status="working"] .robot-head::before,
1002
+ .char-cat[data-status="working"] .cat-head::before,
1003
+ .char-alien[data-status="working"] .alien-dome::before,
1004
+ .char-ghost[data-status="working"] .ghost-body::before,
1005
+ .char-orb[data-status="working"] .orb-core::before,
1006
+ .char-dragon[data-status="working"] .dragon-head::before,
1007
+ .char-penguin[data-status="working"] .penguin-head::before,
1008
+ .char-octopus[data-status="working"] .octo-head::before,
1009
+ .char-mushroom[data-status="working"] .mush-cap::before,
1010
+ .char-fox[data-status="working"] .fox-head::before,
1011
+ .char-unicorn[data-status="working"] .unicorn-head::before,
1012
+ .char-jellyfish[data-status="working"] .jelly-bell::before,
1013
+ .char-owl[data-status="working"] .owl-head::before,
1014
+ .char-bat[data-status="working"] .bat-head::before,
1015
+ .char-cactus[data-status="working"] .cactus-body::before,
1016
+ .char-slime[data-status="working"] .slime-body::before,
1017
+ .char-pumpkin[data-status="working"] .pumpkin-body::before,
1018
+ .char-yeti[data-status="working"] .yeti-head::before,
1019
+ .char-crystal[data-status="working"] .crystal-body::before,
1020
+ .char-bee[data-status="working"] .bee-head::before {
1021
+ /* Reduce from 19 box-shadow drops to 6 for mobile performance */
1022
+ box-shadow:
1023
+ 4px 4px 0 1px rgba(100,220,255,1),
1024
+ -2px 14px 0 0 rgba(80,200,255,0.9),
1025
+ 2px 24px 0 1px rgba(80,200,255,0.85),
1026
+ -5px 34px 0 0 rgba(60,190,255,0.8),
1027
+ 3px 44px 0 1px rgba(100,220,255,0.75),
1028
+ -1px 49px 0 0 rgba(80,200,255,0.7);
1029
+ }
1030
+
1031
+ /* Simplify triggered movement sparks (reduce box-shadow points) */
1032
+ .css-robot[data-movement="sparks"]::before {
1033
+ box-shadow:
1034
+ 12px -15px 0 1px #ffcc00,
1035
+ -16px -10px 0 0 #ffa000,
1036
+ 20px 6px 0 1px #ff9800,
1037
+ -12px 12px 0 0 #ffcc80;
1038
+ }
1039
+ .css-robot[data-movement="sparks"]::after {
1040
+ box-shadow:
1041
+ -10px -18px 0 1px #ffe082,
1042
+ 18px -12px 0 0 #ffca28,
1043
+ -22px 4px 0 1px #ffc107,
1044
+ 10px 16px 0 0 #ffe082;
1045
+ }
1046
+
1047
+ /* Simplify triggered sparkle effects */
1048
+ .css-robot[data-movement="sparkle"]::before {
1049
+ box-shadow:
1050
+ 25px -8px 0 1px rgba(255,255,255,1),
1051
+ -15px 20px 0 0 rgba(255,255,255,0.95),
1052
+ 30px 25px 0 1px rgba(255,255,255,0.9),
1053
+ 8px 40px 0 0 rgba(255,255,255,0.85);
1054
+ }
1055
+ .css-robot[data-movement="sparkle"]::after {
1056
+ box-shadow:
1057
+ 15px 10px 0 1px rgba(255,255,200,0.9),
1058
+ 35px 5px 0 0 rgba(255,255,200,0.85),
1059
+ 5px 30px 0 1px rgba(255,255,200,0.8);
1060
+ }
1061
+
1062
+ /* Simplify energy-ring effects */
1063
+ body[data-effect-working="energy-ring"] .css-robot[data-status="working"]::before {
1064
+ box-shadow: 0 0 15px rgba(255, 152, 0, 0.5);
1065
+ }
1066
+ body[data-effect-working="energy-ring"] .css-robot[data-status="working"]::after {
1067
+ box-shadow: 0 0 10px rgba(255, 200, 0, 0.3);
1068
+ }
1069
+
1070
+ /* Simplify steam clouds */
1071
+ body[data-effect-working="steam"] .css-robot[data-status="working"]::before {
1072
+ box-shadow:
1073
+ 14px -6px 0 4px rgba(220,220,240,0.5),
1074
+ -8px -12px 0 3px rgba(220,220,240,0.4);
1075
+ }
1076
+ body[data-effect-working="steam"] .css-robot[data-status="working"]::after {
1077
+ box-shadow:
1078
+ -12px -8px 0 4px rgba(220,220,240,0.45);
1079
+ }
1080
+
1081
+ /* Movement preview: compact */
1082
+ .movement-preview-viewport {
1083
+ height: 120px;
1084
+ }
1085
+
1086
+ .movement-preview-viewport .css-robot {
1087
+ transform: scale(0.6);
1088
+ }
1089
+
1090
+ /* Animation settings: compact */
1091
+ .movement-preview-label {
1092
+ font-size: 10px;
1093
+ }
1094
+
1095
+ .movement-preview-effect-name {
1096
+ font-size: 9px;
1097
+ }
1098
+ }
1099
+
1100
+ /* --- Phone (max-width: 480px) --- */
1101
+ @media (max-width: 480px) {
1102
+ /* Scale down characters further */
1103
+ .css-robot {
1104
+ transform: scale(0.65);
1105
+ transform-origin: center bottom;
1106
+ }
1107
+
1108
+ /* Reduce animation speed on small screens for less jank */
1109
+ .css-robot[data-status="working"] .robot-head::after,
1110
+ .css-robot[data-status="working"] .robot-head::before {
1111
+ animation-duration: 1.2s;
1112
+ }
1113
+
1114
+ /* Movement preview: even more compact */
1115
+ .movement-preview-viewport {
1116
+ height: 100px;
1117
+ }
1118
+
1119
+ .movement-preview-viewport .css-robot {
1120
+ transform: scale(0.5);
1121
+ }
1122
+
1123
+ /* Anim intensity control: compact layout */
1124
+ .anim-intensity-control span {
1125
+ font-size: 10px;
1126
+ min-width: 24px;
1127
+ }
1128
+ }
1129
+
1130
+ /* --- Small phone (max-width: 375px) --- */
1131
+ @media (max-width: 375px) {
1132
+ .css-robot {
1133
+ transform: scale(0.55);
1134
+ transform-origin: center bottom;
1135
+ }
1136
+
1137
+ .movement-preview-viewport {
1138
+ height: 90px;
1139
+ }
1140
+ }
1141
+
1142
+ /* --- Smallest phone (max-width: 320px) --- */
1143
+ @media (max-width: 320px) {
1144
+ .css-robot {
1145
+ transform: scale(0.5);
1146
+ transform-origin: center bottom;
1147
+ }
1148
+
1149
+ .movement-preview-viewport {
1150
+ height: 80px;
1151
+ }
1152
+
1153
+ .anim-intensity-control {
1154
+ gap: 6px;
1155
+ }
1156
+ }
1157
+
1158
+ /* --- Scroll-snap for horizontally scrollable containers --- */
1159
+ .terminal-toolbar-actions {
1160
+ scroll-snap-type: x proximity;
1161
+ }
1162
+
1163
+ .terminal-toolbar-actions > * {
1164
+ scroll-snap-align: start;
1165
+ }
1166
+
@@ -42,6 +42,18 @@
42
42
  /* Animation control */
43
43
  --anim-intensity: 1;
44
44
  --anim-speed: 1;
45
+
46
+ /* Responsive spacing (desktop defaults) */
47
+ --layout-pad: 24px;
48
+ --layout-pad-sm: 16px;
49
+ --layout-gap: 16px;
50
+ --layout-gap-sm: 8px;
51
+
52
+ /* Safe area fallbacks */
53
+ --safe-top: env(safe-area-inset-top, 0px);
54
+ --safe-right: env(safe-area-inset-right, 0px);
55
+ --safe-bottom: env(safe-area-inset-bottom, 0px);
56
+ --safe-left: env(safe-area-inset-left, 0px);
45
57
  }
46
58
 
47
59
  /* ---- Reset & Base ---- */
@@ -148,6 +160,7 @@ body::after {
148
160
  .modal-panel {
149
161
  -webkit-overflow-scrolling: touch;
150
162
  touch-action: pan-y;
163
+ overflow-y: auto;
151
164
  }
152
165
 
153
166
  /* Ensure all interactive elements meet minimum 44px touch target */
@@ -168,4 +181,128 @@ body::after {
168
181
  min-height: 44px;
169
182
  min-width: 44px;
170
183
  }
184
+
185
+ /* touch-action: manipulation prevents double-tap zoom on interactive elements */
186
+ button,
187
+ a,
188
+ input,
189
+ select,
190
+ textarea,
191
+ .nav-btn,
192
+ .qa-btn,
193
+ .ctrl-btn,
194
+ .tab,
195
+ .nav-actions-toggle,
196
+ .nav-shortcuts-btn,
197
+ .settings-gear,
198
+ .modal-close,
199
+ .theme-swatch,
200
+ .char-swatch,
201
+ .density-btn,
202
+ .ssh-mode-btn,
203
+ .pagination-btn,
204
+ .feed-collapse-btn,
205
+ .session-card,
206
+ .history-row {
207
+ touch-action: manipulation;
208
+ }
209
+ }
210
+
211
+ /* ---- Disable hover-only effects on touch devices ---- */
212
+
213
+ @media (hover: none) {
214
+ .nav-btn:hover,
215
+ .qa-btn:hover,
216
+ .ctrl-btn:hover,
217
+ .tab:hover,
218
+ .nav-actions-toggle:hover,
219
+ .nav-shortcuts-btn:hover,
220
+ .settings-gear:hover,
221
+ .modal-close:hover,
222
+ .history-row:hover,
223
+ .history-delete:hover,
224
+ .pagination-btn:hover,
225
+ .feed-collapse-btn:hover,
226
+ .hook-stats-reset:hover {
227
+ background: unset;
228
+ color: unset;
229
+ border-color: unset;
230
+ box-shadow: unset;
231
+ }
232
+
233
+ /* Keep active-state feedback for touch instead */
234
+ .nav-btn:active,
235
+ .qa-btn:active,
236
+ .ctrl-btn:active,
237
+ .tab:active {
238
+ opacity: 0.7;
239
+ }
240
+ }
241
+
242
+ /* ---- Mobile: enable page scroll, disable zoom ---- */
243
+
244
+ @media (max-width: 768px) {
245
+ html {
246
+ overflow: auto;
247
+ overflow-x: hidden;
248
+ overscroll-behavior-x: none;
249
+ touch-action: pan-x pan-y;
250
+ }
251
+
252
+ body {
253
+ overflow: visible;
254
+ overflow-x: hidden;
255
+ height: auto;
256
+ min-height: 100vh;
257
+ min-height: 100dvh;
258
+ overscroll-behavior: none;
259
+ touch-action: pan-x pan-y;
260
+ }
261
+ }
262
+
263
+ /* ---- Mobile scrollbar hiding ---- */
264
+
265
+ @media (max-width: 768px) {
266
+ .view-panel::-webkit-scrollbar,
267
+ #main-nav::-webkit-scrollbar,
268
+ .modal-panel::-webkit-scrollbar,
269
+ .settings-tab-content::-webkit-scrollbar,
270
+ #feed-entries::-webkit-scrollbar,
271
+ #detail-conversation::-webkit-scrollbar,
272
+ #detail-activity-log::-webkit-scrollbar,
273
+ #notes-list::-webkit-scrollbar,
274
+ #queue-list::-webkit-scrollbar {
275
+ display: none;
276
+ }
277
+
278
+ .view-panel,
279
+ #main-nav,
280
+ .modal-panel,
281
+ .settings-tab-content,
282
+ #feed-entries,
283
+ #detail-conversation,
284
+ #detail-activity-log,
285
+ #notes-list,
286
+ #queue-list {
287
+ scrollbar-width: none;
288
+ }
289
+
290
+ /* Mobile responsive spacing custom properties */
291
+ :root {
292
+ --layout-pad: 12px;
293
+ --layout-pad-sm: 8px;
294
+ --layout-gap: 8px;
295
+ --layout-gap-sm: 4px;
296
+ }
297
+ }
298
+
299
+ /* ---- Extra-small mobile spacing ---- */
300
+
301
+ @media (max-width: 375px) {
302
+ :root {
303
+ --layout-pad: 8px;
304
+ --layout-pad-sm: 6px;
305
+ --layout-gap: 6px;
306
+ --layout-gap-sm: 4px;
307
+ }
171
308
  }