pf2e-spellbook 1.0.0 → 1.1.0

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.
package/dist/index.html CHANGED
@@ -3,168 +3,243 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
- <meta name="theme-color" content="#2b1d12">
6
+ <meta name="theme-color" content="#14161b">
7
7
  <meta name="apple-mobile-web-app-capable" content="yes">
8
8
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
9
9
  <meta name="apple-mobile-web-app-title" content="Spellbook">
10
10
  <meta name="description" content="An interactive Pathfinder 2e spellbook: prepare, browse, and track your spells.">
11
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📖</text></svg>">
11
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237b6cf0' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'><path d='M12 6.5C9.5 4.7 6 4.7 3.5 6v13c2.5-1.3 6-1.3 8.5.5 2.5-1.8 6-1.8 8.5-.5V6c-2.5-1.3-6-1.3-8.5.5z'/><path d='M12 6.5v13'/></svg>">
12
+ <!-- Set the light/dark theme before first paint (avoids a flash); the engine reconciles fully on load. -->
13
+ <script>
14
+ (function(){try{
15
+ var lib=JSON.parse(localStorage.getItem("pf2eSpellbook.v3"))||{};
16
+ var s=lib.settings||{}, mode=s.themeMode||"auto";
17
+ if(mode!=="light"&&mode!=="dark") mode=(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches)?"dark":"light";
18
+ var root=document.documentElement; root.dataset.theme=mode;
19
+ var c=s.custom;
20
+ if(c){ if(c.accent)root.style.setProperty("--accent",c.accent); if(c.bg)root.style.setProperty("--bg",c.bg);
21
+ if(c.ink)root.style.setProperty("--ink",c.ink); if(c.surface)root.style.setProperty("--surface",c.surface); }
22
+ }catch(e){}})();
23
+ </script>
12
24
  <!-- PWA: present alongside index.html on the hosted (GitHub Pages) copy; harmlessly
13
25
  absent when this file is downloaded and opened on its own. -->
14
26
  <link rel="manifest" href="manifest.webmanifest">
15
27
  <link rel="apple-touch-icon" href="icon.svg">
16
28
  <title>PF2e Spellbook</title>
17
29
  <style>
30
+ /* ============================================================
31
+ THEME TOKENS
32
+ Base tokens flip with [data-theme]; --accent (+ --accent-ink) are
33
+ set by JS per class or per custom palette. Accent shades derive via
34
+ color-mix(), so one hue recolours the whole UI.
35
+ ============================================================ */
18
36
  :root{
19
- --bg:#1b130c;
20
- --panel:#2b1d12;
21
- --panel2:#33271a;
22
- --ink:#f4ead8;
23
- --muted:#c9b79a;
24
- --gold:#e0b252;
25
- --gold-dim:#9c7a36;
26
- --heal:#6fc28a;
27
- --harm:#c9657a;
28
- --line:#4a3a26;
29
- --accent:#7fb0d6;
37
+ /* accent — JS overrides --accent and --accent-ink on <html> */
38
+ --accent:#6c7a89;
39
+ --accent-ink:#ffffff;
40
+ --accent-dim:color-mix(in srgb, var(--accent) 55%, var(--line));
41
+ --accent-soft:color-mix(in srgb, var(--accent) 15%, transparent);
42
+ --accent-text:var(--accent);
43
+
44
+ /* fixed rank spectrum (left border bars), shared by both modes */
45
+ --rank0:#b58fd6; --rank1:#7fb0d6; --rank2:#6f9c8f; --rank3:#d6a86f;
46
+ --rank4:#d68f8f; --rank5:#9cc46a; --rank6:#c9c05a; --rank7:#6fb6d6;
47
+ --rank8:#c98fd6; --rank9:#d6a0c0; --rank10:var(--accent);
48
+
30
49
  --radius:16px;
31
- --tap:#3c2c1b;
32
50
  }
51
+
52
+ /* ---- Dark (default) ---- */
53
+ :root, :root[data-theme="dark"]{
54
+ color-scheme:dark;
55
+ --bg:#14161b;
56
+ --surface:#1d2027;
57
+ --surface-2:#262a33;
58
+ --ink:#e8eaf0;
59
+ --muted:#9aa1b0;
60
+ --line:#2f3440;
61
+ --heal:#5bbf7e;
62
+ --harm:#d9637a;
63
+ --info:#6fa8dc;
64
+ --info-bg:color-mix(in srgb, var(--info) 22%, var(--surface));
65
+ --info-ink:#d6e8fb;
66
+ --accent-text:var(--accent);
67
+ --shadow:0 1px 2px rgba(0,0,0,.4), 0 4px 14px rgba(0,0,0,.25);
68
+ }
69
+
70
+ /* ---- Light ---- */
71
+ :root[data-theme="light"]{
72
+ color-scheme:light;
73
+ --bg:#f5f6f8;
74
+ --surface:#ffffff;
75
+ --surface-2:#eef0f4;
76
+ --ink:#1b1e24;
77
+ --muted:#5d6470;
78
+ --line:#dfe3ea;
79
+ --heal:#2f9e5b;
80
+ --harm:#c43a57;
81
+ --info:#2b6cb0;
82
+ --info-bg:color-mix(in srgb, var(--info) 12%, var(--surface));
83
+ --info-ink:#0b3a68;
84
+ --accent-text:color-mix(in srgb, var(--accent) 72%, #000);
85
+ --shadow:0 1px 2px rgba(20,30,50,.08), 0 4px 14px rgba(20,30,50,.06);
86
+ }
87
+
33
88
  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
34
89
  html,body{margin:0;padding:0;}
90
+ html{background:var(--bg);}
35
91
  body{
36
92
  background:var(--bg);
37
93
  color:var(--ink);
38
94
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
39
- font-size:19px;
95
+ font-size:18px;
40
96
  line-height:1.5;
41
97
  padding-bottom:84px; /* room for nav */
42
98
  -webkit-text-size-adjust:100%;
43
99
  }
44
- h1,h2,h3{margin:0 0 .4em;line-height:1.2;}
45
- h1{font-size:1.5rem;color:var(--gold);}
46
- h2{font-size:1.25rem;color:var(--gold);}
100
+ h1,h2,h3{margin:0 0 .4em;line-height:1.2;letter-spacing:-.01em;}
101
+ h1{font-size:1.5rem;color:var(--ink);font-weight:800;}
102
+ h2{font-size:1.2rem;color:var(--ink);font-weight:700;}
47
103
  p{margin:.4em 0;}
48
104
  .wrap{max-width:560px;margin:0 auto;padding:16px 14px;}
49
105
  .hide{display:none !important;}
50
106
 
107
+ /* smooth theme + interaction transitions */
108
+ button,.chip,.card,.cast-card,.classcard,.browse-row,input,select,textarea,
109
+ .stat,nav.bottom button,details.help{
110
+ transition:background-color .15s ease, border-color .15s ease, color .15s ease;
111
+ }
112
+
113
+ /* inline SVG icons inherit accent via currentColor */
114
+ .icn{display:inline-block;width:1em;height:1em;flex:0 0 auto;vertical-align:-.14em;}
115
+
51
116
  /* ---- Header ---- */
52
117
  header.top{
53
- background:linear-gradient(180deg,#3a2715,#241810);
54
- border-bottom:2px solid var(--gold-dim);
118
+ background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 10%, var(--surface)), var(--surface));
119
+ border-bottom:1px solid var(--line);
55
120
  padding:14px 16px;
56
121
  position:sticky;top:0;z-index:20;
57
122
  }
58
- header.top h1{margin:0;font-size:1.35rem;}
59
- header.top .sub{color:var(--muted);font-size:.9rem;}
60
- .statbar{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap;}
123
+ header.top h1{margin:0;font-size:1.3rem;display:flex;align-items:center;gap:10px;}
124
+ header.top h1 #charIcon{display:inline-flex;color:var(--accent-text);font-size:1.5rem;}
125
+ header.top .sub{color:var(--muted);font-size:.88rem;margin-top:2px;}
126
+ .statbar{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;}
61
127
  .stat{
62
- background:var(--panel2);border:1px solid var(--gold-dim);
63
- border-radius:12px;padding:6px 12px;flex:1;min-width:90px;text-align:center;
128
+ background:var(--surface-2);border:1px solid var(--line);
129
+ border-radius:12px;padding:8px 12px;flex:1;min-width:90px;text-align:center;
64
130
  }
65
- .stat .lbl{font-size:.72rem;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;}
66
- .stat .val{font-size:1.4rem;font-weight:700;color:var(--gold);}
131
+ .stat .lbl{font-size:.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:.6px;}
132
+ .stat .val{font-size:1.4rem;font-weight:800;color:var(--accent-text);}
67
133
 
68
134
  /* ---- Cards ---- */
69
135
  .card{
70
- background:var(--panel);
136
+ background:var(--surface);
71
137
  border:1px solid var(--line);
72
138
  border-radius:var(--radius);
73
139
  padding:16px;margin:14px 0;
74
- box-shadow:0 2px 0 rgba(0,0,0,.3);
75
- }
76
- .card.rank0{border-left:6px solid #b58fd6;}
77
- .card.rank1{border-left:6px solid #7fb0d6;}
78
- .card.rank2{border-left:6px solid #6f9c8f;}
79
- .card.rank3{border-left:6px solid #d6a86f;}
80
- .card.rank4{border-left:6px solid #d68f8f;}
81
- .card.rank5{border-left:6px solid #b0d67f;}
82
- .card.rank6{border-left:6px solid #d6cf6f;}
83
- .card.rank7{border-left:6px solid #6fb6d6;}
84
- .card.rank8{border-left:6px solid #c98fd6;}
85
- .card.rank9{border-left:6px solid #d6a0c0;}
86
- .card.rank10{border-left:6px solid var(--gold);}
87
- .card.heal{border-left:6px solid var(--heal);}
88
- .card.harm{border-left:6px solid var(--harm);}
140
+ box-shadow:var(--shadow);
141
+ }
142
+ .card.rank0{border-left:5px solid var(--rank0);}
143
+ .card.rank1{border-left:5px solid var(--rank1);}
144
+ .card.rank2{border-left:5px solid var(--rank2);}
145
+ .card.rank3{border-left:5px solid var(--rank3);}
146
+ .card.rank4{border-left:5px solid var(--rank4);}
147
+ .card.rank5{border-left:5px solid var(--rank5);}
148
+ .card.rank6{border-left:5px solid var(--rank6);}
149
+ .card.rank7{border-left:5px solid var(--rank7);}
150
+ .card.rank8{border-left:5px solid var(--rank8);}
151
+ .card.rank9{border-left:5px solid var(--rank9);}
152
+ .card.rank10{border-left:5px solid var(--rank10);}
153
+ .card.heal{border-left:5px solid var(--heal);}
154
+ .card.harm{border-left:5px solid var(--harm);}
89
155
 
90
156
  .spell-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;}
91
- .spell-name{font-size:1.3rem;font-weight:700;color:var(--ink);margin:0;}
157
+ .spell-name{font-size:1.25rem;font-weight:700;color:var(--ink);margin:0;}
92
158
  .rankpill{
93
- background:var(--panel2);border:1px solid var(--gold-dim);color:var(--gold);
94
- border-radius:20px;padding:3px 12px;font-size:.85rem;font-weight:700;white-space:nowrap;
159
+ background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--accent-text);
160
+ border-radius:20px;padding:3px 12px;font-size:.82rem;font-weight:700;white-space:nowrap;
95
161
  }
96
162
  .actions{
97
163
  display:inline-flex;align-items:center;gap:6px;
98
- background:#11305a;border:1px solid var(--accent);color:#cfe5f7;
164
+ background:var(--info-bg);border:1px solid var(--info);color:var(--info-ink);
99
165
  border-radius:8px;padding:2px 10px;font-size:.85rem;font-weight:700;margin:8px 8px 4px 0;
100
166
  }
101
167
  .save-tag{
102
- display:inline-block;background:#3a1d1d;border:1px solid #8a4b4b;color:#f0c0c0;
168
+ display:inline-block;
169
+ background:color-mix(in srgb, var(--harm) 15%, var(--surface));
170
+ border:1px solid color-mix(in srgb, var(--harm) 45%, var(--line));
171
+ color:var(--harm);
103
172
  border-radius:8px;padding:2px 10px;font-size:.85rem;font-weight:700;margin:8px 8px 4px 0;
104
173
  }
105
- .meta{font-size:.92rem;color:var(--muted);margin:6px 0;}
174
+ .meta{font-size:.9rem;color:var(--muted);margin:6px 0;}
106
175
  .meta b{color:var(--ink);font-weight:600;}
107
176
  .traits{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0;}
108
177
  .trait{
109
- background:#241810;border:1px solid var(--gold-dim);color:var(--gold);
110
- font-size:.74rem;text-transform:uppercase;letter-spacing:.4px;
178
+ background:var(--surface-2);border:1px solid var(--line);color:var(--muted);
179
+ font-size:.72rem;text-transform:uppercase;letter-spacing:.4px;
111
180
  padding:2px 8px;border-radius:6px;
112
181
  }
113
- .desc{font-size:1.02rem;line-height:1.55;margin-top:8px;}
182
+ .desc{font-size:1rem;line-height:1.55;margin-top:8px;}
114
183
  .heighten{
115
- margin-top:10px;padding:10px 12px;background:var(--panel2);
116
- border-radius:10px;font-size:.95rem;color:var(--muted);
184
+ margin-top:10px;padding:10px 12px;background:var(--surface-2);
185
+ border-radius:10px;font-size:.92rem;color:var(--muted);
117
186
  }
118
- .heighten b{color:var(--gold);}
187
+ .heighten b{color:var(--accent-text);}
119
188
 
120
189
  /* ---- Buttons / inputs ---- */
121
190
  button{font-family:inherit;font-size:1.05rem;cursor:pointer;}
122
191
  .btn{
123
- display:block;width:100%;padding:16px;border-radius:14px;border:none;
124
- background:var(--gold);color:#2a1c0c;font-weight:800;font-size:1.15rem;
125
- margin:14px 0;letter-spacing:.3px;
192
+ display:block;width:100%;padding:15px;border-radius:12px;border:none;
193
+ background:var(--accent);color:var(--accent-ink);font-weight:800;font-size:1.1rem;
194
+ margin:14px 0;letter-spacing:.2px;box-shadow:var(--shadow);
126
195
  }
127
- .btn.secondary{background:var(--panel2);color:var(--gold);border:1px solid var(--gold-dim);}
196
+ .btn.secondary{background:var(--surface-2);color:var(--accent-text);border:1px solid var(--accent-dim);box-shadow:none;}
128
197
  .btn:active{transform:translateY(1px);}
129
- .linklike{background:none;border:none;color:var(--accent);font-weight:700;text-decoration:underline;padding:0;font-size:inherit;}
198
+ .linklike{background:none;border:none;color:var(--info);font-weight:700;text-decoration:underline;padding:0;font-size:inherit;}
130
199
 
131
200
  label.field{display:block;margin:14px 0;}
132
- label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:1.02rem;}
201
+ label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:1rem;}
133
202
  label.field .hint{display:block;color:var(--muted);font-size:.82rem;font-weight:400;margin-bottom:6px;}
203
+ input,select,textarea{accent-color:var(--accent);}
134
204
  input,select{
135
- width:100%;padding:14px;font-size:1.1rem;border-radius:12px;
136
- background:var(--panel2);color:var(--ink);border:1px solid var(--gold-dim);
205
+ width:100%;padding:13px;font-size:1.05rem;border-radius:12px;
206
+ background:var(--surface-2);color:var(--ink);border:1px solid var(--line);
137
207
  appearance:none;-webkit-appearance:none;
138
208
  }
139
- select{background-image:linear-gradient(45deg,transparent 50%,var(--gold) 50%),linear-gradient(135deg,var(--gold) 50%,transparent 50%);
209
+ input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);}
210
+ select{background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
140
211
  background-position:calc(100% - 20px) center,calc(100% - 14px) center;
141
212
  background-size:6px 6px,6px 6px;background-repeat:no-repeat;padding-right:40px;}
213
+ input[type="color"]{padding:4px;height:44px;cursor:pointer;}
142
214
  .row{display:flex;gap:10px;}
143
215
  .row > *{flex:1;}
144
216
 
145
217
  .seg{display:flex;gap:8px;margin:6px 0;}
146
218
  .seg button{
147
- flex:1;padding:14px;border-radius:12px;border:1px solid var(--gold-dim);
148
- background:var(--panel2);color:var(--ink);font-weight:700;
219
+ flex:1;padding:13px;border-radius:12px;border:1px solid var(--line);
220
+ background:var(--surface-2);color:var(--ink);font-weight:700;
221
+ display:inline-flex;align-items:center;justify-content:center;gap:8px;
149
222
  }
150
- .seg button.on.heal{background:var(--heal);color:#0c2415;border-color:var(--heal);}
151
- .seg button.on.harm{background:var(--harm);color:#2a0c12;border-color:var(--harm);}
223
+ .seg button.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
224
+ .seg button.on.heal{background:var(--heal);color:#08140d;border-color:var(--heal);}
225
+ .seg button.on.harm{background:var(--harm);color:#1c0709;border-color:var(--harm);}
152
226
 
153
227
  /* ---- Slot picker rows ---- */
154
228
  .slotgroup{margin:18px 0;}
155
- .slotgroup h3{color:var(--gold);font-size:1.1rem;border-bottom:1px solid var(--line);padding-bottom:6px;}
229
+ .slotgroup h3{color:var(--accent-text);font-size:1.05rem;border-bottom:1px solid var(--line);padding-bottom:6px;display:flex;align-items:center;gap:8px;}
156
230
  .slotrow{display:flex;align-items:center;gap:10px;margin:10px 0;}
157
231
  .slotrow .num{
158
232
  width:34px;height:34px;flex:0 0 34px;border-radius:50%;
159
- background:var(--panel2);border:1px solid var(--gold-dim);color:var(--gold);
233
+ background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--accent-text);
160
234
  display:flex;align-items:center;justify-content:center;font-weight:800;
161
235
  }
236
+ .slotrow .num .icn{width:1.1rem;height:1.1rem;}
162
237
  .slotrow select{flex:1;}
163
238
  .prep-slot{margin:10px 0;}
164
239
  .prep-slot .slotrow{margin:0;}
165
240
  .prep-preview{margin:6px 0 0 44px;}
166
241
  .prep-preview-inner{
167
- background:var(--panel2);border:1px solid var(--line);border-radius:10px;
242
+ background:var(--surface-2);border:1px solid var(--line);border-radius:10px;
168
243
  padding:10px 12px;
169
244
  }
170
245
  .prep-preview .meta{font-size:.85rem;margin:2px 0;}
@@ -173,123 +248,128 @@ select{background-image:linear-gradient(45deg,transparent 50%,var(--gold) 50%),l
173
248
 
174
249
  /* ---- Today cast cards ---- */
175
250
  .cast-card{
176
- background:var(--panel);border:1px solid var(--line);border-radius:14px;
177
- padding:14px;margin:12px 0;border-left:6px solid var(--line);
178
- }
179
- .cast-card.rank0{border-left-color:#b58fd6;}
180
- .cast-card.rank1{border-left-color:#7fb0d6;}
181
- .cast-card.rank2{border-left-color:#6f9c8f;}
182
- .cast-card.rank3{border-left-color:#d6a86f;}
183
- .cast-card.rank4{border-left-color:#d68f8f;}
184
- .cast-card.rank5{border-left-color:#b0d67f;}
185
- .cast-card.rank6{border-left-color:#d6cf6f;}
186
- .cast-card.rank7{border-left-color:#6fb6d6;}
187
- .cast-card.rank8{border-left-color:#c98fd6;}
188
- .cast-card.rank9{border-left-color:#d6a0c0;}
189
- .cast-card.rank10{border-left-color:var(--gold);}
251
+ background:var(--surface);border:1px solid var(--line);border-radius:14px;
252
+ padding:14px;margin:12px 0;border-left:5px solid var(--line);box-shadow:var(--shadow);
253
+ }
254
+ .cast-card.rank0{border-left-color:var(--rank0);}
255
+ .cast-card.rank1{border-left-color:var(--rank1);}
256
+ .cast-card.rank2{border-left-color:var(--rank2);}
257
+ .cast-card.rank3{border-left-color:var(--rank3);}
258
+ .cast-card.rank4{border-left-color:var(--rank4);}
259
+ .cast-card.rank5{border-left-color:var(--rank5);}
260
+ .cast-card.rank6{border-left-color:var(--rank6);}
261
+ .cast-card.rank7{border-left-color:var(--rank7);}
262
+ .cast-card.rank8{border-left-color:var(--rank8);}
263
+ .cast-card.rank9{border-left-color:var(--rank9);}
264
+ .cast-card.rank10{border-left-color:var(--rank10);}
190
265
  .cast-card.heal{border-left-color:var(--heal);}
191
266
  .cast-card.harm{border-left-color:var(--harm);}
192
- .cast-card.spent{opacity:.4;}
267
+ .cast-card.spent{opacity:.45;}
193
268
  .cast-top{display:flex;justify-content:space-between;align-items:center;gap:10px;}
194
- .cast-top .nm{font-size:1.2rem;font-weight:700;}
269
+ .cast-top .nm{font-size:1.15rem;font-weight:700;}
195
270
  .uses{display:flex;gap:6px;align-items:center;}
196
- .pip{width:26px;height:26px;border-radius:50%;border:2px solid var(--gold);display:inline-block;}
197
- .pip.full{background:var(--gold);}
271
+ .pip{width:24px;height:24px;border-radius:50%;border:2px solid var(--accent);display:inline-block;}
272
+ .pip.full{background:var(--accent);}
198
273
  .pip.spent{background:transparent;opacity:.35;}
199
274
  .castbtn{
200
- padding:10px 16px;border-radius:10px;border:none;background:var(--gold);
201
- color:#2a1c0c;font-weight:800;font-size:1rem;
275
+ padding:10px 16px;border-radius:10px;border:none;background:var(--accent);
276
+ color:var(--accent-ink);font-weight:800;font-size:1rem;
202
277
  }
203
- .castbtn.zero{background:var(--panel2);color:var(--muted);}
278
+ .castbtn.zero{background:var(--surface-2);color:var(--muted);}
204
279
  .unlim{font-size:.85rem;color:var(--heal);font-weight:700;}
205
280
  .detailsbtn{
206
- background:none;border:none;color:var(--accent);font-weight:700;
281
+ background:none;border:none;color:var(--info);font-weight:700;
207
282
  padding:8px 0 0;font-size:.95rem;text-decoration:underline;
208
283
  }
209
284
 
210
285
  .susbtn{
211
- padding:10px 14px;border-radius:10px;border:1px solid var(--accent);
212
- background:#11305a;color:#cfe5f7;font-weight:800;font-size:.95rem;
286
+ padding:10px 14px;border-radius:10px;border:1px solid var(--info);
287
+ background:var(--info-bg);color:var(--info-ink);font-weight:800;font-size:.95rem;
213
288
  }
214
- .susbtn.on{background:var(--accent);color:#0c1d2a;}
289
+ .susbtn.on{background:var(--info);color:var(--surface);}
215
290
  .susrow{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--line);}
216
291
  .susrow:first-of-type{border-top:none;}
217
292
  .susrow span{font-weight:700;}
218
293
 
219
294
  .dmgline{margin:4px 0 2px;}
220
295
  .dmgchip{
221
- display:inline-block;background:#2a1d14;border:1px solid var(--gold-dim);
222
- color:var(--gold);font-weight:800;font-size:.95rem;border-radius:8px;padding:2px 10px;
296
+ display:inline-block;background:var(--surface-2);border:1px solid var(--line);
297
+ color:var(--ink);font-weight:800;font-size:.92rem;border-radius:8px;padding:2px 10px;
223
298
  }
299
+ .dmgchip.dmg{background:color-mix(in srgb, var(--harm) 15%, var(--surface));border-color:color-mix(in srgb, var(--harm) 40%, var(--line));color:var(--harm);}
300
+ .dmgchip.heal{background:color-mix(in srgb, var(--heal) 15%, var(--surface));border-color:color-mix(in srgb, var(--heal) 40%, var(--line));color:var(--heal);}
224
301
 
225
302
  .empty{text-align:center;color:var(--muted);padding:40px 20px;}
226
303
 
227
304
  .filterbar{display:flex;gap:8px;margin:6px 0 14px;flex-wrap:wrap;}
228
305
  .chip{
229
- padding:8px 14px;border-radius:20px;border:1px solid var(--gold-dim);
230
- background:var(--panel2);color:var(--muted);font-weight:700;font-size:.9rem;
306
+ padding:8px 14px;border-radius:20px;border:1px solid var(--line);
307
+ background:var(--surface-2);color:var(--muted);font-weight:700;font-size:.88rem;
308
+ display:inline-flex;align-items:center;gap:6px;
231
309
  }
232
- .chip.on{background:var(--gold);color:#2a1c0c;border-color:var(--gold);}
310
+ .chip.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
233
311
  #search{margin-bottom:8px;}
234
312
  #filterToggle{margin:0 0 8px;}
235
- #browseFilters{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin:0 0 12px;}
313
+ #browseFilters{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin:0 0 12px;}
236
314
  .fgroup{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
237
315
  .fgroup .flabel{font-size:.8rem;color:var(--muted);font-weight:700;min-width:58px;}
238
316
 
239
317
  .sectionhead{
240
- color:var(--gold);font-size:1.15rem;font-weight:800;margin:22px 0 4px;
318
+ color:var(--accent-text);font-size:1.1rem;font-weight:800;margin:22px 0 4px;
241
319
  display:flex;align-items:center;gap:10px;
242
320
  }
243
- .sectionhead .count{font-size:.85rem;color:var(--muted);font-weight:600;}
321
+ .sectionhead .count{font-size:.82rem;color:var(--muted);font-weight:600;}
244
322
  .divider{height:1px;background:var(--line);margin:4px 0 6px;}
245
323
 
246
324
  .banner{
247
- background:#2a2412;border:1px solid var(--gold-dim);border-radius:12px;
248
- padding:12px 14px;margin:12px 0;color:var(--muted);font-size:.9rem;
325
+ background:var(--accent-soft);border:1px solid var(--accent-dim);border-radius:12px;
326
+ padding:12px 14px;margin:12px 0;color:var(--ink);font-size:.9rem;
249
327
  }
250
- .banner b{color:var(--gold);}
328
+ .banner b{color:var(--accent-text);}
251
329
 
252
- details.help{background:var(--panel);border:1px solid var(--line);border-radius:14px;margin:12px 0;padding:0 16px;}
253
- details.help summary{padding:16px 0;font-weight:700;font-size:1.1rem;color:var(--gold);cursor:pointer;list-style:none;}
330
+ details.help{background:var(--surface);border:1px solid var(--line);border-radius:14px;margin:12px 0;padding:0 16px;}
331
+ details.help summary{padding:16px 0;font-weight:700;font-size:1.05rem;color:var(--accent-text);cursor:pointer;list-style:none;}
254
332
  details.help summary::-webkit-details-marker{display:none;}
255
333
  details.help summary::after{content:"+";float:right;color:var(--muted);}
256
334
  details.help[open] summary::after{content:"-";}
257
335
  details.help .body{padding:0 0 16px;font-size:1rem;line-height:1.6;color:var(--ink);}
258
- details.help .body b{color:var(--gold);}
336
+ details.help .body b{color:var(--accent-text);}
259
337
 
260
338
  /* ---- Class picker ---- */
261
339
  .classcard{
262
340
  display:flex;align-items:center;gap:14px;width:100%;text-align:left;
263
- background:var(--panel);border:1px solid var(--line);border-left:6px solid var(--gold);
264
- border-radius:var(--radius);padding:16px;margin:12px 0;color:var(--ink);
341
+ background:var(--surface);border:1px solid var(--line);border-left:4px solid var(--accent);
342
+ border-radius:var(--radius);padding:16px;margin:12px 0;color:var(--ink);box-shadow:var(--shadow);
265
343
  }
266
344
  .classcard:active{transform:translateY(1px);}
267
- .classcard .ic{font-size:2.2rem;line-height:1;}
345
+ .classcard .ic{font-size:2rem;line-height:1;display:inline-flex;color:var(--accent-text);}
268
346
  .classcard .txt{flex:1;}
269
- .classcard .nm{font-size:1.3rem;font-weight:800;color:var(--gold);}
270
- .classcard .tl{font-size:.9rem;color:var(--muted);margin-top:2px;}
271
- .classcard .arr{color:var(--gold-dim);font-size:1.4rem;font-weight:800;}
347
+ .classcard .nm{font-size:1.25rem;font-weight:800;color:var(--ink);}
348
+ .classcard .tl{font-size:.88rem;color:var(--muted);margin-top:2px;}
349
+ .classcard .arr{color:var(--muted);font-size:1.3rem;font-weight:800;}
272
350
  .previewtag{
273
351
  font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;vertical-align:middle;
274
- background:#3a1d1d;border:1px solid #8a4b4b;color:#f0c0c0;border-radius:6px;padding:1px 6px;font-weight:700;
352
+ background:color-mix(in srgb, var(--harm) 15%, var(--surface));
353
+ border:1px solid color-mix(in srgb, var(--harm) 40%, var(--line));
354
+ color:var(--harm);border-radius:6px;padding:1px 6px;font-weight:700;
275
355
  }
276
356
 
277
357
  /* ---- Browse compact rows (tap to expand) ---- */
278
358
  .browse-row{
279
- background:var(--panel);border:1px solid var(--line);border-left:5px solid var(--line);
359
+ background:var(--surface);border:1px solid var(--line);border-left:4px solid var(--line);
280
360
  border-radius:12px;margin:8px 0;overflow:hidden;
281
361
  }
282
- .browse-row.rank0{border-left-color:#b58fd6;}
283
- .browse-row.rank1{border-left-color:#7fb0d6;}
284
- .browse-row.rank2{border-left-color:#6f9c8f;}
285
- .browse-row.rank3{border-left-color:#d6a86f;}
286
- .browse-row.rank4{border-left-color:#d68f8f;}
287
- .browse-row.rank5{border-left-color:#b0d67f;}
288
- .browse-row.rank6{border-left-color:#d6cf6f;}
289
- .browse-row.rank7{border-left-color:#6fb6d6;}
290
- .browse-row.rank8{border-left-color:#c98fd6;}
291
- .browse-row.rank9{border-left-color:#d6a0c0;}
292
- .browse-row.rank10{border-left-color:var(--gold);}
362
+ .browse-row.rank0{border-left-color:var(--rank0);}
363
+ .browse-row.rank1{border-left-color:var(--rank1);}
364
+ .browse-row.rank2{border-left-color:var(--rank2);}
365
+ .browse-row.rank3{border-left-color:var(--rank3);}
366
+ .browse-row.rank4{border-left-color:var(--rank4);}
367
+ .browse-row.rank5{border-left-color:var(--rank5);}
368
+ .browse-row.rank6{border-left-color:var(--rank6);}
369
+ .browse-row.rank7{border-left-color:var(--rank7);}
370
+ .browse-row.rank8{border-left-color:var(--rank8);}
371
+ .browse-row.rank9{border-left-color:var(--rank9);}
372
+ .browse-row.rank10{border-left-color:var(--rank10);}
293
373
  .browse-row-head{
294
374
  display:flex;flex-direction:column;align-items:flex-start;gap:2px;
295
375
  width:100%;text-align:left;background:none;border:none;color:var(--ink);
@@ -297,75 +377,83 @@ details.help .body b{color:var(--gold);}
297
377
  }
298
378
  .browse-row-head::after{content:"▾";position:absolute;right:14px;top:14px;color:var(--muted);}
299
379
  .browse-row-head.open::after{content:"▴";}
300
- .browse-row-head .brn{font-size:1.1rem;font-weight:700;}
380
+ .browse-row-head .brn{font-size:1.05rem;font-weight:700;}
301
381
  .browse-row-head .brmeta{font-size:.82rem;color:var(--muted);}
302
- .brfocus{font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;color:#c0a0e0;border:1px solid #6f5b8a;border-radius:5px;padding:0 5px;vertical-align:middle;}
382
+ .brfocus{font-size:.62rem;text-transform:uppercase;letter-spacing:.5px;color:var(--accent-text);border:1px solid var(--accent-dim);border-radius:5px;padding:0 5px;vertical-align:middle;}
303
383
  .formerly{font-size:.72rem;font-style:italic;font-weight:600;color:var(--muted);white-space:nowrap;}
304
- .ritualtag{font-size:.7rem;text-transform:uppercase;letter-spacing:.5px;font-weight:800;background:#3a1d3a;border:1px solid #8a4b8a;color:#e7c6e7;border-radius:6px;padding:1px 7px;}
384
+ .ritualtag{font-size:.7rem;text-transform:uppercase;letter-spacing:.5px;font-weight:800;background:var(--accent-soft);border:1px solid var(--accent-dim);color:var(--accent-text);border-radius:6px;padding:1px 7px;}
305
385
  .browse-card{padding:0 12px 4px;}
306
386
  .browse-card .card{margin:0 0 10px;}
307
387
 
308
388
  /* ---- Header menu button + character menu ---- */
309
389
  .tophead{display:flex;align-items:center;justify-content:space-between;gap:10px;}
310
390
  .menubtn{
311
- flex:0 0 auto;background:var(--panel2);border:1px solid var(--gold-dim);
312
- border-radius:12px;width:44px;height:44px;font-size:1.3rem;line-height:1;color:var(--gold);
391
+ flex:0 0 auto;background:var(--surface-2);border:1px solid var(--line);
392
+ border-radius:12px;width:44px;height:44px;font-size:1.3rem;line-height:1;color:var(--accent-text);
393
+ display:inline-flex;align-items:center;justify-content:center;
313
394
  }
314
- .classcard.activechar{border-left-color:var(--gold);background:#332615;}
395
+ .classcard.activechar{border-left-color:var(--accent);background:var(--accent-soft);}
315
396
  textarea{
316
- width:100%;padding:14px;font-size:1rem;border-radius:12px;line-height:1.4;
317
- background:var(--panel2);color:var(--ink);border:1px solid var(--gold-dim);
397
+ width:100%;padding:13px;font-size:1rem;border-radius:12px;line-height:1.4;
398
+ background:var(--surface-2);color:var(--ink);border:1px solid var(--line);
318
399
  font-family:inherit;resize:vertical;margin:6px 0;
319
400
  }
320
401
 
402
+ /* ---- Appearance / custom palette ---- */
403
+ .swatchrow{display:flex;align-items:center;gap:10px;margin:8px 0;}
404
+ .swatchrow label{flex:1;font-weight:600;font-size:.95rem;}
405
+ .swatchrow input[type="color"]{flex:0 0 56px;width:56px;}
406
+
321
407
  /* ---- Repertoire builder (spontaneous) ---- */
322
408
  .rep-row .slotrow{gap:8px;}
323
409
  .sigstar{
324
410
  width:38px;height:38px;flex:0 0 38px;border-radius:10px;
325
- background:var(--panel2);border:1px solid var(--gold-dim);color:var(--muted);
411
+ background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--muted);
326
412
  font-size:1.2rem;line-height:1;display:flex;align-items:center;justify-content:center;
327
413
  }
328
- .sigstar.on{background:var(--gold);color:#2a1c0c;border-color:var(--gold);}
414
+ .sigstar.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
329
415
  .rmrow{
330
416
  width:38px;height:38px;flex:0 0 38px;border-radius:10px;
331
- background:var(--panel2);border:1px solid var(--line);color:var(--muted);font-weight:700;
417
+ background:var(--surface-2);border:1px solid var(--line);color:var(--muted);font-weight:700;
332
418
  }
333
- .focus-checklist{max-height:320px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--panel2);}
419
+ .focus-checklist{max-height:320px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--surface-2);}
334
420
  .fcheck{display:flex;align-items:center;gap:10px;padding:11px 12px;border-bottom:1px solid var(--line);cursor:pointer;}
335
421
  .fcheck:last-child{border-bottom:none;}
336
422
  .fcheck input{width:22px;height:22px;flex:0 0 22px;}
337
423
  .fcheck .fcname{flex:1;font-weight:600;}
338
- .fcheck .fcrank{font-size:.75rem;color:var(--muted);background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:1px 7px;}
424
+ .fcheck .fcrank{font-size:.75rem;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:1px 7px;}
339
425
 
340
426
  .btn.addrow{
341
427
  display:inline-block;width:auto;padding:10px 16px;margin:4px 0 0 44px;
342
- font-size:.95rem;font-weight:700;border-radius:10px;
343
- background:var(--panel2);color:var(--gold);border:1px solid var(--gold-dim);
428
+ font-size:.95rem;font-weight:700;border-radius:10px;box-shadow:none;
429
+ background:var(--surface-2);color:var(--accent-text);border:1px solid var(--accent-dim);
344
430
  }
345
431
  .slottag{
346
- font-size:.85rem;background:var(--panel2);border:1px solid var(--gold-dim);
347
- border-radius:6px;padding:1px 6px;margin-right:4px;
432
+ display:inline-flex;align-items:center;gap:4px;
433
+ font-size:.85rem;background:var(--surface-2);border:1px solid var(--accent-dim);
434
+ color:var(--accent-text);border-radius:6px;padding:1px 6px;margin-right:4px;
348
435
  }
436
+ .slottag .icn{width:.95rem;height:.95rem;}
349
437
 
350
438
  /* ---- Bottom nav ---- */
351
439
  nav.bottom{
352
440
  position:fixed;bottom:0;left:0;right:0;z-index:30;
353
- background:#241810;border-top:2px solid var(--gold-dim);
441
+ background:var(--surface);border-top:1px solid var(--line);
354
442
  display:flex;justify-content:space-around;
355
443
  padding:6px 4px calc(6px + env(safe-area-inset-bottom));
356
444
  }
357
445
  nav.bottom button{
358
446
  flex:1;background:none;border:none;color:var(--muted);
359
- display:flex;flex-direction:column;align-items:center;gap:2px;
360
- padding:8px 2px;font-size:.72rem;font-weight:700;
447
+ display:flex;flex-direction:column;align-items:center;gap:3px;
448
+ padding:8px 2px;font-size:.7rem;font-weight:700;
361
449
  }
362
- nav.bottom button .ic{font-size:1.5rem;line-height:1;}
363
- nav.bottom button.on{color:var(--gold);}
450
+ nav.bottom button .ic{font-size:1.4rem;line-height:1;display:inline-flex;}
451
+ nav.bottom button.on{color:var(--accent-text);}
364
452
  .toast{
365
453
  position:fixed;bottom:96px;left:50%;transform:translateX(-50%);
366
- background:var(--gold);color:#2a1c0c;font-weight:800;padding:12px 20px;
454
+ background:var(--accent);color:var(--accent-ink);font-weight:800;padding:12px 20px;
367
455
  border-radius:30px;z-index:50;opacity:0;transition:opacity .25s;pointer-events:none;
368
- box-shadow:0 4px 16px rgba(0,0,0,.5);
456
+ box-shadow:0 4px 16px rgba(0,0,0,.35);
369
457
  }
370
458
  .toast.show{opacity:1;}
371
459
 
@@ -375,8 +463,8 @@ nav.bottom button.on{color:var(--gold);}
375
463
 
376
464
  <header class="top hide">
377
465
  <div class="tophead">
378
- <h1><span id="charIcon">📖</span> <span id="charNameHead">Spellbook</span></h1>
379
- <button class="menubtn" onclick="openMenu()" title="Characters &amp; backup" aria-label="Characters menu">👤</button>
466
+ <h1><span id="charIcon"></span> <span id="charNameHead">Spellbook</span></h1>
467
+ <button class="menubtn" onclick="openMenu()" title="Characters &amp; backup" aria-label="Characters menu"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="8" r="3.4"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/></svg></button>
380
468
  </div>
381
469
  <div class="sub" id="charSub">Pathfinder 2e (Remaster)</div>
382
470
  <div class="statbar" id="statbar">
@@ -391,11 +479,14 @@ nav.bottom button.on{color:var(--gold);}
391
479
 
392
480
  <!-- ================= CHARACTER MENU ================= -->
393
481
  <section id="view-menu" class="wrap hide">
394
- <h2>📲 Use offline</h2>
482
+ <h2>Appearance</h2>
483
+ <div id="appearancePanel"></div>
484
+
485
+ <h2 style="margin-top:24px">Use offline</h2>
395
486
  <p class="meta">Everything you do — your characters and prepared spells — is saved <b>on this device only</b>, in your browser. Nothing is sent anywhere. Keep a copy so it always works, even with no signal:</p>
396
487
  <div class="row">
397
- <button class="btn" onclick="downloadOffline()">📥 Save offline copy</button>
398
- <button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()">⬇ Install app</button>
488
+ <button class="btn" onclick="downloadOffline()">Save offline copy</button>
489
+ <button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()">Install app</button>
399
490
  </div>
400
491
  <p class="meta" id="offlineHint" style="font-size:.8rem">“Save offline copy” downloads this whole spellbook as a single file you can open any time — no internet needed. “Install app” adds it to your home screen like a normal app (appears when your browser supports it).</p>
401
492
 
@@ -407,8 +498,8 @@ nav.bottom button.on{color:var(--gold);}
407
498
  <h2 style="margin-top:24px">Share &amp; back up</h2>
408
499
  <p class="meta">Export your <b>active</b> character to a code you can save or send to a friend. To load one, paste a code below and tap Import — it's added as a new character.</p>
409
500
  <div class="row">
410
- <button class="btn secondary" onclick="exportCharacter()">⬆ Export active</button>
411
- <button class="btn secondary" onclick="importCharacter()">⬇ Import code</button>
501
+ <button class="btn secondary" onclick="exportCharacter()">Export active</button>
502
+ <button class="btn secondary" onclick="importCharacter()">Import code</button>
412
503
  </div>
413
504
  <textarea id="menuIO" rows="3" placeholder="Character code appears here on Export — or paste one here, then tap Import."></textarea>
414
505
  <button class="btn secondary" onclick="closeMenu()">← Back</button>
@@ -428,7 +519,7 @@ nav.bottom button.on{color:var(--gold);}
428
519
  <!-- ================= PREPARE / REPERTOIRE ================= -->
429
520
  <section id="view-prepare" class="wrap hide">
430
521
  <p class="meta" id="prepClassLine"></p>
431
- <div class="banner hide" id="prepPreviewBanner">ℹ️ Spell slots, cantrips, and focus spells are fully tracked. A few subclass extras (blood magic, oracular curse, companions/familiars) stay on your character sheet — noted below where relevant.</div>
522
+ <div class="banner hide" id="prepPreviewBanner">Spell slots, cantrips, and focus spells are fully tracked. A few subclass extras (blood magic, oracular curse, companions/familiars) stay on your character sheet — noted below where relevant.</div>
432
523
 
433
524
  <h2>① Your character</h2>
434
525
  <p class="meta">Copy these from your character sheet.</p>
@@ -465,9 +556,9 @@ nav.bottom button.on{color:var(--gold);}
465
556
  <!-- ================= BROWSE ================= -->
466
557
  <section id="view-browse" class="wrap hide">
467
558
  <h2 id="browseTitle">All spells</h2>
468
- <input type="text" id="search" placeholder="🔍 Search spells…" oninput="renderBrowse()">
559
+ <input type="text" id="search" placeholder="Search spells…" oninput="renderBrowse()">
469
560
  <div class="filterbar" id="rankChips"></div>
470
- <button class="chip" id="filterToggle" onclick="toggleBrowseFilters()" aria-expanded="false">⚙ Filters</button>
561
+ <button class="chip" id="filterToggle" onclick="toggleBrowseFilters()" aria-expanded="false"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/><circle cx="9" cy="7" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="8" cy="17" r="2"/></svg> Filters</button>
471
562
  <div id="browseFilters" class="hide"></div>
472
563
  <div id="browseList"></div>
473
564
  </section>
@@ -482,10 +573,10 @@ nav.bottom button.on{color:var(--gold);}
482
573
  <div class="toast" id="toast"></div>
483
574
 
484
575
  <nav class="bottom hide">
485
- <button id="nav-today" class="on" onclick="go('today')"><span class="ic">📜</span>Cast Today</button>
486
- <button id="nav-prepare" onclick="go('prepare')"><span class="ic">🌅</span>Prepare</button>
487
- <button id="nav-browse" onclick="go('browse')"><span class="ic">📖</span>All Spells</button>
488
- <button id="nav-help" onclick="go('help')"><span class="ic">❓</span>Help</button>
576
+ <button id="nav-today" class="on" onclick="go('today')"><span class="ic"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="3.5" width="12.5" height="17" rx="2"/><path d="M4.5 7v11.5a2 2 0 0 0 2 2h9.5"/></svg></span>Cast Today</button>
577
+ <button id="nav-prepare" onclick="go('prepare')"><span class="ic"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 18.5h18"/><path d="M6.5 18.5a5.5 5.5 0 0 1 11 0"/><path d="M12 3v3M4.2 8.7l1.7 1.7M19.8 8.7l-1.7 1.7"/></svg></span>Prepare</button>
578
+ <button id="nav-browse" onclick="go('browse')"><span class="ic"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5.2A2 2 0 0 1 6 3.2h5.2v15.6H6a2 2 0 0 0-2 2z"/><path d="M20 5.2a2 2 0 0 0-2-2h-5.2v15.6H18a2 2 0 0 1 2 2z"/></svg></span>All Spells</button>
579
+ <button id="nav-help" onclick="go('help')"><span class="ic"><svg class="icn" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9.4 9.4a2.6 2.6 0 1 1 3.6 2.4c-.9.5-1.1 1-1.1 1.9"/><path d="M12 16.7v.01"/></svg></span>Help</button>
489
580
  </nav>
490
581
 
491
582
  <script>
@@ -58710,6 +58801,46 @@ function psychicSlots(level){
58710
58801
  return slots;
58711
58802
  }
58712
58803
 
58804
+ /* ============================================================
58805
+ INLINE SVG ICON REGISTRY
58806
+ Thin line icons (24x24, stroke = currentColor) so they inherit the
58807
+ active accent colour. Keyed by name; classes/features reference a key
58808
+ instead of an emoji. iconSvg() wraps the path data in an <svg>.
58809
+ ============================================================ */
58810
+ const ICONS={
58811
+ /* classes */
58812
+ cleric:'<circle cx="12" cy="12" r="3.4"/><path d="M12 3v2.6M12 18.4V21M3 12h2.6M18.4 12H21M5.6 5.6l1.9 1.9M16.5 16.5l1.9 1.9M18.4 5.6l-1.9 1.9M7.5 16.5l-1.9 1.9"/>',
58813
+ wizard:'<path d="M12 3.5 4.5 19.5h15z"/><circle cx="12" cy="13.5" r="1.9"/>',
58814
+ druid:'<path d="M5 19.5C5 11 10.5 5 19 4.5 19 13 13.5 19 5 19.5z"/><path d="M11.5 12 5 19.5"/>',
58815
+ witch:'<path d="M17.5 15.2A6 6 0 1 1 11.3 5a5 5 0 0 0 6.2 10.2z"/><path d="M18.3 3.6l.8 1.9 1.9.8-1.9.8-.8 1.9-.8-1.9-1.9-.8 1.9-.8z"/>',
58816
+ sorcerer:'<path d="M12 3c2.8 3.6 4.6 5.6 4.6 8.4a4.6 4.6 0 0 1-9.2 0c0-1.2.6-2.2 1.6-3.1.3 1.1 1 1.6 1.6 1.6C9.4 8.4 10.3 5.7 12 3z"/>',
58817
+ bard:'<path d="M9.5 17.5V6.2l9-2v9.6"/><circle cx="6.8" cy="17.5" r="2.4"/><circle cx="15.8" cy="15.8" r="2.4"/>',
58818
+ oracle:'<path d="M2.5 12S6 6 12 6s9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6z"/><circle cx="12" cy="12" r="2.4"/>',
58819
+ magus:'<path d="M17.5 3.5 20.5 6.5 12 15l-3 1 1-3z"/><path d="M8 14l-4 4"/><path d="M5.5 15.5l3 3"/>',
58820
+ summoner:'<circle cx="9.2" cy="12" r="4.8"/><circle cx="14.8" cy="12" r="4.8"/>',
58821
+ psychic:'<path d="M12 12a1.5 1.5 0 1 0 1.6-1.5A3.6 3.6 0 1 0 8.4 12 5.7 5.7 0 1 0 17.7 7.7"/>',
58822
+ animist:'<path d="M12 3c2 2.5 3.1 4 3.1 6a3.1 3.1 0 0 1-6.2 0c0-1 .5-2 1.5-2.9.3.8.8 1.2 1.3 1.2C11 6 11 5 12 3z"/><path d="M12 12v6"/><path d="M9 21h6"/>',
58823
+ /* navigation */
58824
+ today:'<rect x="7" y="3.5" width="12.5" height="17" rx="2"/><path d="M4.5 7v11.5a2 2 0 0 0 2 2h9.5"/>',
58825
+ prepare:'<path d="M3 18.5h18"/><path d="M6.5 18.5a5.5 5.5 0 0 1 11 0"/><path d="M12 3v3M4.2 8.7l1.7 1.7M19.8 8.7l-1.7 1.7"/>',
58826
+ browse:'<path d="M4 5.2A2 2 0 0 1 6 3.2h5.2v15.6H6a2 2 0 0 0-2 2z"/><path d="M20 5.2a2 2 0 0 0-2-2h-5.2v15.6H18a2 2 0 0 1 2 2z"/>',
58827
+ help:'<circle cx="12" cy="12" r="9"/><path d="M9.4 9.4a2.6 2.6 0 1 1 3.6 2.4c-.9.5-1.1 1-1.1 1.9"/><path d="M12 16.7v.01"/>',
58828
+ /* features / utility */
58829
+ school:'<path d="M2.5 8.5 12 4.5l9.5 4-9.5 4z"/><path d="M6.5 10.5v4c0 1.5 11 1.5 11 0v-4"/><path d="M21.5 8.5v5"/>',
58830
+ bonded:'<path d="M9.3 12.4a3 3 0 0 1 0-4.2l1.9-1.9a3 3 0 0 1 4.2 4.2l-1 1"/><path d="M14.7 11.6a3 3 0 0 1 0 4.2l-1.9 1.9a3 3 0 0 1-4.2-4.2l1-1"/>',
58831
+ heal:'<path d="M12 5.5v13M5.5 12h13"/>',
58832
+ harm:'<path d="M12 3a7 7 0 0 0-7 7c0 2 1 3.6 2.2 4.6V17a1 1 0 0 0 1 1h7.6a1 1 0 0 0 1-1v-2.4C19 13.6 20 12 20 10a7 7 0 0 0-7-7z" transform="translate(-1 0)"/><circle cx="8.5" cy="10.5" r="1.1"/><circle cx="13.5" cy="10.5" r="1.1"/><path d="M9 18v2M11 18v2M13 18v2"/>',
58833
+ menu:'<circle cx="12" cy="8" r="3.4"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/>',
58834
+ filter:'<path d="M4 7h16M4 12h16M4 17h16"/><circle cx="9" cy="7" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="8" cy="17" r="2"/>',
58835
+ plus:'<path d="M12 5.5v13M5.5 12h13"/>',
58836
+ book:'<path d="M12 6.5C9.5 4.7 6 4.7 3.5 6v13c2.5-1.3 6-1.3 8.5.5 2.5-1.8 6-1.8 8.5-.5V6c-2.5-1.3-6-1.3-8.5.5z"/><path d="M12 6.5v13"/>',
58837
+ };
58838
+ function iconSvg(name, cls){
58839
+ return '<svg class="icn '+(cls||"")+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" '+
58840
+ 'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
58841
+ (ICONS[name]||"")+'</svg>';
58842
+ }
58843
+
58713
58844
  /* [minLevel, proficiency bonus] for spell DC / spell attack, highest first.
58714
58845
  Bonus = 2/4/6/8 for trained/expert/master/legendary.
58715
58846
  Wizard, Druid, Sorcerer, Bard, Oracle, Psychic and the Cloistered Cleric
@@ -58757,13 +58888,13 @@ const CORE_HELP=[
58757
58888
  ["Remaster vs. legacy (original) rules",
58758
58889
  "This app uses the current <b>Remaster</b> spell names and rules. If your group still plays the <b>original</b> (pre-Remaster) edition, most spells are identical — and renamed ones show a small <i>“formerly …”</i> note, so you can also <b>search by the old name</b> (e.g. searching <b>Magic Missile</b> finds <b>Force Barrage</b>). A few legacy spells that weren't changed are still listed under their original names too.<br><br>A few wording changes to keep in mind: spell <b>rank</b> = spell <b>level</b>; <b>vitality</b>/<b>void</b> damage = the old <b>positive</b>/<b>negative</b>; and <b>holy</b>/<b>unholy</b> replace the old <b>good</b>/<b>evil</b> alignment damage. The numbers and effects are the same."],
58759
58890
  ["Multiple characters, backup & sharing",
58760
- "Tap the <b>👤</b> button (top right) to keep <b>several characters</b> and switch between them — each remembers its own prepared spells. From there you can also <b>Export</b> a character to a code to back it up or send to a friend, and <b>Import</b> a code someone shares with you. Everything is stored on your device."],
58891
+ "Tap the <b>character</b> button (top right) to keep <b>several characters</b> and switch between them — each remembers its own prepared spells. From there you can also <b>Export</b> a character to a code to back it up or send to a friend, and <b>Import</b> a code someone shares with you. Everything is stored on your device."],
58761
58892
  ];
58762
58893
 
58763
58894
  const CLASSES={
58764
58895
  /* ---------------- CLERIC ---------------- */
58765
58896
  cleric:{
58766
- id:"cleric", name:"Cleric", icon:"",
58897
+ id:"cleric", name:"Cleric", icon:"cleric", color:"#d8a23a",
58767
58898
  tagline:"Divine prepared caster · the party's main healer",
58768
58899
  tradition:"divine", keyAbility:"Wisdom", keyAbilityShort:"Wis",
58769
58900
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
@@ -58786,63 +58917,59 @@ const CLASSES={
58786
58917
 
58787
58918
  /* ---------------- WIZARD ---------------- */
58788
58919
  wizard:{
58789
- id:"wizard", name:"Wizard", icon:"🪄",
58920
+ id:"wizard", name:"Wizard", icon:"wizard", color:"#7b6cf0",
58790
58921
  tagline:"Arcane prepared caster · the widest spell list",
58791
58922
  tradition:"arcane", keyAbility:"Intelligence", keyAbilityShort:"Int",
58792
58923
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58793
- preview:true,
58794
58924
  features:[
58795
- {type:"extraSlots", key:"school", icon:"🎓", label:"Arcane school slot",
58925
+ {type:"extraSlots", key:"school", icon:"school", label:"Arcane school slot",
58796
58926
  note:"One extra slot of each rank you can cast, which must hold a spell from your chosen school / curriculum."},
58797
- {type:"dailyResource", key:"bonded", icon:"🔗", label:"Drain Bonded Item", uses:1,
58927
+ {type:"dailyResource", key:"bonded", icon:"bonded", label:"Drain Bonded Item", uses:1,
58798
58928
  note:"Once per day, recast a spell you already cast today without spending its slot."},
58799
58929
  ],
58800
58930
  help:[
58801
58931
  ["What does a wizard do?",
58802
58932
  "You're an <b>Arcane</b> prepared caster with the broadest spell list in the game. You Prepare spells each morning from your spellbook, leaning on control and damage like <b>Fireball</b> plus utility."],
58803
58933
  ["Arcane school slot",
58804
- "Your school (curriculum) gives you <b>one extra prepared slot of every rank</b> you can cast. Fill the 🎓 slots with spells from your school. The app adds these slots automatically."],
58934
+ "Your school (curriculum) gives you <b>one extra prepared slot of every rank</b> you can cast. Fill the <b>school</b> slots with spells from your school. The app adds these slots automatically."],
58805
58935
  ["Drain Bonded Item",
58806
- "Once per day you can <b>Drain your Bonded Item</b> to recast a spell you've already cast that day, without spending another slot. Tick the 🔗 tracker on Cast Today when you use it."],
58936
+ "Once per day you can <b>Drain your Bonded Item</b> to recast a spell you've already cast that day, without spending another slot. Tick the <b>Drain Bonded Item</b> tracker on Cast Today when you use it."],
58807
58937
  ],
58808
58938
  },
58809
58939
 
58810
58940
  /* ---------------- DRUID ---------------- */
58811
58941
  druid:{
58812
- id:"druid", name:"Druid", icon:"🌿",
58942
+ id:"druid", name:"Druid", icon:"druid", color:"#4faf6d",
58813
58943
  tagline:"Primal prepared caster · nature's versatility",
58814
58944
  tradition:"primal", keyAbility:"Wisdom", keyAbilityShort:"Wis",
58815
58945
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58816
- preview:true,
58817
58946
  features:[{type:"note", title:"Druidic order", text:"Pick your order's focus spells (Heal Animal, Tempest Surge, Wild/Untamed Form…) in the Focus section below. Companion/familiar bookkeeping stays on your sheet."}],
58818
58947
  help:[
58819
58948
  ["What does a druid do?",
58820
- "You're a <b>Primal</b> prepared caster tied to nature, with strong healing, control, and battlefield spells. Many druids also use order focus spells to shapeshift (coming soon to the app)."],
58949
+ "You're a <b>Primal</b> prepared caster tied to nature, with strong healing, control, and battlefield spells. Many druids also use order focus spells to shapeshift add the ones you know in the Focus section."],
58821
58950
  ],
58822
58951
  },
58823
58952
 
58824
58953
  /* ---------------- WITCH ---------------- */
58825
58954
  witch:{
58826
- id:"witch", name:"Witch", icon:"🔮",
58955
+ id:"witch", name:"Witch", icon:"witch", color:"#b061b8",
58827
58956
  tagline:"Prepared caster · your patron sets the tradition",
58828
58957
  traditionFrom:"patron", defaultTradition:"occult",
58829
58958
  keyAbility:"Intelligence", keyAbilityShort:"Int",
58830
58959
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58831
- preview:true,
58832
58960
  features:[{type:"note", title:"Hexes & familiar", text:"Choose your patron's tradition above, then add your hex focus spells in the Focus section below. Familiar details stay on your sheet."}],
58833
58961
  help:[
58834
58962
  ["What does a witch do?",
58835
- "You're a prepared caster whose <b>patron</b> grants your magic — set your patron's <b>tradition</b> (arcane, divine, occult, or primal) in the character section, and your spell list follows. Witches also cast <b>hexes</b> through a familiar (coming soon)."],
58963
+ "You're a prepared caster whose <b>patron</b> grants your magic — set your patron's <b>tradition</b> (arcane, divine, occult, or primal) in the character section, and your spell list follows. Witches also cast <b>hexes</b> add your hex focus spells in the Focus section (your familiar stays on your sheet)."],
58836
58964
  ],
58837
58965
  },
58838
58966
 
58839
58967
  /* ---------------- SORCERER ---------------- */
58840
58968
  sorcerer:{
58841
- id:"sorcerer", name:"Sorcerer", icon:"",
58969
+ id:"sorcerer", name:"Sorcerer", icon:"sorcerer", color:"#e05a52",
58842
58970
  tagline:"Spontaneous caster · magic in the blood",
58843
58971
  traditionFrom:"bloodline", keyAbility:"Charisma", keyAbilityShort:"Cha",
58844
58972
  casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58845
- preview:true,
58846
58973
  features:[
58847
58974
  {type:"signature"},
58848
58975
  {type:"note", title:"Bloodline magic", text:"Add your bloodline focus spells in the Focus section below. Blood-magic side effects are described on each spell but tracked by you."},
@@ -58857,18 +58984,17 @@ const CLASSES={
58857
58984
 
58858
58985
  /* ---------------- BARD ---------------- */
58859
58986
  bard:{
58860
- id:"bard", name:"Bard", icon:"🎵",
58987
+ id:"bard", name:"Bard", icon:"bard", color:"#34a8c4",
58861
58988
  tagline:"Occult spontaneous caster · master of support",
58862
58989
  tradition:"occult", keyAbility:"Charisma", keyAbilityShort:"Cha",
58863
58990
  casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58864
- preview:true,
58865
58991
  features:[
58866
58992
  {type:"signature"},
58867
58993
  {type:"note", title:"Compositions & muse", text:"Add your composition focus spells (Courageous Anthem, Counter Performance…) in the Focus section below."},
58868
58994
  ],
58869
58995
  help:[
58870
58996
  ["What does a bard do?",
58871
- "You're an <b>Occult</b> <b>spontaneous</b> caster and the game's best supporter. You build a repertoire and cast flexibly, and your composition cantrips/focus spells buff the party (coming soon)."],
58997
+ "You're an <b>Occult</b> <b>spontaneous</b> caster and the game's best supporter. You build a repertoire and cast flexibly, and your composition cantrips/focus spells buff the party add them in the Focus section."],
58872
58998
  ["Signature spells",
58873
58999
  "Mark spells as <b>signature</b> (the ★) to heighten them with higher-rank slots. One per rank is the usual choice."],
58874
59000
  ],
@@ -58876,30 +59002,35 @@ const CLASSES={
58876
59002
 
58877
59003
  /* ---------------- ORACLE ---------------- */
58878
59004
  oracle:{
58879
- id:"oracle", name:"Oracle", icon:"👁",
59005
+ id:"oracle", name:"Oracle", icon:"oracle", color:"#c061d6",
58880
59006
  tagline:"Divine spontaneous caster · power at a price",
58881
59007
  tradition:"divine", keyAbility:"Charisma", keyAbilityShort:"Cha",
58882
59008
  casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58883
- preview:true,
58884
59009
  features:[
58885
59010
  {type:"signature"},
58886
- {type:"note", title:"Mystery & revelations", text:"Add your revelation focus spells in the Focus section below. Your escalating oracular curse is tracked on your sheet."},
59011
+ {type:"stageTracker", key:"curse", icon:"oracle", label:"Oracular curse", max:4,
59012
+ note:"Your curse deepens by a stage each time you use a cursebound action — stronger effects, worse drawbacks. Resting clears it."},
59013
+ {type:"note", title:"Mystery & revelations", text:"Add your revelation focus spells in the Focus section below, and track your escalating oracular curse on Cast Today."},
58887
59014
  ],
58888
59015
  help:[
58889
59016
  ["What does an oracle do?",
58890
- "You're a <b>Divine</b> <b>spontaneous</b> caster channeling a <b>mystery</b>. You build a repertoire and cast flexibly; your revelation spells deepen an escalating <b>curse</b> (coming soon)."],
59017
+ "You're a <b>Divine</b> <b>spontaneous</b> caster channeling a <b>mystery</b>. You build a repertoire and cast flexibly. Add your revelation focus spells in the Focus section; your escalating <b>curse</b> is tracked on your sheet."],
58891
59018
  ["Signature spells",
58892
59019
  "Mark spells as <b>signature</b> (the ★) to heighten them with higher-rank slots — usually one per spell rank."],
58893
59020
  ],
58894
59021
  },
58895
59022
  /* ---------------- MAGUS ---------------- */
58896
59023
  magus:{
58897
- id:"magus", name:"Magus", icon:"⚔️",
59024
+ id:"magus", name:"Magus", icon:"magus", color:"#5d82e0",
58898
59025
  tagline:"Arcane half-caster · blade and spell as one",
58899
59026
  tradition:"arcane", keyAbility:"Intelligence", keyAbilityShort:"Int",
58900
59027
  casting:"prepared", slots:"partial", prof:PARTIAL_CASTER_PROF, cantrips:5,
58901
- preview:true,
58902
- features:[{type:"note", title:"Spellstrike & conflux spells", text:"Add your conflux focus spells (Force Fang, etc.) in the Focus section. Spellstrike itself is a martial action recharged by Arcane Cascade — track it on your sheet. You have only a few spell slots, always in your top two ranks."}],
59028
+ features:[
59029
+ {type:"studiousSlots", key:"studious", minLevel:7, rank:2, count:2, label:"Studious spells",
59030
+ spells:["gecko-grip","sure-strike","water-breathing"],
59031
+ note:"From level 7, two extra 2nd-rank slots for utility spells only: Gecko Grip, Sure Strike, or Water Breathing — plus one spell from your hybrid study, which you add on your sheet."},
59032
+ {type:"note", title:"Spellstrike & conflux spells", text:"Add your conflux focus spells (Force Fang, etc.) in the Focus section. Spellstrike itself is a martial action recharged by Arcane Cascade — track it on your sheet. Your main spell slots are always your top two ranks."},
59033
+ ],
58903
59034
  help:[
58904
59035
  ["What does a magus do?",
58905
59036
  "You're a martial <b>Arcane</b> caster who fuses weapon strikes with spells via <b>Spellstrike</b>. You only get a <b>handful of spell slots</b> (always your two highest ranks), so each prepared spell is precious — often a damaging spell to channel through Spellstrike."],
@@ -58910,14 +59041,13 @@ const CLASSES={
58910
59041
 
58911
59042
  /* ---------------- SUMMONER ---------------- */
58912
59043
  summoner:{
58913
- id:"summoner", name:"Summoner", icon:"👹",
59044
+ id:"summoner", name:"Summoner", icon:"summoner", color:"#2bb59a",
58914
59045
  tagline:"Half-caster · you and your eidolon, as one",
58915
59046
  traditionFrom:"patron", defaultTradition:"arcane",
58916
59047
  traditionChoiceLabel:"Eidolon's tradition",
58917
59048
  traditionChoiceHint:"your eidolon's essence sets your spell tradition",
58918
59049
  keyAbility:"Charisma", keyAbilityShort:"Cha",
58919
59050
  casting:"spontaneous", slots:"partial", prof:PARTIAL_CASTER_PROF, cantrips:5,
58920
- preview:true,
58921
59051
  features:[
58922
59052
  {type:"signature"},
58923
59053
  {type:"note", title:"Eidolon & shared life", text:"Choose your eidolon's tradition above and add evolution focus spells in the Focus section. You get only a few slots (top two ranks). Your shared HP pool and the eidolon's actions stay on your sheet."},
@@ -58932,14 +59062,15 @@ const CLASSES={
58932
59062
 
58933
59063
  /* ---------------- PSYCHIC ---------------- */
58934
59064
  psychic:{
58935
- id:"psychic", name:"Psychic", icon:"🧠",
59065
+ id:"psychic", name:"Psychic", icon:"psychic", color:"#e05fa6",
58936
59066
  tagline:"Occult spontaneous caster · few spells, mighty cantrips",
58937
59067
  tradition:"occult", keyAbility:"Int or Cha", keyAbilityShort:"Key",
58938
- casting:"spontaneous", slots:"psychic", prof:FULL_CASTER_PROF, cantrips:5,
58939
- preview:true,
59068
+ casting:"spontaneous", slots:"psychic", prof:FULL_CASTER_PROF, cantrips:3,
58940
59069
  features:[
58941
59070
  {type:"signature"},
58942
- {type:"note", title:"Psi cantrips, Amps & Unleash Psyche", text:"Your psi cantrips appear in the Focus section — cast them at will, and spend a Focus Point to Amp them (use Refocus to recover). Your spell DC uses Intelligence or Charisma, whichever your conscious mind chose. Unleash Psyche is tracked on your sheet."},
59071
+ {type:"dailyResource", key:"unleash", icon:"psychic", label:"Unleash Psyche", uses:1, verb:"Unleash", recharge:"refocus",
59072
+ note:"Surge your psyche for 2 rounds — bigger cantrips and better spell attacks. Afterward your mind is strained; Refocus to Unleash again."},
59073
+ {type:"note", title:"Psi cantrips & Amps", text:"Your psi cantrips appear in the Focus section — cast them at will, or spend a Focus Point to Amp them (tap Amp). Refocus to recover points. Your spell DC uses Intelligence or Charisma, whichever your conscious mind chose."},
58943
59074
  ],
58944
59075
  help:[
58945
59076
  ["What does a psychic do?",
@@ -58951,15 +59082,14 @@ const CLASSES={
58951
59082
 
58952
59083
  /* ---------------- ANIMIST ---------------- */
58953
59084
  animist:{
58954
- id:"animist", name:"Animist", icon:"🕯️",
59085
+ id:"animist", name:"Animist", icon:"animist", color:"#e08a3c",
58955
59086
  tagline:"Divine prepared caster · channels apparitions (spirits)",
58956
59087
  tradition:"divine", keyAbility:"Wisdom", keyAbilityShort:"Wis",
58957
59088
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:4,
58958
- preview:true,
58959
59089
  features:[{type:"note", title:"Apparitions & vessel spells", text:"You prepare divine spells; your attuned apparitions grant a vessel focus spell (add the ones you know in the Focus section below) and a separate spontaneous repertoire of apparition spells that stays on your character sheet."}],
58960
59090
  help:[
58961
59091
  ["What does an animist do?",
58962
- "You're a <b>Divine</b> prepared caster who channels <b>apparitions</b> — spirits that lend you power. You prepare divine spells like a cleric, and your attuned apparitions grant extra spells plus a <b>vessel</b> focus spell. The app tracks your divine slots, cantrips and DC; apparition attunement and the apparition repertoire stay on your sheet (coming soon)."],
59092
+ "You're a <b>Divine</b> prepared caster who channels <b>apparitions</b> — spirits that lend you power. You prepare divine spells like a cleric, and your attuned apparitions grant extra spells plus a <b>vessel</b> focus spell. The app tracks your divine slots, cantrips and DC; apparition attunement and the apparition repertoire stay on your character sheet."],
58963
59093
  ["Cantrips & apparitions",
58964
59094
  "You know <b>4 cantrips</b> — two you choose plus two granted by your attuned apparitions. Your <b>vessel</b> focus spell (from your primary apparition) appears in the Focus section; cast it with Focus Points like other focus spells."],
58965
59095
  ],
@@ -59050,14 +59180,16 @@ function loadLibrary(){
59050
59180
  }catch(e){}
59051
59181
  try{ // migrate a single v2 character
59052
59182
  const v2=JSON.parse(localStorage.getItem(OLD_KEY));
59053
- if(v2 && v2.classId){ const c=Object.assign(blankChar(), v2); return {characters:{[c.id]:c}, activeId:c.id}; }
59183
+ if(v2 && v2.classId){ const c=Object.assign(blankChar(), v2); return normalizeLib({characters:{[c.id]:c}, activeId:c.id}); }
59054
59184
  }catch(e){}
59055
59185
  const c=blankChar();
59056
- return {characters:{[c.id]:c}, activeId:c.id};
59186
+ return normalizeLib({characters:{[c.id]:c}, activeId:c.id});
59057
59187
  }
59188
+ function defaultSettings(){ return { themeMode:"auto", custom:null }; }
59058
59189
  function normalizeLib(lib){
59059
59190
  Object.keys(lib.characters).forEach(id=>{ lib.characters[id]=Object.assign(defaultStateFields(), lib.characters[id], {id}); });
59060
59191
  if(!lib.characters[lib.activeId]) lib.activeId=Object.keys(lib.characters)[0];
59192
+ lib.settings=Object.assign(defaultSettings(), lib.settings||{});
59061
59193
  return lib;
59062
59194
  }
59063
59195
  let _storageWarned=false;
@@ -59065,9 +59197,66 @@ function saveState(){
59065
59197
  library.characters[state.id]=state;
59066
59198
  try{ localStorage.setItem(LS_KEY, JSON.stringify(library)); }
59067
59199
  catch(e){
59068
- if(!_storageWarned && typeof toast==="function"){ toast("Couldn't save — storage full or disabled"); _storageWarned=true; }
59200
+ if(!_storageWarned && typeof toast==="function"){ toast("Couldn't save — storage full or disabled"); _storageWarned=true; }
59069
59201
  }
59070
59202
  }
59203
+ function saveSettings(){ try{ localStorage.setItem(LS_KEY, JSON.stringify(library)); }catch(e){} }
59204
+
59205
+ /* ============================================================
59206
+ THEME (light/dark · per-class accent · custom palette)
59207
+ The base palette flips with [data-theme]; --accent (and its readable
59208
+ ink) is set per active class, or overridden by a custom palette.
59209
+ ============================================================ */
59210
+ const DEFAULT_ACCENT="#6c7a89";
59211
+ function hexToRgb(h){
59212
+ h=(h||"").replace("#","").trim();
59213
+ if(h.length===3) h=h.split("").map(c=>c+c).join("");
59214
+ const n=parseInt(h||"000000",16);
59215
+ return {r:(n>>16)&255, g:(n>>8)&255, b:n&255};
59216
+ }
59217
+ function relLuminance(hex){
59218
+ const {r,g,b}=hexToRgb(hex);
59219
+ const f=v=>{ v/=255; return v<=.03928 ? v/12.92 : Math.pow((v+.055)/1.055,2.4); };
59220
+ return .2126*f(r)+.7152*f(g)+.0722*f(b);
59221
+ }
59222
+ function inkFor(hex){ return relLuminance(hex) < .48 ? "#ffffff" : "#15171c"; }
59223
+ function mixHex(a,b,t){
59224
+ const A=hexToRgb(a), B=hexToRgb(b);
59225
+ const c=k=>Math.round(A[k]+(B[k]-A[k])*t).toString(16).padStart(2,"0");
59226
+ return "#"+c("r")+c("g")+c("b");
59227
+ }
59228
+ function resolveThemeMode(){
59229
+ const m=(library.settings&&library.settings.themeMode)||"auto";
59230
+ if(m==="light"||m==="dark") return m;
59231
+ try{ return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light"; }
59232
+ catch(e){ return "dark"; }
59233
+ }
59234
+ function applyTheme(){
59235
+ const root=document.documentElement;
59236
+ const custom=(library.settings&&library.settings.custom)||null;
59237
+ root.dataset.theme=resolveThemeMode();
59238
+ const accent=(custom&&custom.accent) || (activeClass()&&activeClass().color) || DEFAULT_ACCENT;
59239
+ root.style.setProperty("--accent", accent);
59240
+ root.style.setProperty("--accent-ink", inkFor(accent));
59241
+ // optional full-palette overrides (background / text)
59242
+ if(custom&&custom.bg) root.style.setProperty("--bg", custom.bg); else root.style.removeProperty("--bg");
59243
+ if(custom&&custom.ink) root.style.setProperty("--ink", custom.ink); else root.style.removeProperty("--ink");
59244
+ if(custom&&custom.surface){
59245
+ root.style.setProperty("--surface", custom.surface);
59246
+ root.style.setProperty("--surface-2", mixHex(custom.surface, custom.ink||"#808080", .10));
59247
+ }else{ root.style.removeProperty("--surface"); root.style.removeProperty("--surface-2"); }
59248
+ // keep the browser chrome colour in step with the background
59249
+ const meta=document.querySelector('meta[name="theme-color"]');
59250
+ if(meta){ const bg=getComputedStyle(root).getPropertyValue("--bg").trim(); if(bg) meta.setAttribute("content", bg); }
59251
+ }
59252
+ function themeColorValue(token){ return (getComputedStyle(document.documentElement).getPropertyValue(token)||"").trim()||"#000000"; }
59253
+ function setThemeMode(m){ library.settings.themeMode=m; saveSettings(); applyTheme(); renderMenu(); }
59254
+ function setCustomColor(key,val){
59255
+ library.settings.custom=library.settings.custom||{};
59256
+ library.settings.custom[key]=val;
59257
+ saveSettings(); applyTheme();
59258
+ }
59259
+ function resetTheme(){ library.settings.custom=null; saveSettings(); applyTheme(); renderMenu(); }
59071
59260
 
59072
59261
  /* ============================================================
59073
59262
  CLASS-AWARE HELPERS
@@ -59130,7 +59319,7 @@ function actionLabel(a){
59130
59319
  }
59131
59320
  const m=String(a).match(/^([1-3])\s*(?:to|or|–|-)\s*([1-3])$/);
59132
59321
  if(m) return `${ACTION_GLYPH[m[1]]}–${ACTION_GLYPH[m[2]]} ${m[1]} to ${m[2]} actions`;
59133
- return "🕑 "+a;
59322
+ return a;
59134
59323
  }
59135
59324
  function titleCaseTrait(t){ return t.charAt(0).toUpperCase()+t.slice(1); }
59136
59325
  function escapeHtml(s){ return (s||"").replace(/[&<>]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[c])); }
@@ -59158,12 +59347,11 @@ function scaleDamage(s, castRank){
59158
59347
  }
59159
59348
  function damageChipHTML(s, castRank){
59160
59349
  const ds=scaleDamage(s, castRank); if(!ds) return "";
59161
- const parts=ds.map(d=>{
59162
- const icon=d.healing?"":"💥";
59350
+ return ds.map(d=>{
59351
+ const cls=d.healing?"heal":"dmg";
59163
59352
  const ty=(d.type&&!d.healing)?" "+escapeHtml(d.type):"";
59164
- return `${icon} ${escapeHtml(d.formula)}${ty}`;
59165
- });
59166
- return `<span class="dmgchip">${parts.join(" &nbsp; ")}</span>`;
59353
+ return `<span class="dmgchip ${cls}">${escapeHtml(d.formula)}${ty}</span>`;
59354
+ }).join(" ");
59167
59355
  }
59168
59356
 
59169
59357
  function spellCardHTML(s, opts){
@@ -59176,7 +59364,7 @@ function spellCardHTML(s, opts){
59176
59364
  if(s.duration) m.push(`<b>Duration</b> ${escapeHtml(s.duration)}`);
59177
59365
  const meta = m.length?`<div class="meta">${m.join(" &nbsp;·&nbsp; ")}</div>`:"";
59178
59366
  const traits=`<div class="traits">${s.traits.map(t=>`<span class="trait">${escapeHtml(titleCaseTrait(t))}</span>`).join("")}</div>`;
59179
- const saveTag = s.save ? `<span class="save-tag">🛡 ${escapeHtml(s.save)}</span>` : "";
59367
+ const saveTag = s.save ? `<span class="save-tag">${escapeHtml(s.save)}</span>` : "";
59180
59368
  const heighten = s.heightened ? `<div class="heighten"><b>Heightened</b> ${textToHtml(s.heightened)}</div>` : "";
59181
59369
  const note=legacyNote(s)?`<div class="meta" style="margin:-2px 0 4px"><span class="formerly">${escapeHtml(legacyNote(s))}</span></div>`:"";
59182
59370
  return `
@@ -59199,7 +59387,7 @@ function spellCardHTML(s, opts){
59199
59387
  function spellPreviewHTML(s, castRank){
59200
59388
  if(!s) return "";
59201
59389
  const heightNote=(castRank>s.rank)?` · cast at rank ${castRank}`:"";
59202
- const saveTag=s.save?` · 🛡 ${escapeHtml(s.save)}`:"";
59390
+ const saveTag=s.save?` · ${escapeHtml(s.save)}`:"";
59203
59391
  const m=[];
59204
59392
  if(s.range) m.push(`<b>Range</b> ${escapeHtml(s.range)}`);
59205
59393
  if(s.area) m.push(`<b>Area</b> ${escapeHtml(s.area)}`);
@@ -59251,7 +59439,7 @@ function toast(msg){
59251
59439
  function charSummary(c){
59252
59440
  if(!c.classId) return "New character — no class yet";
59253
59441
  const cl=CLASSES[c.classId];
59254
- return `${cl.icon} ${cl.name} · Level ${c.level}`;
59442
+ return `${cl.name} · Level ${c.level}`;
59255
59443
  }
59256
59444
  function openMenu(){
59257
59445
  VIEWS.forEach(v=>document.getElementById("view-"+v).classList.add("hide"));
@@ -59268,18 +59456,33 @@ function renderMenu(){
59268
59456
  const c=library.characters[id];
59269
59457
  const active=id===library.activeId;
59270
59458
  return `<div class="classcard ${active?"activechar":""}">
59271
- <div class="ic">${c.classId?CLASSES[c.classId].icon:""}</div>
59459
+ <div class="ic" style="color:${c.classId?CLASSES[c.classId].color:"var(--muted)"}">${c.classId?iconSvg(CLASSES[c.classId].icon):iconSvg("plus")}</div>
59272
59460
  <div class="txt" onclick="switchCharacter('${id}')" style="cursor:pointer">
59273
- <div class="nm">${escapeHtml(c.name||(c.classId?CLASSES[c.classId].name:"Unnamed"))}${active?` <span class="previewtag" style="background:var(--gold);color:#2a1c0c;border-color:var(--gold)">active</span>`:""}</div>
59461
+ <div class="nm">${escapeHtml(c.name||(c.classId?CLASSES[c.classId].name:"Unnamed"))}${active?` <span class="previewtag" style="background:var(--accent);color:var(--accent-ink);border-color:var(--accent)">active</span>`:""}</div>
59274
59462
  <div class="tl">${charSummary(c)}</div>
59275
59463
  </div>
59276
59464
  ${ids.length>1?`<button class="rmrow" title="delete" onclick="deleteCharacter('${id}')">✕</button>`:""}
59277
59465
  </div>`;
59278
59466
  }).join("");
59279
59467
  document.getElementById("menuList").innerHTML=list;
59468
+ renderAppearance();
59280
59469
  showInstallButton(!!deferredInstall);
59281
59470
  const ds=document.getElementById("dataStamp"); if(ds) ds.textContent=dataStampText();
59282
59471
  }
59472
+ /* Appearance panel: Light/Dark/Auto + a four-swatch custom palette. */
59473
+ function renderAppearance(){
59474
+ const box=document.getElementById("appearancePanel"); if(!box) return;
59475
+ const mode=(library.settings&&library.settings.themeMode)||"auto";
59476
+ const seg=["light","dark","auto"].map(m=>
59477
+ `<button class="${mode===m?"on":""}" onclick="setThemeMode('${m}')">${m[0].toUpperCase()+m.slice(1)}</button>`).join("");
59478
+ const rows=[["Background","bg","--bg"],["Surface","surface","--surface"],["Text","ink","--ink"],["Accent","accent","--accent"]]
59479
+ .map(([label,key,token])=>`<div class="swatchrow"><label>${label}</label>
59480
+ <input type="color" value="${themeColorValue(token)}" oninput="setCustomColor('${key}',this.value)" aria-label="${label} colour"></div>`).join("");
59481
+ box.innerHTML=`<div class="seg" id="themeSeg">${seg}</div>
59482
+ <p class="meta">Colours follow each class's theme by default. Pick your own below — they apply to every character.</p>
59483
+ ${rows}
59484
+ <button class="btn secondary" onclick="resetTheme()">Reset to class default</button>`;
59485
+ }
59283
59486
  function newCharacter(){ const c=blankChar(); library.characters[c.id]=c; library.activeId=c.id; state=c; saveState(); showClassPicker(); }
59284
59487
  function switchCharacter(id){
59285
59488
  if(!library.characters[id]) return;
@@ -59305,7 +59508,7 @@ function exportCharacter(){
59305
59508
  const code="PF2E1:"+b64encode(JSON.stringify(c));
59306
59509
  const io=document.getElementById("menuIO");
59307
59510
  io.value=code; io.focus(); io.select();
59308
- if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(code).then(()=>toast("📋 Copied to clipboard")).catch(()=>toast("Code ready — copy it")); }
59511
+ if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(code).then(()=>toast("Copied to clipboard")).catch(()=>toast("Code ready — copy it")); }
59309
59512
  else toast("Code ready — copy it");
59310
59513
  }
59311
59514
  function importCharacter(){
@@ -59317,7 +59520,7 @@ function importCharacter(){
59317
59520
  if(typeof obj!=="object"||!("level" in obj)) throw new Error("bad");
59318
59521
  const c=Object.assign(blankChar(), obj, {id:uid()});
59319
59522
  library.characters[c.id]=c; library.activeId=c.id; state=c; saveState();
59320
- toast("📥 Character imported");
59523
+ toast("Character imported");
59321
59524
  if(!state.classId){ showClassPicker(); } else { renderHeader(); go(state.prepared?"today":"prepare"); }
59322
59525
  }catch(e){ alert("That code didn't work — make sure you pasted the whole thing."); }
59323
59526
  }
@@ -59332,18 +59535,19 @@ function showClassPicker(){
59332
59535
  const sec=document.getElementById("view-classpick");
59333
59536
  sec.classList.remove("hide");
59334
59537
  sec.innerHTML=`
59335
- <h1 style="text-align:center">📖 PF2e Spellbook</h1>
59538
+ <h1 style="text-align:center">PF2e Spellbook</h1>
59336
59539
  <p class="meta" style="text-align:center;margin-bottom:18px">Choose your class to begin.</p>
59337
59540
  ${CLASS_ORDER.map(id=>{
59338
59541
  const c=CLASSES[id];
59339
- return `<button class="classcard" onclick="chooseClass('${id}')">
59340
- <div class="ic">${c.icon}</div>
59542
+ return `<button class="classcard" style="border-left-color:${c.color}" onclick="chooseClass('${id}')">
59543
+ <div class="ic" style="color:${c.color}">${iconSvg(c.icon)}</div>
59341
59544
  <div class="txt"><div class="nm">${c.name}${c.preview?` <span class="previewtag">preview</span>`:""}</div>
59342
59545
  <div class="tl">${c.tagline}</div></div>
59343
59546
  <div class="arr">→</div>
59344
59547
  </button>`;
59345
59548
  }).join("")}
59346
59549
  <p class="meta" style="text-align:center;margin-top:18px;font-size:.8rem">Pathfinder 2e (Remaster) · unofficial fan tool<br>${escapeHtml(dataStampText())}</p>`;
59550
+ applyTheme();
59347
59551
  window.scrollTo(0,0);
59348
59552
  }
59349
59553
  function chooseClass(id){
@@ -59354,8 +59558,9 @@ function chooseClass(id){
59354
59558
  if(!hasFeature("divineFont")) state.font="heal";
59355
59559
  if(c.traditionFrom==="patron" && !state.patronTradition) state.patronTradition=c.defaultTradition||"occult";
59356
59560
  saveState();
59561
+ applyTheme();
59357
59562
  renderHeader();
59358
- toast(`${c.icon} ${c.name} selected`);
59563
+ toast(`${c.name} selected`);
59359
59564
  go(state.prepared ? "today" : "prepare");
59360
59565
  }
59361
59566
  function openClassPicker(){ showClassPicker(); }
@@ -59365,7 +59570,8 @@ function openClassPicker(){ showClassPicker(); }
59365
59570
  ============================================================ */
59366
59571
  function renderHeader(){
59367
59572
  const c=activeClass(); if(!c) return;
59368
- document.getElementById("charIcon").textContent=c.icon;
59573
+ applyTheme();
59574
+ document.getElementById("charIcon").innerHTML=iconSvg(c.icon);
59369
59575
  document.getElementById("charNameHead").textContent=state.name||c.name;
59370
59576
  document.getElementById("charSub").textContent=
59371
59577
  `${c.name} · Pathfinder 2e (Remaster) · ${TRADITION_LABEL[activeTradition()]} list`;
@@ -59431,8 +59637,8 @@ function renderCharExtra(){
59431
59637
  <label class="field"><span class="name">Divine Font</span>
59432
59638
  <span class="hint">Which one did you choose at level 1?</span></label>
59433
59639
  <div class="seg" id="fontSeg">
59434
- <button data-font="heal" class="heal ${state.font==="heal"?"on":""}" onclick="setFont('heal')">✚ Healing Font</button>
59435
- <button data-font="harm" class="harm ${state.font==="harm"?"on":""}" onclick="setFont('harm')">☠ Harmful Font</button>
59640
+ <button data-font="heal" class="heal ${state.font==="heal"?"on":""}" onclick="setFont('heal')">${iconSvg("heal")} Healing Font</button>
59641
+ <button data-font="harm" class="harm ${state.font==="harm"?"on":""}" onclick="setFont('harm')">${iconSvg("harm")} Harmful Font</button>
59436
59642
  </div>`;
59437
59643
  }
59438
59644
  if(c.traditionFrom==="bloodline"){
@@ -59463,22 +59669,25 @@ function renderPrepSummary(){
59463
59669
  let extra="";
59464
59670
  if(hasFeature("divineFont")){
59465
59671
  const fontName=state.font==="heal"?"Heal":"Harm";
59466
- extra+=`<div style="margin-top:4px">${state.font==="heal"?"":""} Divine Font: <b>${fontCount()}× ${fontName}</b> (free, heightened to rank ${hr})</div>`;
59672
+ extra+=`<div style="margin-top:4px">${iconSvg(state.font==="heal"?"heal":"harm")} Divine Font: <b>${fontCount()}× ${fontName}</b> (free, heightened to rank ${hr})</div>`;
59467
59673
  }
59468
59674
  getFeatures("extraSlots").forEach(f=>{
59469
- extra+=`<div style="margin-top:4px">${f.icon} ${f.label}: <b>+1</b> per rank</div>`;
59675
+ extra+=`<div style="margin-top:4px">${iconSvg(f.icon)} ${f.label}: <b>+1</b> per rank</div>`;
59470
59676
  });
59471
59677
  getFeatures("dailyResource").forEach(f=>{
59472
- extra+=`<div style="margin-top:4px">${f.icon} ${f.label}: <b>${f.uses}×</b>/day</div>`;
59678
+ extra+=`<div style="margin-top:4px">${iconSvg(f.icon)} ${f.label}: <b>${f.uses}×</b>/day</div>`;
59679
+ });
59680
+ studiousFeats().forEach(f=>{
59681
+ extra+=`<div style="margin-top:4px">${iconSvg("school")} ${f.label}: <b>${f.count}×</b> rank ${f.rank} (utility)</div>`;
59473
59682
  });
59474
59683
  if(hasFocus()){
59475
- extra+=`<div style="margin-top:4px">🔵 Focus pool: <b>${state.focusPool||1}</b> Focus Point${(state.focusPool||1)>1?"s":""} (focus spells heighten to rank ${focusRank()})</div>`;
59684
+ extra+=`<div style="margin-top:4px">Focus pool: <b>${state.focusPool||1}</b> Focus Point${(state.focusPool||1)>1?"s":""} (focus spells heighten to rank ${focusRank()})</div>`;
59476
59685
  }
59477
59686
  document.getElementById("prepSummary").innerHTML=`
59478
59687
  <div class="meta" style="font-size:1rem">
59479
- <div>🎯 <b>Spell DC ${spellDC()}</b> &nbsp;·&nbsp; Spell attack ${spellAtk()}</div>
59480
- <div style="margin-top:8px">🪄 Cantrips ${isSpontaneous()?"known":""}: <b>${c.cantrips}</b></div>
59481
- <div style="margin-top:4px">📜 ${isSpontaneous()?"Slots (cast any known spell of that rank)":"Spell slots"}: ${slotText||"—"}</div>
59688
+ <div><b>Spell DC ${spellDC()}</b> &nbsp;·&nbsp; Spell attack ${spellAtk()}</div>
59689
+ <div style="margin-top:8px">Cantrips ${isSpontaneous()?"known":""}: <b>${c.cantrips}</b></div>
59690
+ <div style="margin-top:4px">${isSpontaneous()?"Slots (cast any known spell of that rank)":"Spell slots"}: ${slotText||"—"}</div>
59482
59691
  ${extra}
59483
59692
  </div>`;
59484
59693
  }
@@ -59504,6 +59713,7 @@ function renderPrepDynamic(){
59504
59713
  getFeatures("note").forEach(f=>{ html+=`<div class="banner"><b>${f.title}</b> — ${f.text}</div>`; });
59505
59714
  html+=cantripPicksHTML();
59506
59715
  html+= isSpontaneous() ? repertoireHTML() : preparedSlotsHTML();
59716
+ if(!isSpontaneous()) html+=studiousSlotsHTML();
59507
59717
  if(hasFocus()) html+=focusPicksHTML();
59508
59718
  document.getElementById("prepDynamic").innerHTML=html;
59509
59719
  }
@@ -59522,14 +59732,14 @@ function focusPicksHTML(){
59522
59732
  <span class="fcname">${escapeHtml(s.name)}</span><span class="fcrank">${rank}</span></label>`;
59523
59733
  }).join("");
59524
59734
  return `<div class="slotgroup">
59525
- <h3>🔵 Focus spells <span class="count" style="font-weight:600;color:var(--muted)">(pool of ${pool})</span></h3>
59735
+ <h3>Focus spells <span class="count" style="font-weight:600;color:var(--muted)">(pool of ${pool})</span></h3>
59526
59736
  <p class="meta">Focus spells cost <b>Focus Points</b> (shared pool, max 3) and auto-heighten to rank ${focusRank()}. <b>Refocus</b> restores points. Check the ones you know — type below to filter to your domain / bloodline / order.</p>
59527
59737
  <label class="field" style="margin:8px 0"><span class="name">Focus Point pool</span>
59528
59738
  <span class="hint">1 + 1 per extra focus feat, up to 3</span>
59529
59739
  <select id="inFocusPool" onchange="setFocusPool(this.value)">
59530
59740
  ${[1,2,3].map(n=>`<option value="${n}" ${n===pool?"selected":""}>${n} Focus Point${n>1?"s":""}</option>`).join("")}
59531
59741
  </select></label>
59532
- <input type="text" id="focusFilter" placeholder="🔍 filter focus spells (name or trait)…" oninput="filterFocusChecklist(this.value)" aria-label="filter focus spells">
59742
+ <input type="text" id="focusFilter" placeholder="Filter focus spells (name or trait)…" oninput="filterFocusChecklist(this.value)" aria-label="filter focus spells">
59533
59743
  <div class="focus-checklist" id="focusChecklist">${rows}</div>
59534
59744
  </div>`;
59535
59745
  }
@@ -59547,7 +59757,7 @@ function gatherFocus(){
59547
59757
  function cantripPicksHTML(){
59548
59758
  const c=activeClass();
59549
59759
  const prev=(state.prepared&&state.prepared.cantrips)||[];
59550
- let html=`<div class="slotgroup"><h3>🪄 Cantrips (pick ${c.cantrips})</h3>`;
59760
+ let html=`<div class="slotgroup"><h3>Cantrips (pick ${c.cantrips})</h3>`;
59551
59761
  for(let i=0;i<c.cantrips;i++){
59552
59762
  const sel=prev[i]||"", pid="cprev"+i;
59553
59763
  html+=`<div class="prep-slot">
@@ -59570,10 +59780,10 @@ function preparedSlotsHTML(){
59570
59780
  const fontName=state.font==="heal"?"Heal":"Harm";
59571
59781
  const fontSpell=findSpell(state.font==="heal"?feat.healSlug:feat.harmSlug);
59572
59782
  html+=`<div class="slotgroup">
59573
- <h3>${state.font==="heal"?"":""} Divine Font — ${fontName} (free slots)</h3>
59783
+ <h3>${iconSvg(state.font==="heal"?"heal":"harm")} Divine Font — ${fontName} (free slots)</h3>
59574
59784
  <p class="meta">These ${fontCount()} castings are free and separate from your normal slots, usable only for <b>${fontName}</b>, auto-heightened to rank ${hr}.</p>`;
59575
59785
  for(let i=0;i<fontCount();i++){
59576
- html+=`<div class="slotrow"><div class="num">${state.font==="heal"?"":""}</div>
59786
+ html+=`<div class="slotrow"><div class="num">${iconSvg(state.font==="heal"?"heal":"harm")}</div>
59577
59787
  <select disabled><option>${fontName} (rank ${hr})</option></select></div>`;
59578
59788
  }
59579
59789
  html+=`<div class="prep-preview">${spellPreviewHTML(fontSpell, hr)}</div></div>`;
@@ -59583,7 +59793,7 @@ function preparedSlotsHTML(){
59583
59793
  Object.keys(slots).map(Number).sort((a,b)=>b-a).forEach(r=>{
59584
59794
  const n=slots[r];
59585
59795
  const prev=prevSlots[r]||[];
59586
- html+=`<div class="slotgroup"><h3>📜 Rank ${r} slots (${n})</h3>`;
59796
+ html+=`<div class="slotgroup"><h3>Rank ${r} slots (${n})</h3>`;
59587
59797
  for(let i=0;i<n;i++){
59588
59798
  const sel=prev[i]||"", pid="sprev"+r+"_"+i;
59589
59799
  html+=`<div class="prep-slot">
@@ -59596,7 +59806,7 @@ function preparedSlotsHTML(){
59596
59806
  const ex=(prevExtra[f.key]&&prevExtra[f.key][r]&&prevExtra[f.key][r][0])||"";
59597
59807
  const pid="xprev"+f.key+r;
59598
59808
  html+=`<div class="prep-slot">
59599
- <div class="slotrow"><div class="num">${f.icon}</div>
59809
+ <div class="slotrow"><div class="num">${iconSvg(f.icon)}</div>
59600
59810
  <select class="extraSel" data-key="${f.key}" data-rank="${r}" data-castrank="${r}" data-preview="${pid}" onchange="updatePreview(this)">${spellOptions(r, ex, false)}</select></div>
59601
59811
  <div class="prep-preview" id="${pid}">${spellPreviewHTML(findSpell(ex),r)}</div></div>`;
59602
59812
  });
@@ -59605,6 +59815,33 @@ function preparedSlotsHTML(){
59605
59815
  return html;
59606
59816
  }
59607
59817
 
59818
+ /* Studious spells (magus, level 7+): a couple of extra fixed-rank slots that can
59819
+ only hold a short list of utility spells. Restricted <select> so the app stays
59820
+ honest about what's legal in these slots. */
59821
+ function studiousFeats(){ return getFeatures("studiousSlots").filter(f=>Number(state.level)>=(f.minLevel||1)); }
59822
+ function studiousSlotsHTML(){
59823
+ const feats=studiousFeats(); if(!feats.length) return "";
59824
+ const prevAll=(state.prepared&&state.prepared.studious)||{};
59825
+ let html="";
59826
+ feats.forEach(f=>{
59827
+ const prev=prevAll[f.key]||[];
59828
+ const opts=(f.spells||[]).map(findSpell).filter(Boolean).sort((a,b)=>a.name.localeCompare(b.name));
59829
+ html+=`<div class="slotgroup"><h3>${f.label} <span class="count" style="font-weight:600;color:var(--muted)">(${f.count} × rank ${f.rank}, utility only)</span></h3>
59830
+ <p class="meta">${escapeHtml(f.note||"")}</p>`;
59831
+ for(let i=0;i<f.count;i++){
59832
+ const sel=prev[i]||"", pid="stud_"+f.key+"_"+i;
59833
+ const optionHtml=`<option value="">— choose —</option>`+
59834
+ opts.map(s=>`<option value="${s.slug}" ${s.slug===sel?"selected":""}>${escapeHtml(s.name)}</option>`).join("");
59835
+ html+=`<div class="prep-slot">
59836
+ <div class="slotrow"><div class="num">${iconSvg("school")}</div>
59837
+ <select class="studiousSel" data-key="${f.key}" data-castrank="${f.rank}" data-preview="${pid}" onchange="updatePreview(this)">${optionHtml}</select></div>
59838
+ <div class="prep-preview" id="${pid}">${spellPreviewHTML(findSpell(sel),f.rank)}</div></div>`;
59839
+ }
59840
+ html+=`</div>`;
59841
+ });
59842
+ return html;
59843
+ }
59844
+
59608
59845
  let repUID=0;
59609
59846
  function repRowHTML(rank, sel, sig){
59610
59847
  const pid="rprev"+(repUID++);
@@ -59628,7 +59865,7 @@ function repertoireHTML(){
59628
59865
  const count=Math.max(known.length, slots[r]||0);
59629
59866
  if(!count) continue; // no slots of this rank (e.g. a summoner's shed low ranks) → no repertoire of that rank
59630
59867
  html+=`<div class="slotgroup" data-reprank="${r}">
59631
- <h3>📜 Rank ${r} spells known <span class="count" style="font-weight:600;color:var(--muted)">(${slots[r]||0} slot${(slots[r]||0)===1?"":"s"}/day)</span></h3>
59868
+ <h3>Rank ${r} spells known <span class="count" style="font-weight:600;color:var(--muted)">(${slots[r]||0} slot${(slots[r]||0)===1?"":"s"}/day)</span></h3>
59632
59869
  <div class="repRows" id="repRows-${r}">`;
59633
59870
  for(let i=0;i<count;i++){
59634
59871
  const e=known[i]||{};
@@ -59668,8 +59905,11 @@ function savePrep(){
59668
59905
  document.querySelectorAll(".slotSel").forEach(sel=>{ const r=sel.dataset.rank; (slots[r]=slots[r]||[]).push(sel.value); });
59669
59906
  const extra={};
59670
59907
  document.querySelectorAll(".extraSel").forEach(sel=>{ const k=sel.dataset.key,r=sel.dataset.rank; extra[k]=extra[k]||{}; (extra[k][r]=extra[k][r]||[]).push(sel.value); });
59908
+ const studious={};
59909
+ document.querySelectorAll(".studiousSel").forEach(sel=>{ const k=sel.dataset.key; (studious[k]=studious[k]||[]).push(sel.value); });
59671
59910
  const hr=highestRank(state.level);
59672
59911
  state.prepared={ type:"prepared", cantrips, slots, extra };
59912
+ if(Object.keys(studious).length) state.prepared.studious=studious;
59673
59913
  if(hasFeature("divineFont")){
59674
59914
  const feat=getFeature("divineFont");
59675
59915
  state.prepared.divineFont={ font:state.font, fontName:state.font==="heal"?"Heal":"Harm",
@@ -59678,7 +59918,7 @@ function savePrep(){
59678
59918
  }
59679
59919
  if(focus) state.prepared.focus=focus;
59680
59920
  saveState();
59681
- toast("Prepared for the day!");
59921
+ toast("Prepared for the day!");
59682
59922
  go("today");
59683
59923
  }
59684
59924
 
@@ -59691,7 +59931,7 @@ function todayHeaderHTML(){
59691
59931
  return `<div class="card" style="text-align:center">
59692
59932
  <div style="font-size:1.1rem"><b>${escapeHtml(state.name||c.name)}</b> · ${c.name} · Level ${state.level}</div>
59693
59933
  <div class="meta" style="font-size:1rem;margin-top:6px">Spell DC <b>${spellDC()}</b> &nbsp;·&nbsp; Spell attack <b>${spellAtk()}</b></div>
59694
- <button class="btn secondary" style="margin-top:12px" onclick="newDay()">🌅 Rest &amp; reset the day</button>
59934
+ <button class="btn secondary" style="margin-top:12px" onclick="newDay()">Rest &amp; reset the day</button>
59695
59935
  </div>`;
59696
59936
  }
59697
59937
  function dailyResourceHTML(){
@@ -59701,14 +59941,34 @@ function dailyResourceHTML(){
59701
59941
  const spent=state.cast[key]||0, remaining=f.uses-spent, done=remaining<=0;
59702
59942
  let pips=""; for(let k=0;k<f.uses;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
59703
59943
  const btn=done?`<button class="castbtn zero" onclick="uncast('${key}')">↩ undo</button>`
59704
- :`<button class="castbtn" onclick="doCast('${key}',${f.uses})">Use</button>`;
59944
+ :`<button class="castbtn" onclick="doCast('${key}',${f.uses})">${f.verb||"Use"}</button>`;
59705
59945
  html+=`<div class="cast-card ${done?"spent":""}">
59706
- <div class="cast-top"><div class="nm">${f.icon} ${f.label}</div><div class="uses">${pips} ${btn}</div></div>
59946
+ <div class="cast-top"><div class="nm">${iconSvg(f.icon)} ${f.label}</div><div class="uses">${pips} ${btn}</div></div>
59707
59947
  <div class="meta">${escapeHtml(f.note||"")}</div></div>`;
59708
59948
  });
59709
59949
  return html;
59710
59950
  }
59711
59951
 
59952
+ /* Escalating-stage tracker (e.g. the oracle's cursebound curse). A simple
59953
+ 0..max stepper that persists in state.cast and resets on a new day. */
59954
+ function stageTrackerHTML(){
59955
+ let html="";
59956
+ getFeatures("stageTracker").forEach(f=>{
59957
+ const k="stage:"+f.key;
59958
+ const cur=Math.max(0, Math.min(f.max, state.cast[k]||0));
59959
+ let pips=""; for(let i=1;i<=f.max;i++){ pips+=`<span class="pip ${i<=cur?"full":"spent"}"></span>`; }
59960
+ html+=`<div class="cast-card">
59961
+ <div class="cast-top"><div class="nm">${iconSvg(f.icon)} ${f.label} <span class="meta">stage ${cur} / ${f.max}</span></div>
59962
+ <div class="uses">${pips}
59963
+ <button class="castbtn zero" onclick="stageDown('${f.key}')" ${cur<=0?"disabled":""} aria-label="lower ${escapeHtml(f.label)} stage">−</button>
59964
+ <button class="castbtn" onclick="stageUp('${f.key}')" ${cur>=f.max?"disabled":""} aria-label="raise ${escapeHtml(f.label)} stage">+</button></div></div>
59965
+ <div class="meta">${escapeHtml(f.note||"")}</div></div>`;
59966
+ });
59967
+ return html;
59968
+ }
59969
+ function stageUp(key){ const f=getFeatures("stageTracker").find(x=>x.key===key); if(!f) return; const k="stage:"+key; state.cast[k]=Math.min(f.max,(state.cast[k]||0)+1); saveState(); renderToday(); }
59970
+ function stageDown(key){ const k="stage:"+key; state.cast[k]=Math.max(0,(state.cast[k]||0)-1); saveState(); renderToday(); }
59971
+
59712
59972
  function renderToday(){
59713
59973
  renderHeader();
59714
59974
  const p=state.prepared;
@@ -59729,41 +59989,58 @@ function focusSectionHTML(p){
59729
59989
  const fr=focusRank();
59730
59990
  let pips=""; for(let k=0;k<pool;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
59731
59991
  const refocus=`<button class="castbtn ${spent>0?"":"zero"}" ${spent>0?"":"disabled"} style="margin-left:8px" onclick="refocus()">↻ Refocus</button>`;
59732
- let html=`<div class="sectionhead">🔵 Focus spells <span class="count">${remaining}/${pool} point${pool>1?"s":""}</span></div>
59992
+ let html=`<div class="sectionhead">Focus spells <span class="count">${remaining}/${pool} point${pool>1?"s":""}</span></div>
59733
59993
  <div style="margin:2px 0 8px"><span class="uses">${pips}${refocus}</span></div><div class="divider"></div>`;
59994
+ const psychic = !!(activeClass() && activeClass().id==="psychic");
59734
59995
  f.spells.forEach((slug,i)=>{
59735
59996
  const s=findSpell(slug); if(!s) return;
59736
59997
  const atWill = s.rank===0;
59737
59998
  const out = !atWill && remaining<=0;
59738
59999
  const castRank=fr;
59739
60000
  const heightNote=(castRank>s.rank)?`<span class="meta"> · ↑ heightened to rank ${castRank}</span>`:"";
59740
- const btn = atWill ? `<span class="unlim">∞ at will</span>`
59741
- : (out ? `<button class="castbtn zero" disabled>No points</button>`
59742
- : `<button class="castbtn" onclick="castFocus(${pool})">Cast</button>`);
60001
+ // Psychic psi cantrips are free at will, but can be Amped by spending a Focus Point.
60002
+ let btn;
60003
+ if(atWill && psychic){
60004
+ const amp = remaining>0
60005
+ ? `<button class="castbtn" onclick="castFocus(${pool})" title="Amp — spend 1 Focus Point">Amp (1 pt)</button>`
60006
+ : `<button class="castbtn zero" disabled>No points</button>`;
60007
+ btn = `<span class="unlim">∞ at will</span> ${amp}`;
60008
+ } else if(atWill){
60009
+ btn = `<span class="unlim">∞ at will</span>`;
60010
+ } else {
60011
+ btn = out ? `<button class="castbtn zero" disabled>No points</button>`
60012
+ : `<button class="castbtn" onclick="castFocus(${pool})">Cast</button>`;
60013
+ }
59743
60014
  const detailsId="f_"+i;
59744
- html+=`<div class="cast-card ${out?"spent":""}" style="border-left-color:#9b7fd6">
59745
- <div class="cast-top"><div class="nm">🔵 ${escapeHtml(s.name)} ${heightNote}</div><div class="uses">${btn}</div></div>
59746
- <div class="meta">${actionLabel(s.actions)}${s.save?` · 🛡 ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
60015
+ html+=`<div class="cast-card ${out?"spent":""}" style="border-left-color:var(--accent)">
60016
+ <div class="cast-top"><div class="nm">${escapeHtml(s.name)} ${heightNote}</div><div class="uses">${btn}</div></div>
60017
+ <div class="meta">${actionLabel(s.actions)}${s.save?` · ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
59747
60018
  <button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
59748
60019
  <div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s)}</div></div>`;
59749
60020
  });
59750
60021
  return html;
59751
60022
  }
59752
60023
  function castFocus(pool){ const key="focuspool"; state.cast[key]=Math.min(pool,(state.cast[key]||0)+1); saveState(); renderToday(); }
59753
- function refocus(){ state.cast["focuspool"]=0; saveState(); renderToday(); toast("🔵 Refocused"); }
60024
+ function refocus(){
60025
+ const key="focuspool"; state.cast[key]=Math.max(0,(state.cast[key]||0)-1);
60026
+ // Some resources (e.g. the psychic's Unleash Psyche) recharge when you Refocus.
60027
+ getFeatures("dailyResource").forEach(f=>{ if(f.recharge==="refocus") delete state.cast["resource:"+f.key]; });
60028
+ saveState(); renderToday(); toast("Refocused — +1 Focus Point");
60029
+ }
59754
60030
 
59755
60031
  function renderTodayPrepared(p){
59756
60032
  let html=todayHeaderHTML();
59757
60033
  html+=dailyResourceHTML();
60034
+ html+=stageTrackerHTML();
59758
60035
 
59759
60036
  if(p.divineFont){
59760
60037
  const f=p.divineFont; const fontSpell=findSpell(f.fontSlug);
59761
- html+=`<div class="sectionhead">${f.font==="heal"?"":""} Divine Font — ${f.fontName} <span class="count">free</span></div><div class="divider"></div>`;
60038
+ html+=`<div class="sectionhead">${iconSvg(f.font==="heal"?"heal":"harm")} Divine Font — ${f.fontName} <span class="count">free</span></div><div class="divider"></div>`;
59762
60039
  html+=castableCard(fontSpell,"font",f.fontRank,f.fontCount,f.fontRank,0,{cls:f.font});
59763
60040
  }
59764
60041
 
59765
60042
  if(p.cantrips && p.cantrips.length){
59766
- html+=`<div class="sectionhead">🪄 Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
60043
+ html+=`<div class="sectionhead">Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
59767
60044
  p.cantrips.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"cantrip",0,Infinity,0,i); });
59768
60045
  }
59769
60046
 
@@ -59773,19 +60050,26 @@ function renderTodayPrepared(p){
59773
60050
  const extras=[];
59774
60051
  extraFeats.forEach(f=>{ const v=p.extra&&p.extra[f.key]&&p.extra[f.key][r]&&p.extra[f.key][r][0]; if(v) extras.push({f,slug:v}); });
59775
60052
  if(!arr.length && !extras.length) return;
59776
- html+=`<div class="sectionhead">📜 Rank ${r} spells <span class="count">${arr.length+extras.length} slot${(arr.length+extras.length)>1?"s":""}</span></div><div class="divider"></div>`;
60053
+ html+=`<div class="sectionhead">Rank ${r} spells <span class="count">${arr.length+extras.length} slot${(arr.length+extras.length)>1?"s":""}</span></div><div class="divider"></div>`;
59777
60054
  (p.slots[r]||[]).forEach((slug,i)=>{ if(!slug) return; const s=findSpell(slug); if(!s) return; html+=castableCard(s,"slot",r,1,r,i); });
59778
60055
  extras.forEach(({f,slug})=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"extra_"+f.key,r,1,r,0,{label:f.icon}); });
59779
60056
  });
60057
+ studiousFeats().forEach(f=>{
60058
+ const arr=((p.studious&&p.studious[f.key])||[]).filter(Boolean);
60059
+ if(!arr.length) return;
60060
+ html+=`<div class="sectionhead">${f.label} <span class="count">${arr.length} slot${arr.length>1?"s":""}</span></div><div class="divider"></div>`;
60061
+ arr.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"studious",f.rank,1,f.rank,i,{label:"school"}); });
60062
+ });
59780
60063
  return html;
59781
60064
  }
59782
60065
 
59783
60066
  function renderTodaySpontaneous(p){
59784
60067
  let html=todayHeaderHTML();
59785
60068
  html+=dailyResourceHTML();
60069
+ html+=stageTrackerHTML();
59786
60070
 
59787
60071
  if(p.cantrips && p.cantrips.length){
59788
- html+=`<div class="sectionhead">🪄 Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
60072
+ html+=`<div class="sectionhead">Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
59789
60073
  p.cantrips.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"cantrip",0,Infinity,0,i); });
59790
60074
  }
59791
60075
 
@@ -59802,7 +60086,7 @@ function renderTodaySpontaneous(p){
59802
60086
 
59803
60087
  let pips=""; for(let k=0;k<max;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
59804
60088
  const undo = spent>0 ? `<button class="castbtn zero" style="margin-left:8px" onclick="uncastPool(${R})">↩</button>` : "";
59805
- html+=`<div class="sectionhead">📜 Rank ${R} <span class="count">${remaining}/${max} slot${max>1?"s":""} left</span></div>
60089
+ html+=`<div class="sectionhead">Rank ${R} <span class="count">${remaining}/${max} slot${max>1?"s":""} left</span></div>
59806
60090
  <div style="margin:2px 0 8px"><span class="uses">${pips}${undo}</span></div><div class="divider"></div>`;
59807
60091
 
59808
60092
  known.forEach((e,i)=>{ const s=findSpell(e.slug); if(!s) return; html+=poolSpellCard(s,R,R,remaining,max,"k"+i,e.sig); });
@@ -59822,7 +60106,7 @@ function castableCard(s, kind, rank, uses, castRank, idx, opts){
59822
60106
  const isSpent=!unlimited && remaining<=0;
59823
60107
  const cls = opts.cls || (isSpent?"":cardClass(s));
59824
60108
  const heightNote=(castRank>s.rank)?`<span class="meta"> · cast at rank ${castRank}</span>`:"";
59825
- const tag = opts.label?`<span class="slottag">${opts.label}</span> `:"";
60109
+ const tag = opts.label?`<span class="slottag">${iconSvg(opts.label)}</span> `:"";
59826
60110
 
59827
60111
  let usesHtml="";
59828
60112
  if(unlimited){ usesHtml=`<span class="unlim">∞ at will</span>`; }
@@ -59841,7 +60125,7 @@ function castableCard(s, kind, rank, uses, castRank, idx, opts){
59841
60125
  <div class="nm">${tag}${escapeHtml(s.name)} ${heightNote}</div>
59842
60126
  <div class="uses">${usesHtml} ${btn} ${sustainBtn(s)}</div>
59843
60127
  </div>
59844
- <div class="meta">${actionLabel(s.actions)}${s.save?` · 🛡 ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
60128
+ <div class="meta">${actionLabel(s.actions)}${s.save?` · ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
59845
60129
  <button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
59846
60130
  <div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s,{cls})}</div>
59847
60131
  </div>`;
@@ -59862,7 +60146,7 @@ function poolSpellCard(s, poolRank, castRank, remaining, max, uid, sig){
59862
60146
  <div class="nm">${star}${escapeHtml(s.name)} ${heightNote}</div>
59863
60147
  <div class="uses">${btn} ${sustainBtn(s)}</div>
59864
60148
  </div>
59865
- <div class="meta">${actionLabel(s.actions)}${s.save?` · 🛡 ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
60149
+ <div class="meta">${actionLabel(s.actions)}${s.save?` · ${escapeHtml(s.save)}`:""}${s.range?` · ${escapeHtml(s.range)}`:""}</div>${damageChipHTML(s,castRank)?`<div class="dmgline">${damageChipHTML(s,castRank)}</div>`:""}
59866
60150
  <button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
59867
60151
  <div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s)}</div>
59868
60152
  </div>`;
@@ -59873,22 +60157,22 @@ function doCast(key,max){ state.cast[key]=(state.cast[key]||0)+1; if(state.cast[
59873
60157
  function uncast(key){ state.cast[key]=Math.max(0,(state.cast[key]||0)-1); saveState(); renderToday(); }
59874
60158
  function castPool(rank,max){ const key="pool:"+rank; state.cast[key]=Math.min(max,(state.cast[key]||0)+1); saveState(); renderToday(); }
59875
60159
  function uncastPool(rank){ const key="pool:"+rank; state.cast[key]=Math.max(0,(state.cast[key]||0)-1); saveState(); renderToday(); }
59876
- function newDay(){ if(confirm("Reset all spent slots for a new day? Your prepared list stays the same.")){ state.cast={}; state.sustaining=[]; saveState(); renderToday(); toast("🌅 A new day dawns!"); } }
60160
+ function newDay(){ if(confirm("Reset all spent slots for a new day? Your prepared list stays the same.")){ state.cast={}; state.sustaining=[]; saveState(); renderToday(); toast("A new day dawns!"); } }
59877
60161
 
59878
60162
  /* ---- Sustained-spell tracker ---- */
59879
60163
  function sustainBtn(s){
59880
60164
  if(!s || !s.sustained) return "";
59881
60165
  const on=(state.sustaining||[]).includes(s.slug);
59882
- return on ? `<button class="susbtn on" onclick="endSustain('${s.slug}')">⏳ End</button>`
59883
- : `<button class="susbtn" onclick="startSustain('${s.slug}')">⏳ Sustain</button>`;
60166
+ return on ? `<button class="susbtn on" onclick="endSustain('${s.slug}')">End</button>`
60167
+ : `<button class="susbtn" onclick="startSustain('${s.slug}')">Sustain</button>`;
59884
60168
  }
59885
- function startSustain(slug){ state.sustaining=state.sustaining||[]; if(!state.sustaining.includes(slug)){ state.sustaining.push(slug); saveState(); renderToday(); toast("Now sustaining"); } }
60169
+ function startSustain(slug){ state.sustaining=state.sustaining||[]; if(!state.sustaining.includes(slug)){ state.sustaining.push(slug); saveState(); renderToday(); toast("Now sustaining"); } }
59886
60170
  function endSustain(slug){ state.sustaining=(state.sustaining||[]).filter(x=>x!==slug); saveState(); renderToday(); }
59887
60171
  function sustainingBarHTML(){
59888
60172
  const list=(state.sustaining||[]).filter(slug=>findSpell(slug));
59889
60173
  if(!list.length) return "";
59890
- return `<div class="card" style="border-left:6px solid var(--accent)">
59891
- <div class="sectionhead" style="margin:0 0 6px">⏳ Sustaining now <span class="count">spend an action each turn</span></div>
60174
+ return `<div class="card" style="border-left:5px solid var(--info)">
60175
+ <div class="sectionhead" style="margin:0 0 6px">Sustaining now <span class="count">spend an action each turn</span></div>
59892
60176
  ${list.map(slug=>{const s=findSpell(slug); return `<div class="susrow"><span>${escapeHtml(s.name)}</span><button class="castbtn zero" onclick="endSustain('${slug}')">✓ End</button></div>`;}).join("")}
59893
60177
  </div>`;
59894
60178
  }
@@ -59931,8 +60215,8 @@ function renderBrowseFilters(){
59931
60215
  function renderRankChips(){
59932
60216
  const ranks=[["all","All"],["0","Cantrips"]];
59933
60217
  for(let r=1;r<=10;r++) ranks.push([String(r),"Rank "+r]);
59934
- if(hasFocus()) ranks.push(["focus","🔵 Focus"]);
59935
- ranks.push(["rituals","📜 Rituals"]);
60218
+ if(hasFocus()) ranks.push(["focus","Focus"]);
60219
+ ranks.push(["rituals","Rituals"]);
59936
60220
  document.getElementById("rankChips").innerHTML=
59937
60221
  ranks.map(([v,l])=>`<button class="chip ${browseRank===v?"on":""}" onclick="setBrowseRank('${v}')">${l}</button>`).join("");
59938
60222
  }
@@ -59967,7 +60251,7 @@ function renderBrowse(){
59967
60251
  }
59968
60252
  /* Lightweight collapsed row; the full card renders on demand when tapped. */
59969
60253
  function browseRowHTML(s){
59970
- const save=s.save?` · 🛡 ${escapeHtml(s.save)}`:"";
60254
+ const save=s.save?` · ${escapeHtml(s.save)}`:"";
59971
60255
  const note=legacyNote(s)?` <span class="formerly">${escapeHtml(legacyNote(s))}</span>`:"";
59972
60256
  return `<div class="browse-row rank${s.rank}">
59973
60257
  <button class="browse-row-head" onclick="toggleBrowse('${s.slug}',this)">
@@ -60078,6 +60362,14 @@ function downloadOffline(){
60078
60362
 
60079
60363
  function init(){
60080
60364
  setupInstall();
60365
+ applyTheme();
60366
+ try{
60367
+ if(window.matchMedia){
60368
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{
60369
+ if(((library.settings&&library.settings.themeMode)||"auto")==="auto") applyTheme();
60370
+ });
60371
+ }
60372
+ }catch(e){}
60081
60373
  if(!state.classId){ showClassPicker(); }
60082
60374
  else { renderHeader(); go(state.prepared?"today":"prepare"); }
60083
60375
  }