pf2e-spellbook 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.html CHANGED
@@ -3,168 +3,245 @@
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
+ a.btn{display:flex;align-items:center;justify-content:center;gap:8px;text-decoration:none;text-align:center;}
199
+ a.btn .icn{width:1.15em;height:1.15em;}
200
+ .linklike{background:none;border:none;color:var(--info);font-weight:700;text-decoration:underline;padding:0;font-size:inherit;}
130
201
 
131
202
  label.field{display:block;margin:14px 0;}
132
- label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:1.02rem;}
203
+ label.field .name{display:block;font-weight:700;margin-bottom:6px;font-size:1rem;}
133
204
  label.field .hint{display:block;color:var(--muted);font-size:.82rem;font-weight:400;margin-bottom:6px;}
205
+ input,select,textarea{accent-color:var(--accent);}
134
206
  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);
207
+ width:100%;padding:13px;font-size:1.05rem;border-radius:12px;
208
+ background:var(--surface-2);color:var(--ink);border:1px solid var(--line);
137
209
  appearance:none;-webkit-appearance:none;
138
210
  }
139
- select{background-image:linear-gradient(45deg,transparent 50%,var(--gold) 50%),linear-gradient(135deg,var(--gold) 50%,transparent 50%);
211
+ input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent);}
212
+ select{background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);
140
213
  background-position:calc(100% - 20px) center,calc(100% - 14px) center;
141
214
  background-size:6px 6px,6px 6px;background-repeat:no-repeat;padding-right:40px;}
215
+ input[type="color"]{padding:4px;height:44px;cursor:pointer;}
142
216
  .row{display:flex;gap:10px;}
143
217
  .row > *{flex:1;}
144
218
 
145
219
  .seg{display:flex;gap:8px;margin:6px 0;}
146
220
  .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;
221
+ flex:1;padding:13px;border-radius:12px;border:1px solid var(--line);
222
+ background:var(--surface-2);color:var(--ink);font-weight:700;
223
+ display:inline-flex;align-items:center;justify-content:center;gap:8px;
149
224
  }
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);}
225
+ .seg button.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
226
+ .seg button.on.heal{background:var(--heal);color:#08140d;border-color:var(--heal);}
227
+ .seg button.on.harm{background:var(--harm);color:#1c0709;border-color:var(--harm);}
152
228
 
153
229
  /* ---- Slot picker rows ---- */
154
230
  .slotgroup{margin:18px 0;}
155
- .slotgroup h3{color:var(--gold);font-size:1.1rem;border-bottom:1px solid var(--line);padding-bottom:6px;}
231
+ .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
232
  .slotrow{display:flex;align-items:center;gap:10px;margin:10px 0;}
157
233
  .slotrow .num{
158
234
  width:34px;height:34px;flex:0 0 34px;border-radius:50%;
159
- background:var(--panel2);border:1px solid var(--gold-dim);color:var(--gold);
235
+ background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--accent-text);
160
236
  display:flex;align-items:center;justify-content:center;font-weight:800;
161
237
  }
238
+ .slotrow .num .icn{width:1.1rem;height:1.1rem;}
162
239
  .slotrow select{flex:1;}
163
240
  .prep-slot{margin:10px 0;}
164
241
  .prep-slot .slotrow{margin:0;}
165
242
  .prep-preview{margin:6px 0 0 44px;}
166
243
  .prep-preview-inner{
167
- background:var(--panel2);border:1px solid var(--line);border-radius:10px;
244
+ background:var(--surface-2);border:1px solid var(--line);border-radius:10px;
168
245
  padding:10px 12px;
169
246
  }
170
247
  .prep-preview .meta{font-size:.85rem;margin:2px 0;}
@@ -173,123 +250,128 @@ select{background-image:linear-gradient(45deg,transparent 50%,var(--gold) 50%),l
173
250
 
174
251
  /* ---- Today cast cards ---- */
175
252
  .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);}
253
+ background:var(--surface);border:1px solid var(--line);border-radius:14px;
254
+ padding:14px;margin:12px 0;border-left:5px solid var(--line);box-shadow:var(--shadow);
255
+ }
256
+ .cast-card.rank0{border-left-color:var(--rank0);}
257
+ .cast-card.rank1{border-left-color:var(--rank1);}
258
+ .cast-card.rank2{border-left-color:var(--rank2);}
259
+ .cast-card.rank3{border-left-color:var(--rank3);}
260
+ .cast-card.rank4{border-left-color:var(--rank4);}
261
+ .cast-card.rank5{border-left-color:var(--rank5);}
262
+ .cast-card.rank6{border-left-color:var(--rank6);}
263
+ .cast-card.rank7{border-left-color:var(--rank7);}
264
+ .cast-card.rank8{border-left-color:var(--rank8);}
265
+ .cast-card.rank9{border-left-color:var(--rank9);}
266
+ .cast-card.rank10{border-left-color:var(--rank10);}
190
267
  .cast-card.heal{border-left-color:var(--heal);}
191
268
  .cast-card.harm{border-left-color:var(--harm);}
192
- .cast-card.spent{opacity:.4;}
269
+ .cast-card.spent{opacity:.45;}
193
270
  .cast-top{display:flex;justify-content:space-between;align-items:center;gap:10px;}
194
- .cast-top .nm{font-size:1.2rem;font-weight:700;}
271
+ .cast-top .nm{font-size:1.15rem;font-weight:700;}
195
272
  .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);}
273
+ .pip{width:24px;height:24px;border-radius:50%;border:2px solid var(--accent);display:inline-block;}
274
+ .pip.full{background:var(--accent);}
198
275
  .pip.spent{background:transparent;opacity:.35;}
199
276
  .castbtn{
200
- padding:10px 16px;border-radius:10px;border:none;background:var(--gold);
201
- color:#2a1c0c;font-weight:800;font-size:1rem;
277
+ padding:10px 16px;border-radius:10px;border:none;background:var(--accent);
278
+ color:var(--accent-ink);font-weight:800;font-size:1rem;
202
279
  }
203
- .castbtn.zero{background:var(--panel2);color:var(--muted);}
280
+ .castbtn.zero{background:var(--surface-2);color:var(--muted);}
204
281
  .unlim{font-size:.85rem;color:var(--heal);font-weight:700;}
205
282
  .detailsbtn{
206
- background:none;border:none;color:var(--accent);font-weight:700;
283
+ background:none;border:none;color:var(--info);font-weight:700;
207
284
  padding:8px 0 0;font-size:.95rem;text-decoration:underline;
208
285
  }
209
286
 
210
287
  .susbtn{
211
- padding:10px 14px;border-radius:10px;border:1px solid var(--accent);
212
- background:#11305a;color:#cfe5f7;font-weight:800;font-size:.95rem;
288
+ padding:10px 14px;border-radius:10px;border:1px solid var(--info);
289
+ background:var(--info-bg);color:var(--info-ink);font-weight:800;font-size:.95rem;
213
290
  }
214
- .susbtn.on{background:var(--accent);color:#0c1d2a;}
291
+ .susbtn.on{background:var(--info);color:var(--surface);}
215
292
  .susrow{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:6px 0;border-top:1px solid var(--line);}
216
293
  .susrow:first-of-type{border-top:none;}
217
294
  .susrow span{font-weight:700;}
218
295
 
219
296
  .dmgline{margin:4px 0 2px;}
220
297
  .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;
298
+ display:inline-block;background:var(--surface-2);border:1px solid var(--line);
299
+ color:var(--ink);font-weight:800;font-size:.92rem;border-radius:8px;padding:2px 10px;
223
300
  }
301
+ .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);}
302
+ .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
303
 
225
304
  .empty{text-align:center;color:var(--muted);padding:40px 20px;}
226
305
 
227
306
  .filterbar{display:flex;gap:8px;margin:6px 0 14px;flex-wrap:wrap;}
228
307
  .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;
308
+ padding:8px 14px;border-radius:20px;border:1px solid var(--line);
309
+ background:var(--surface-2);color:var(--muted);font-weight:700;font-size:.88rem;
310
+ display:inline-flex;align-items:center;gap:6px;
231
311
  }
232
- .chip.on{background:var(--gold);color:#2a1c0c;border-color:var(--gold);}
312
+ .chip.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
233
313
  #search{margin-bottom:8px;}
234
314
  #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;}
315
+ #browseFilters{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin:0 0 12px;}
236
316
  .fgroup{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:4px 0;}
237
317
  .fgroup .flabel{font-size:.8rem;color:var(--muted);font-weight:700;min-width:58px;}
238
318
 
239
319
  .sectionhead{
240
- color:var(--gold);font-size:1.15rem;font-weight:800;margin:22px 0 4px;
320
+ color:var(--accent-text);font-size:1.1rem;font-weight:800;margin:22px 0 4px;
241
321
  display:flex;align-items:center;gap:10px;
242
322
  }
243
- .sectionhead .count{font-size:.85rem;color:var(--muted);font-weight:600;}
323
+ .sectionhead .count{font-size:.82rem;color:var(--muted);font-weight:600;}
244
324
  .divider{height:1px;background:var(--line);margin:4px 0 6px;}
245
325
 
246
326
  .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;
327
+ background:var(--accent-soft);border:1px solid var(--accent-dim);border-radius:12px;
328
+ padding:12px 14px;margin:12px 0;color:var(--ink);font-size:.9rem;
249
329
  }
250
- .banner b{color:var(--gold);}
330
+ .banner b{color:var(--accent-text);}
251
331
 
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;}
332
+ details.help{background:var(--surface);border:1px solid var(--line);border-radius:14px;margin:12px 0;padding:0 16px;}
333
+ details.help summary{padding:16px 0;font-weight:700;font-size:1.05rem;color:var(--accent-text);cursor:pointer;list-style:none;}
254
334
  details.help summary::-webkit-details-marker{display:none;}
255
335
  details.help summary::after{content:"+";float:right;color:var(--muted);}
256
336
  details.help[open] summary::after{content:"-";}
257
337
  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);}
338
+ details.help .body b{color:var(--accent-text);}
259
339
 
260
340
  /* ---- Class picker ---- */
261
341
  .classcard{
262
342
  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);
343
+ background:var(--surface);border:1px solid var(--line);border-left:4px solid var(--accent);
344
+ border-radius:var(--radius);padding:16px;margin:12px 0;color:var(--ink);box-shadow:var(--shadow);
265
345
  }
266
346
  .classcard:active{transform:translateY(1px);}
267
- .classcard .ic{font-size:2.2rem;line-height:1;}
347
+ .classcard .ic{font-size:2rem;line-height:1;display:inline-flex;color:var(--accent-text);}
268
348
  .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;}
349
+ .classcard .nm{font-size:1.25rem;font-weight:800;color:var(--ink);}
350
+ .classcard .tl{font-size:.88rem;color:var(--muted);margin-top:2px;}
351
+ .classcard .arr{color:var(--muted);font-size:1.3rem;font-weight:800;}
272
352
  .previewtag{
273
353
  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;
354
+ background:color-mix(in srgb, var(--harm) 15%, var(--surface));
355
+ border:1px solid color-mix(in srgb, var(--harm) 40%, var(--line));
356
+ color:var(--harm);border-radius:6px;padding:1px 6px;font-weight:700;
275
357
  }
276
358
 
277
359
  /* ---- Browse compact rows (tap to expand) ---- */
278
360
  .browse-row{
279
- background:var(--panel);border:1px solid var(--line);border-left:5px solid var(--line);
361
+ background:var(--surface);border:1px solid var(--line);border-left:4px solid var(--line);
280
362
  border-radius:12px;margin:8px 0;overflow:hidden;
281
363
  }
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);}
364
+ .browse-row.rank0{border-left-color:var(--rank0);}
365
+ .browse-row.rank1{border-left-color:var(--rank1);}
366
+ .browse-row.rank2{border-left-color:var(--rank2);}
367
+ .browse-row.rank3{border-left-color:var(--rank3);}
368
+ .browse-row.rank4{border-left-color:var(--rank4);}
369
+ .browse-row.rank5{border-left-color:var(--rank5);}
370
+ .browse-row.rank6{border-left-color:var(--rank6);}
371
+ .browse-row.rank7{border-left-color:var(--rank7);}
372
+ .browse-row.rank8{border-left-color:var(--rank8);}
373
+ .browse-row.rank9{border-left-color:var(--rank9);}
374
+ .browse-row.rank10{border-left-color:var(--rank10);}
293
375
  .browse-row-head{
294
376
  display:flex;flex-direction:column;align-items:flex-start;gap:2px;
295
377
  width:100%;text-align:left;background:none;border:none;color:var(--ink);
@@ -297,75 +379,83 @@ details.help .body b{color:var(--gold);}
297
379
  }
298
380
  .browse-row-head::after{content:"▾";position:absolute;right:14px;top:14px;color:var(--muted);}
299
381
  .browse-row-head.open::after{content:"▴";}
300
- .browse-row-head .brn{font-size:1.1rem;font-weight:700;}
382
+ .browse-row-head .brn{font-size:1.05rem;font-weight:700;}
301
383
  .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;}
384
+ .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
385
  .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;}
386
+ .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
387
  .browse-card{padding:0 12px 4px;}
306
388
  .browse-card .card{margin:0 0 10px;}
307
389
 
308
390
  /* ---- Header menu button + character menu ---- */
309
391
  .tophead{display:flex;align-items:center;justify-content:space-between;gap:10px;}
310
392
  .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);
393
+ flex:0 0 auto;background:var(--surface-2);border:1px solid var(--line);
394
+ border-radius:12px;width:44px;height:44px;font-size:1.3rem;line-height:1;color:var(--accent-text);
395
+ display:inline-flex;align-items:center;justify-content:center;
313
396
  }
314
- .classcard.activechar{border-left-color:var(--gold);background:#332615;}
397
+ .classcard.activechar{border-left-color:var(--accent);background:var(--accent-soft);}
315
398
  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);
399
+ width:100%;padding:13px;font-size:1rem;border-radius:12px;line-height:1.4;
400
+ background:var(--surface-2);color:var(--ink);border:1px solid var(--line);
318
401
  font-family:inherit;resize:vertical;margin:6px 0;
319
402
  }
320
403
 
404
+ /* ---- Appearance / custom palette ---- */
405
+ .swatchrow{display:flex;align-items:center;gap:10px;margin:8px 0;}
406
+ .swatchrow label{flex:1;font-weight:600;font-size:.95rem;}
407
+ .swatchrow input[type="color"]{flex:0 0 56px;width:56px;}
408
+
321
409
  /* ---- Repertoire builder (spontaneous) ---- */
322
410
  .rep-row .slotrow{gap:8px;}
323
411
  .sigstar{
324
412
  width:38px;height:38px;flex:0 0 38px;border-radius:10px;
325
- background:var(--panel2);border:1px solid var(--gold-dim);color:var(--muted);
413
+ background:var(--surface-2);border:1px solid var(--accent-dim);color:var(--muted);
326
414
  font-size:1.2rem;line-height:1;display:flex;align-items:center;justify-content:center;
327
415
  }
328
- .sigstar.on{background:var(--gold);color:#2a1c0c;border-color:var(--gold);}
416
+ .sigstar.on{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
329
417
  .rmrow{
330
418
  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;
419
+ background:var(--surface-2);border:1px solid var(--line);color:var(--muted);font-weight:700;
332
420
  }
333
- .focus-checklist{max-height:320px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--panel2);}
421
+ .focus-checklist{max-height:320px;overflow-y:auto;border:1px solid var(--line);border-radius:12px;margin-top:8px;background:var(--surface-2);}
334
422
  .fcheck{display:flex;align-items:center;gap:10px;padding:11px 12px;border-bottom:1px solid var(--line);cursor:pointer;}
335
423
  .fcheck:last-child{border-bottom:none;}
336
424
  .fcheck input{width:22px;height:22px;flex:0 0 22px;}
337
425
  .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;}
426
+ .fcheck .fcrank{font-size:.75rem;color:var(--muted);background:var(--surface);border:1px solid var(--line);border-radius:6px;padding:1px 7px;}
339
427
 
340
428
  .btn.addrow{
341
429
  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);
430
+ font-size:.95rem;font-weight:700;border-radius:10px;box-shadow:none;
431
+ background:var(--surface-2);color:var(--accent-text);border:1px solid var(--accent-dim);
344
432
  }
345
433
  .slottag{
346
- font-size:.85rem;background:var(--panel2);border:1px solid var(--gold-dim);
347
- border-radius:6px;padding:1px 6px;margin-right:4px;
434
+ display:inline-flex;align-items:center;gap:4px;
435
+ font-size:.85rem;background:var(--surface-2);border:1px solid var(--accent-dim);
436
+ color:var(--accent-text);border-radius:6px;padding:1px 6px;margin-right:4px;
348
437
  }
438
+ .slottag .icn{width:.95rem;height:.95rem;}
349
439
 
350
440
  /* ---- Bottom nav ---- */
351
441
  nav.bottom{
352
442
  position:fixed;bottom:0;left:0;right:0;z-index:30;
353
- background:#241810;border-top:2px solid var(--gold-dim);
443
+ background:var(--surface);border-top:1px solid var(--line);
354
444
  display:flex;justify-content:space-around;
355
445
  padding:6px 4px calc(6px + env(safe-area-inset-bottom));
356
446
  }
357
447
  nav.bottom button{
358
448
  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;
449
+ display:flex;flex-direction:column;align-items:center;gap:3px;
450
+ padding:8px 2px;font-size:.7rem;font-weight:700;
361
451
  }
362
- nav.bottom button .ic{font-size:1.5rem;line-height:1;}
363
- nav.bottom button.on{color:var(--gold);}
452
+ nav.bottom button .ic{font-size:1.4rem;line-height:1;display:inline-flex;}
453
+ nav.bottom button.on{color:var(--accent-text);}
364
454
  .toast{
365
455
  position:fixed;bottom:96px;left:50%;transform:translateX(-50%);
366
- background:var(--gold);color:#2a1c0c;font-weight:800;padding:12px 20px;
456
+ background:var(--accent);color:var(--accent-ink);font-weight:800;padding:12px 20px;
367
457
  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);
458
+ box-shadow:0 4px 16px rgba(0,0,0,.35);
369
459
  }
370
460
  .toast.show{opacity:1;}
371
461
 
@@ -375,8 +465,8 @@ nav.bottom button.on{color:var(--gold);}
375
465
 
376
466
  <header class="top hide">
377
467
  <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>
468
+ <h1><span id="charIcon"></span> <span id="charNameHead">Spellbook</span></h1>
469
+ <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
470
  </div>
381
471
  <div class="sub" id="charSub">Pathfinder 2e (Remaster)</div>
382
472
  <div class="statbar" id="statbar">
@@ -391,11 +481,14 @@ nav.bottom button.on{color:var(--gold);}
391
481
 
392
482
  <!-- ================= CHARACTER MENU ================= -->
393
483
  <section id="view-menu" class="wrap hide">
394
- <h2>📲 Use offline</h2>
484
+ <h2>Appearance</h2>
485
+ <div id="appearancePanel"></div>
486
+
487
+ <h2 style="margin-top:24px">Use offline</h2>
395
488
  <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
489
  <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>
490
+ <button class="btn" onclick="downloadOffline()">Save offline copy</button>
491
+ <button class="btn secondary" id="installBtn" style="display:none" onclick="installApp()">Install app</button>
399
492
  </div>
400
493
  <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
494
 
@@ -407,10 +500,15 @@ nav.bottom button.on{color:var(--gold);}
407
500
  <h2 style="margin-top:24px">Share &amp; back up</h2>
408
501
  <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
502
  <div class="row">
410
- <button class="btn secondary" onclick="exportCharacter()">⬆ Export active</button>
411
- <button class="btn secondary" onclick="importCharacter()">⬇ Import code</button>
503
+ <button class="btn secondary" onclick="exportCharacter()">Export active</button>
504
+ <button class="btn secondary" onclick="importCharacter()">Import code</button>
412
505
  </div>
413
506
  <textarea id="menuIO" rows="3" placeholder="Character code appears here on Export — or paste one here, then tap Import."></textarea>
507
+
508
+ <h2 style="margin-top:24px">Support</h2>
509
+ <p class="meta">This spellbook is a free fan project. If it has earned a place at your table, you can help keep it going:</p>
510
+ <a class="btn secondary kofi" href="https://ko-fi.com/whytevoyd" target="_blank" rel="noopener"><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.5 8h13v6.5a4 4 0 0 1-4 4h-5a4 4 0 0 1-4-4z"/><path d="M16.5 9h1.75a2.75 2.75 0 1 1 0 5.5H16.5"/><path d="M10 15.2c-1.8-1.1-2.7-2.2-2.7-3.2 0-.9.7-1.6 1.6-1.6.5 0 .9.2 1.1.5.2-.3.6-.5 1.1-.5.9 0 1.6.7 1.6 1.6 0 1-.9 2.1-2.7 3.2z" fill="currentColor" stroke-width="0"/></svg> Support on Ko-fi</a>
511
+
414
512
  <button class="btn secondary" onclick="closeMenu()">← Back</button>
415
513
  <p class="meta" style="text-align:center;margin-top:18px;font-size:.8rem">Spell data: Paizo (OGL/ORC) via the Foundry VTT pf2e project · unofficial fan tool<br><span id="dataStamp"></span></p>
416
514
  </section>
@@ -428,7 +526,7 @@ nav.bottom button.on{color:var(--gold);}
428
526
  <!-- ================= PREPARE / REPERTOIRE ================= -->
429
527
  <section id="view-prepare" class="wrap hide">
430
528
  <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>
529
+ <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
530
 
433
531
  <h2>① Your character</h2>
434
532
  <p class="meta">Copy these from your character sheet.</p>
@@ -465,9 +563,9 @@ nav.bottom button.on{color:var(--gold);}
465
563
  <!-- ================= BROWSE ================= -->
466
564
  <section id="view-browse" class="wrap hide">
467
565
  <h2 id="browseTitle">All spells</h2>
468
- <input type="text" id="search" placeholder="🔍 Search spells…" oninput="renderBrowse()">
566
+ <input type="text" id="search" placeholder="Search spells…" oninput="renderBrowse()">
469
567
  <div class="filterbar" id="rankChips"></div>
470
- <button class="chip" id="filterToggle" onclick="toggleBrowseFilters()" aria-expanded="false">⚙ Filters</button>
568
+ <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
569
  <div id="browseFilters" class="hide"></div>
472
570
  <div id="browseList"></div>
473
571
  </section>
@@ -482,10 +580,10 @@ nav.bottom button.on{color:var(--gold);}
482
580
  <div class="toast" id="toast"></div>
483
581
 
484
582
  <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>
583
+ <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>
584
+ <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>
585
+ <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>
586
+ <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
587
  </nav>
490
588
 
491
589
  <script>
@@ -58710,6 +58808,46 @@ function psychicSlots(level){
58710
58808
  return slots;
58711
58809
  }
58712
58810
 
58811
+ /* ============================================================
58812
+ INLINE SVG ICON REGISTRY
58813
+ Thin line icons (24x24, stroke = currentColor) so they inherit the
58814
+ active accent colour. Keyed by name; classes/features reference a key
58815
+ instead of an emoji. iconSvg() wraps the path data in an <svg>.
58816
+ ============================================================ */
58817
+ const ICONS={
58818
+ /* classes */
58819
+ 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"/>',
58820
+ wizard:'<path d="M12 3.5 4.5 19.5h15z"/><circle cx="12" cy="13.5" r="1.9"/>',
58821
+ 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"/>',
58822
+ 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"/>',
58823
+ 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"/>',
58824
+ 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"/>',
58825
+ 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"/>',
58826
+ 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"/>',
58827
+ summoner:'<circle cx="9.2" cy="12" r="4.8"/><circle cx="14.8" cy="12" r="4.8"/>',
58828
+ 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"/>',
58829
+ 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"/>',
58830
+ /* navigation */
58831
+ 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"/>',
58832
+ 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"/>',
58833
+ 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"/>',
58834
+ 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"/>',
58835
+ /* features / utility */
58836
+ 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"/>',
58837
+ 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"/>',
58838
+ heal:'<path d="M12 5.5v13M5.5 12h13"/>',
58839
+ 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"/>',
58840
+ menu:'<circle cx="12" cy="8" r="3.4"/><path d="M5.5 20a6.5 6.5 0 0 1 13 0"/>',
58841
+ 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"/>',
58842
+ plus:'<path d="M12 5.5v13M5.5 12h13"/>',
58843
+ 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"/>',
58844
+ };
58845
+ function iconSvg(name, cls){
58846
+ return '<svg class="icn '+(cls||"")+'" viewBox="0 0 24 24" fill="none" stroke="currentColor" '+
58847
+ 'stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+
58848
+ (ICONS[name]||"")+'</svg>';
58849
+ }
58850
+
58713
58851
  /* [minLevel, proficiency bonus] for spell DC / spell attack, highest first.
58714
58852
  Bonus = 2/4/6/8 for trained/expert/master/legendary.
58715
58853
  Wizard, Druid, Sorcerer, Bard, Oracle, Psychic and the Cloistered Cleric
@@ -58757,13 +58895,13 @@ const CORE_HELP=[
58757
58895
  ["Remaster vs. legacy (original) rules",
58758
58896
  "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
58897
  ["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."],
58898
+ "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
58899
  ];
58762
58900
 
58763
58901
  const CLASSES={
58764
58902
  /* ---------------- CLERIC ---------------- */
58765
58903
  cleric:{
58766
- id:"cleric", name:"Cleric", icon:"",
58904
+ id:"cleric", name:"Cleric", icon:"cleric", color:"#d8a23a",
58767
58905
  tagline:"Divine prepared caster · the party's main healer",
58768
58906
  tradition:"divine", keyAbility:"Wisdom", keyAbilityShort:"Wis",
58769
58907
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
@@ -58786,63 +58924,59 @@ const CLASSES={
58786
58924
 
58787
58925
  /* ---------------- WIZARD ---------------- */
58788
58926
  wizard:{
58789
- id:"wizard", name:"Wizard", icon:"🪄",
58927
+ id:"wizard", name:"Wizard", icon:"wizard", color:"#7b6cf0",
58790
58928
  tagline:"Arcane prepared caster · the widest spell list",
58791
58929
  tradition:"arcane", keyAbility:"Intelligence", keyAbilityShort:"Int",
58792
58930
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58793
- preview:true,
58794
58931
  features:[
58795
- {type:"extraSlots", key:"school", icon:"🎓", label:"Arcane school slot",
58932
+ {type:"extraSlots", key:"school", icon:"school", label:"Arcane school slot",
58796
58933
  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,
58934
+ {type:"dailyResource", key:"bonded", icon:"bonded", label:"Drain Bonded Item", uses:1,
58798
58935
  note:"Once per day, recast a spell you already cast today without spending its slot."},
58799
58936
  ],
58800
58937
  help:[
58801
58938
  ["What does a wizard do?",
58802
58939
  "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
58940
  ["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."],
58941
+ "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
58942
  ["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."],
58943
+ "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
58944
  ],
58808
58945
  },
58809
58946
 
58810
58947
  /* ---------------- DRUID ---------------- */
58811
58948
  druid:{
58812
- id:"druid", name:"Druid", icon:"🌿",
58949
+ id:"druid", name:"Druid", icon:"druid", color:"#4faf6d",
58813
58950
  tagline:"Primal prepared caster · nature's versatility",
58814
58951
  tradition:"primal", keyAbility:"Wisdom", keyAbilityShort:"Wis",
58815
58952
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58816
- preview:true,
58817
58953
  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
58954
  help:[
58819
58955
  ["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)."],
58956
+ "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
58957
  ],
58822
58958
  },
58823
58959
 
58824
58960
  /* ---------------- WITCH ---------------- */
58825
58961
  witch:{
58826
- id:"witch", name:"Witch", icon:"🔮",
58962
+ id:"witch", name:"Witch", icon:"witch", color:"#b061b8",
58827
58963
  tagline:"Prepared caster · your patron sets the tradition",
58828
58964
  traditionFrom:"patron", defaultTradition:"occult",
58829
58965
  keyAbility:"Intelligence", keyAbilityShort:"Int",
58830
58966
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58831
- preview:true,
58832
58967
  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
58968
  help:[
58834
58969
  ["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)."],
58970
+ "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
58971
  ],
58837
58972
  },
58838
58973
 
58839
58974
  /* ---------------- SORCERER ---------------- */
58840
58975
  sorcerer:{
58841
- id:"sorcerer", name:"Sorcerer", icon:"",
58976
+ id:"sorcerer", name:"Sorcerer", icon:"sorcerer", color:"#e05a52",
58842
58977
  tagline:"Spontaneous caster · magic in the blood",
58843
58978
  traditionFrom:"bloodline", keyAbility:"Charisma", keyAbilityShort:"Cha",
58844
58979
  casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58845
- preview:true,
58846
58980
  features:[
58847
58981
  {type:"signature"},
58848
58982
  {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 +58991,17 @@ const CLASSES={
58857
58991
 
58858
58992
  /* ---------------- BARD ---------------- */
58859
58993
  bard:{
58860
- id:"bard", name:"Bard", icon:"🎵",
58994
+ id:"bard", name:"Bard", icon:"bard", color:"#34a8c4",
58861
58995
  tagline:"Occult spontaneous caster · master of support",
58862
58996
  tradition:"occult", keyAbility:"Charisma", keyAbilityShort:"Cha",
58863
58997
  casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58864
- preview:true,
58865
58998
  features:[
58866
58999
  {type:"signature"},
58867
59000
  {type:"note", title:"Compositions & muse", text:"Add your composition focus spells (Courageous Anthem, Counter Performance…) in the Focus section below."},
58868
59001
  ],
58869
59002
  help:[
58870
59003
  ["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)."],
59004
+ "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
59005
  ["Signature spells",
58873
59006
  "Mark spells as <b>signature</b> (the ★) to heighten them with higher-rank slots. One per rank is the usual choice."],
58874
59007
  ],
@@ -58876,30 +59009,35 @@ const CLASSES={
58876
59009
 
58877
59010
  /* ---------------- ORACLE ---------------- */
58878
59011
  oracle:{
58879
- id:"oracle", name:"Oracle", icon:"👁",
59012
+ id:"oracle", name:"Oracle", icon:"oracle", color:"#c061d6",
58880
59013
  tagline:"Divine spontaneous caster · power at a price",
58881
59014
  tradition:"divine", keyAbility:"Charisma", keyAbilityShort:"Cha",
58882
59015
  casting:"spontaneous", slots:"full", prof:FULL_CASTER_PROF, cantrips:5,
58883
- preview:true,
58884
59016
  features:[
58885
59017
  {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."},
59018
+ {type:"stageTracker", key:"curse", icon:"oracle", label:"Oracular curse", max:4,
59019
+ note:"Your curse deepens by a stage each time you use a cursebound action — stronger effects, worse drawbacks. Resting clears it."},
59020
+ {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
59021
  ],
58888
59022
  help:[
58889
59023
  ["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)."],
59024
+ "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
59025
  ["Signature spells",
58892
59026
  "Mark spells as <b>signature</b> (the ★) to heighten them with higher-rank slots — usually one per spell rank."],
58893
59027
  ],
58894
59028
  },
58895
59029
  /* ---------------- MAGUS ---------------- */
58896
59030
  magus:{
58897
- id:"magus", name:"Magus", icon:"⚔️",
59031
+ id:"magus", name:"Magus", icon:"magus", color:"#5d82e0",
58898
59032
  tagline:"Arcane half-caster · blade and spell as one",
58899
59033
  tradition:"arcane", keyAbility:"Intelligence", keyAbilityShort:"Int",
58900
59034
  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."}],
59035
+ features:[
59036
+ {type:"studiousSlots", key:"studious", minLevel:7, rank:2, count:2, label:"Studious spells",
59037
+ spells:["gecko-grip","sure-strike","water-breathing"],
59038
+ 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."},
59039
+ {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."},
59040
+ ],
58903
59041
  help:[
58904
59042
  ["What does a magus do?",
58905
59043
  "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 +59048,13 @@ const CLASSES={
58910
59048
 
58911
59049
  /* ---------------- SUMMONER ---------------- */
58912
59050
  summoner:{
58913
- id:"summoner", name:"Summoner", icon:"👹",
59051
+ id:"summoner", name:"Summoner", icon:"summoner", color:"#2bb59a",
58914
59052
  tagline:"Half-caster · you and your eidolon, as one",
58915
59053
  traditionFrom:"patron", defaultTradition:"arcane",
58916
59054
  traditionChoiceLabel:"Eidolon's tradition",
58917
59055
  traditionChoiceHint:"your eidolon's essence sets your spell tradition",
58918
59056
  keyAbility:"Charisma", keyAbilityShort:"Cha",
58919
59057
  casting:"spontaneous", slots:"partial", prof:PARTIAL_CASTER_PROF, cantrips:5,
58920
- preview:true,
58921
59058
  features:[
58922
59059
  {type:"signature"},
58923
59060
  {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 +59069,15 @@ const CLASSES={
58932
59069
 
58933
59070
  /* ---------------- PSYCHIC ---------------- */
58934
59071
  psychic:{
58935
- id:"psychic", name:"Psychic", icon:"🧠",
59072
+ id:"psychic", name:"Psychic", icon:"psychic", color:"#e05fa6",
58936
59073
  tagline:"Occult spontaneous caster · few spells, mighty cantrips",
58937
59074
  tradition:"occult", keyAbility:"Int or Cha", keyAbilityShort:"Key",
58938
- casting:"spontaneous", slots:"psychic", prof:FULL_CASTER_PROF, cantrips:5,
58939
- preview:true,
59075
+ casting:"spontaneous", slots:"psychic", prof:FULL_CASTER_PROF, cantrips:3,
58940
59076
  features:[
58941
59077
  {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."},
59078
+ {type:"dailyResource", key:"unleash", icon:"psychic", label:"Unleash Psyche", uses:1, verb:"Unleash", recharge:"refocus",
59079
+ note:"Surge your psyche for 2 rounds — bigger cantrips and better spell attacks. Afterward your mind is strained; Refocus to Unleash again."},
59080
+ {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
59081
  ],
58944
59082
  help:[
58945
59083
  ["What does a psychic do?",
@@ -58951,15 +59089,14 @@ const CLASSES={
58951
59089
 
58952
59090
  /* ---------------- ANIMIST ---------------- */
58953
59091
  animist:{
58954
- id:"animist", name:"Animist", icon:"🕯️",
59092
+ id:"animist", name:"Animist", icon:"animist", color:"#e08a3c",
58955
59093
  tagline:"Divine prepared caster · channels apparitions (spirits)",
58956
59094
  tradition:"divine", keyAbility:"Wisdom", keyAbilityShort:"Wis",
58957
59095
  casting:"prepared", slots:"full", prof:FULL_CASTER_PROF, cantrips:4,
58958
- preview:true,
58959
59096
  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
59097
  help:[
58961
59098
  ["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)."],
59099
+ "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
59100
  ["Cantrips & apparitions",
58964
59101
  "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
59102
  ],
@@ -59050,14 +59187,16 @@ function loadLibrary(){
59050
59187
  }catch(e){}
59051
59188
  try{ // migrate a single v2 character
59052
59189
  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}; }
59190
+ if(v2 && v2.classId){ const c=Object.assign(blankChar(), v2); return normalizeLib({characters:{[c.id]:c}, activeId:c.id}); }
59054
59191
  }catch(e){}
59055
59192
  const c=blankChar();
59056
- return {characters:{[c.id]:c}, activeId:c.id};
59193
+ return normalizeLib({characters:{[c.id]:c}, activeId:c.id});
59057
59194
  }
59195
+ function defaultSettings(){ return { themeMode:"auto", custom:null }; }
59058
59196
  function normalizeLib(lib){
59059
59197
  Object.keys(lib.characters).forEach(id=>{ lib.characters[id]=Object.assign(defaultStateFields(), lib.characters[id], {id}); });
59060
59198
  if(!lib.characters[lib.activeId]) lib.activeId=Object.keys(lib.characters)[0];
59199
+ lib.settings=Object.assign(defaultSettings(), lib.settings||{});
59061
59200
  return lib;
59062
59201
  }
59063
59202
  let _storageWarned=false;
@@ -59065,9 +59204,66 @@ function saveState(){
59065
59204
  library.characters[state.id]=state;
59066
59205
  try{ localStorage.setItem(LS_KEY, JSON.stringify(library)); }
59067
59206
  catch(e){
59068
- if(!_storageWarned && typeof toast==="function"){ toast("Couldn't save — storage full or disabled"); _storageWarned=true; }
59207
+ if(!_storageWarned && typeof toast==="function"){ toast("Couldn't save — storage full or disabled"); _storageWarned=true; }
59069
59208
  }
59070
59209
  }
59210
+ function saveSettings(){ try{ localStorage.setItem(LS_KEY, JSON.stringify(library)); }catch(e){} }
59211
+
59212
+ /* ============================================================
59213
+ THEME (light/dark · per-class accent · custom palette)
59214
+ The base palette flips with [data-theme]; --accent (and its readable
59215
+ ink) is set per active class, or overridden by a custom palette.
59216
+ ============================================================ */
59217
+ const DEFAULT_ACCENT="#6c7a89";
59218
+ function hexToRgb(h){
59219
+ h=(h||"").replace("#","").trim();
59220
+ if(h.length===3) h=h.split("").map(c=>c+c).join("");
59221
+ const n=parseInt(h||"000000",16);
59222
+ return {r:(n>>16)&255, g:(n>>8)&255, b:n&255};
59223
+ }
59224
+ function relLuminance(hex){
59225
+ const {r,g,b}=hexToRgb(hex);
59226
+ const f=v=>{ v/=255; return v<=.03928 ? v/12.92 : Math.pow((v+.055)/1.055,2.4); };
59227
+ return .2126*f(r)+.7152*f(g)+.0722*f(b);
59228
+ }
59229
+ function inkFor(hex){ return relLuminance(hex) < .48 ? "#ffffff" : "#15171c"; }
59230
+ function mixHex(a,b,t){
59231
+ const A=hexToRgb(a), B=hexToRgb(b);
59232
+ const c=k=>Math.round(A[k]+(B[k]-A[k])*t).toString(16).padStart(2,"0");
59233
+ return "#"+c("r")+c("g")+c("b");
59234
+ }
59235
+ function resolveThemeMode(){
59236
+ const m=(library.settings&&library.settings.themeMode)||"auto";
59237
+ if(m==="light"||m==="dark") return m;
59238
+ try{ return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light"; }
59239
+ catch(e){ return "dark"; }
59240
+ }
59241
+ function applyTheme(){
59242
+ const root=document.documentElement;
59243
+ const custom=(library.settings&&library.settings.custom)||null;
59244
+ root.dataset.theme=resolveThemeMode();
59245
+ const accent=(custom&&custom.accent) || (activeClass()&&activeClass().color) || DEFAULT_ACCENT;
59246
+ root.style.setProperty("--accent", accent);
59247
+ root.style.setProperty("--accent-ink", inkFor(accent));
59248
+ // optional full-palette overrides (background / text)
59249
+ if(custom&&custom.bg) root.style.setProperty("--bg", custom.bg); else root.style.removeProperty("--bg");
59250
+ if(custom&&custom.ink) root.style.setProperty("--ink", custom.ink); else root.style.removeProperty("--ink");
59251
+ if(custom&&custom.surface){
59252
+ root.style.setProperty("--surface", custom.surface);
59253
+ root.style.setProperty("--surface-2", mixHex(custom.surface, custom.ink||"#808080", .10));
59254
+ }else{ root.style.removeProperty("--surface"); root.style.removeProperty("--surface-2"); }
59255
+ // keep the browser chrome colour in step with the background
59256
+ const meta=document.querySelector('meta[name="theme-color"]');
59257
+ if(meta){ const bg=getComputedStyle(root).getPropertyValue("--bg").trim(); if(bg) meta.setAttribute("content", bg); }
59258
+ }
59259
+ function themeColorValue(token){ return (getComputedStyle(document.documentElement).getPropertyValue(token)||"").trim()||"#000000"; }
59260
+ function setThemeMode(m){ library.settings.themeMode=m; saveSettings(); applyTheme(); renderMenu(); }
59261
+ function setCustomColor(key,val){
59262
+ library.settings.custom=library.settings.custom||{};
59263
+ library.settings.custom[key]=val;
59264
+ saveSettings(); applyTheme();
59265
+ }
59266
+ function resetTheme(){ library.settings.custom=null; saveSettings(); applyTheme(); renderMenu(); }
59071
59267
 
59072
59268
  /* ============================================================
59073
59269
  CLASS-AWARE HELPERS
@@ -59130,7 +59326,7 @@ function actionLabel(a){
59130
59326
  }
59131
59327
  const m=String(a).match(/^([1-3])\s*(?:to|or|–|-)\s*([1-3])$/);
59132
59328
  if(m) return `${ACTION_GLYPH[m[1]]}–${ACTION_GLYPH[m[2]]} ${m[1]} to ${m[2]} actions`;
59133
- return "🕑 "+a;
59329
+ return a;
59134
59330
  }
59135
59331
  function titleCaseTrait(t){ return t.charAt(0).toUpperCase()+t.slice(1); }
59136
59332
  function escapeHtml(s){ return (s||"").replace(/[&<>]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[c])); }
@@ -59158,12 +59354,11 @@ function scaleDamage(s, castRank){
59158
59354
  }
59159
59355
  function damageChipHTML(s, castRank){
59160
59356
  const ds=scaleDamage(s, castRank); if(!ds) return "";
59161
- const parts=ds.map(d=>{
59162
- const icon=d.healing?"":"💥";
59357
+ return ds.map(d=>{
59358
+ const cls=d.healing?"heal":"dmg";
59163
59359
  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>`;
59360
+ return `<span class="dmgchip ${cls}">${escapeHtml(d.formula)}${ty}</span>`;
59361
+ }).join(" ");
59167
59362
  }
59168
59363
 
59169
59364
  function spellCardHTML(s, opts){
@@ -59176,7 +59371,7 @@ function spellCardHTML(s, opts){
59176
59371
  if(s.duration) m.push(`<b>Duration</b> ${escapeHtml(s.duration)}`);
59177
59372
  const meta = m.length?`<div class="meta">${m.join(" &nbsp;·&nbsp; ")}</div>`:"";
59178
59373
  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>` : "";
59374
+ const saveTag = s.save ? `<span class="save-tag">${escapeHtml(s.save)}</span>` : "";
59180
59375
  const heighten = s.heightened ? `<div class="heighten"><b>Heightened</b> ${textToHtml(s.heightened)}</div>` : "";
59181
59376
  const note=legacyNote(s)?`<div class="meta" style="margin:-2px 0 4px"><span class="formerly">${escapeHtml(legacyNote(s))}</span></div>`:"";
59182
59377
  return `
@@ -59199,7 +59394,7 @@ function spellCardHTML(s, opts){
59199
59394
  function spellPreviewHTML(s, castRank){
59200
59395
  if(!s) return "";
59201
59396
  const heightNote=(castRank>s.rank)?` · cast at rank ${castRank}`:"";
59202
- const saveTag=s.save?` · 🛡 ${escapeHtml(s.save)}`:"";
59397
+ const saveTag=s.save?` · ${escapeHtml(s.save)}`:"";
59203
59398
  const m=[];
59204
59399
  if(s.range) m.push(`<b>Range</b> ${escapeHtml(s.range)}`);
59205
59400
  if(s.area) m.push(`<b>Area</b> ${escapeHtml(s.area)}`);
@@ -59251,7 +59446,7 @@ function toast(msg){
59251
59446
  function charSummary(c){
59252
59447
  if(!c.classId) return "New character — no class yet";
59253
59448
  const cl=CLASSES[c.classId];
59254
- return `${cl.icon} ${cl.name} · Level ${c.level}`;
59449
+ return `${cl.name} · Level ${c.level}`;
59255
59450
  }
59256
59451
  function openMenu(){
59257
59452
  VIEWS.forEach(v=>document.getElementById("view-"+v).classList.add("hide"));
@@ -59268,18 +59463,33 @@ function renderMenu(){
59268
59463
  const c=library.characters[id];
59269
59464
  const active=id===library.activeId;
59270
59465
  return `<div class="classcard ${active?"activechar":""}">
59271
- <div class="ic">${c.classId?CLASSES[c.classId].icon:""}</div>
59466
+ <div class="ic" style="color:${c.classId?CLASSES[c.classId].color:"var(--muted)"}">${c.classId?iconSvg(CLASSES[c.classId].icon):iconSvg("plus")}</div>
59272
59467
  <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>
59468
+ <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
59469
  <div class="tl">${charSummary(c)}</div>
59275
59470
  </div>
59276
59471
  ${ids.length>1?`<button class="rmrow" title="delete" onclick="deleteCharacter('${id}')">✕</button>`:""}
59277
59472
  </div>`;
59278
59473
  }).join("");
59279
59474
  document.getElementById("menuList").innerHTML=list;
59475
+ renderAppearance();
59280
59476
  showInstallButton(!!deferredInstall);
59281
59477
  const ds=document.getElementById("dataStamp"); if(ds) ds.textContent=dataStampText();
59282
59478
  }
59479
+ /* Appearance panel: Light/Dark/Auto + a four-swatch custom palette. */
59480
+ function renderAppearance(){
59481
+ const box=document.getElementById("appearancePanel"); if(!box) return;
59482
+ const mode=(library.settings&&library.settings.themeMode)||"auto";
59483
+ const seg=["light","dark","auto"].map(m=>
59484
+ `<button class="${mode===m?"on":""}" onclick="setThemeMode('${m}')">${m[0].toUpperCase()+m.slice(1)}</button>`).join("");
59485
+ const rows=[["Background","bg","--bg"],["Surface","surface","--surface"],["Text","ink","--ink"],["Accent","accent","--accent"]]
59486
+ .map(([label,key,token])=>`<div class="swatchrow"><label>${label}</label>
59487
+ <input type="color" value="${themeColorValue(token)}" oninput="setCustomColor('${key}',this.value)" aria-label="${label} colour"></div>`).join("");
59488
+ box.innerHTML=`<div class="seg" id="themeSeg">${seg}</div>
59489
+ <p class="meta">Colours follow each class's theme by default. Pick your own below — they apply to every character.</p>
59490
+ ${rows}
59491
+ <button class="btn secondary" onclick="resetTheme()">Reset to class default</button>`;
59492
+ }
59283
59493
  function newCharacter(){ const c=blankChar(); library.characters[c.id]=c; library.activeId=c.id; state=c; saveState(); showClassPicker(); }
59284
59494
  function switchCharacter(id){
59285
59495
  if(!library.characters[id]) return;
@@ -59305,7 +59515,7 @@ function exportCharacter(){
59305
59515
  const code="PF2E1:"+b64encode(JSON.stringify(c));
59306
59516
  const io=document.getElementById("menuIO");
59307
59517
  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")); }
59518
+ if(navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(code).then(()=>toast("Copied to clipboard")).catch(()=>toast("Code ready — copy it")); }
59309
59519
  else toast("Code ready — copy it");
59310
59520
  }
59311
59521
  function importCharacter(){
@@ -59317,7 +59527,7 @@ function importCharacter(){
59317
59527
  if(typeof obj!=="object"||!("level" in obj)) throw new Error("bad");
59318
59528
  const c=Object.assign(blankChar(), obj, {id:uid()});
59319
59529
  library.characters[c.id]=c; library.activeId=c.id; state=c; saveState();
59320
- toast("📥 Character imported");
59530
+ toast("Character imported");
59321
59531
  if(!state.classId){ showClassPicker(); } else { renderHeader(); go(state.prepared?"today":"prepare"); }
59322
59532
  }catch(e){ alert("That code didn't work — make sure you pasted the whole thing."); }
59323
59533
  }
@@ -59332,18 +59542,19 @@ function showClassPicker(){
59332
59542
  const sec=document.getElementById("view-classpick");
59333
59543
  sec.classList.remove("hide");
59334
59544
  sec.innerHTML=`
59335
- <h1 style="text-align:center">📖 PF2e Spellbook</h1>
59545
+ <h1 style="text-align:center">PF2e Spellbook</h1>
59336
59546
  <p class="meta" style="text-align:center;margin-bottom:18px">Choose your class to begin.</p>
59337
59547
  ${CLASS_ORDER.map(id=>{
59338
59548
  const c=CLASSES[id];
59339
- return `<button class="classcard" onclick="chooseClass('${id}')">
59340
- <div class="ic">${c.icon}</div>
59549
+ return `<button class="classcard" style="border-left-color:${c.color}" onclick="chooseClass('${id}')">
59550
+ <div class="ic" style="color:${c.color}">${iconSvg(c.icon)}</div>
59341
59551
  <div class="txt"><div class="nm">${c.name}${c.preview?` <span class="previewtag">preview</span>`:""}</div>
59342
59552
  <div class="tl">${c.tagline}</div></div>
59343
59553
  <div class="arr">→</div>
59344
59554
  </button>`;
59345
59555
  }).join("")}
59346
59556
  <p class="meta" style="text-align:center;margin-top:18px;font-size:.8rem">Pathfinder 2e (Remaster) · unofficial fan tool<br>${escapeHtml(dataStampText())}</p>`;
59557
+ applyTheme();
59347
59558
  window.scrollTo(0,0);
59348
59559
  }
59349
59560
  function chooseClass(id){
@@ -59354,8 +59565,9 @@ function chooseClass(id){
59354
59565
  if(!hasFeature("divineFont")) state.font="heal";
59355
59566
  if(c.traditionFrom==="patron" && !state.patronTradition) state.patronTradition=c.defaultTradition||"occult";
59356
59567
  saveState();
59568
+ applyTheme();
59357
59569
  renderHeader();
59358
- toast(`${c.icon} ${c.name} selected`);
59570
+ toast(`${c.name} selected`);
59359
59571
  go(state.prepared ? "today" : "prepare");
59360
59572
  }
59361
59573
  function openClassPicker(){ showClassPicker(); }
@@ -59365,7 +59577,8 @@ function openClassPicker(){ showClassPicker(); }
59365
59577
  ============================================================ */
59366
59578
  function renderHeader(){
59367
59579
  const c=activeClass(); if(!c) return;
59368
- document.getElementById("charIcon").textContent=c.icon;
59580
+ applyTheme();
59581
+ document.getElementById("charIcon").innerHTML=iconSvg(c.icon);
59369
59582
  document.getElementById("charNameHead").textContent=state.name||c.name;
59370
59583
  document.getElementById("charSub").textContent=
59371
59584
  `${c.name} · Pathfinder 2e (Remaster) · ${TRADITION_LABEL[activeTradition()]} list`;
@@ -59431,8 +59644,8 @@ function renderCharExtra(){
59431
59644
  <label class="field"><span class="name">Divine Font</span>
59432
59645
  <span class="hint">Which one did you choose at level 1?</span></label>
59433
59646
  <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>
59647
+ <button data-font="heal" class="heal ${state.font==="heal"?"on":""}" onclick="setFont('heal')">${iconSvg("heal")} Healing Font</button>
59648
+ <button data-font="harm" class="harm ${state.font==="harm"?"on":""}" onclick="setFont('harm')">${iconSvg("harm")} Harmful Font</button>
59436
59649
  </div>`;
59437
59650
  }
59438
59651
  if(c.traditionFrom==="bloodline"){
@@ -59463,22 +59676,25 @@ function renderPrepSummary(){
59463
59676
  let extra="";
59464
59677
  if(hasFeature("divineFont")){
59465
59678
  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>`;
59679
+ 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
59680
  }
59468
59681
  getFeatures("extraSlots").forEach(f=>{
59469
- extra+=`<div style="margin-top:4px">${f.icon} ${f.label}: <b>+1</b> per rank</div>`;
59682
+ extra+=`<div style="margin-top:4px">${iconSvg(f.icon)} ${f.label}: <b>+1</b> per rank</div>`;
59470
59683
  });
59471
59684
  getFeatures("dailyResource").forEach(f=>{
59472
- extra+=`<div style="margin-top:4px">${f.icon} ${f.label}: <b>${f.uses}×</b>/day</div>`;
59685
+ extra+=`<div style="margin-top:4px">${iconSvg(f.icon)} ${f.label}: <b>${f.uses}×</b>/day</div>`;
59686
+ });
59687
+ studiousFeats().forEach(f=>{
59688
+ extra+=`<div style="margin-top:4px">${iconSvg("school")} ${f.label}: <b>${f.count}×</b> rank ${f.rank} (utility)</div>`;
59473
59689
  });
59474
59690
  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>`;
59691
+ 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
59692
  }
59477
59693
  document.getElementById("prepSummary").innerHTML=`
59478
59694
  <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>
59695
+ <div><b>Spell DC ${spellDC()}</b> &nbsp;·&nbsp; Spell attack ${spellAtk()}</div>
59696
+ <div style="margin-top:8px">Cantrips ${isSpontaneous()?"known":""}: <b>${c.cantrips}</b></div>
59697
+ <div style="margin-top:4px">${isSpontaneous()?"Slots (cast any known spell of that rank)":"Spell slots"}: ${slotText||"—"}</div>
59482
59698
  ${extra}
59483
59699
  </div>`;
59484
59700
  }
@@ -59504,6 +59720,7 @@ function renderPrepDynamic(){
59504
59720
  getFeatures("note").forEach(f=>{ html+=`<div class="banner"><b>${f.title}</b> — ${f.text}</div>`; });
59505
59721
  html+=cantripPicksHTML();
59506
59722
  html+= isSpontaneous() ? repertoireHTML() : preparedSlotsHTML();
59723
+ if(!isSpontaneous()) html+=studiousSlotsHTML();
59507
59724
  if(hasFocus()) html+=focusPicksHTML();
59508
59725
  document.getElementById("prepDynamic").innerHTML=html;
59509
59726
  }
@@ -59522,14 +59739,14 @@ function focusPicksHTML(){
59522
59739
  <span class="fcname">${escapeHtml(s.name)}</span><span class="fcrank">${rank}</span></label>`;
59523
59740
  }).join("");
59524
59741
  return `<div class="slotgroup">
59525
- <h3>🔵 Focus spells <span class="count" style="font-weight:600;color:var(--muted)">(pool of ${pool})</span></h3>
59742
+ <h3>Focus spells <span class="count" style="font-weight:600;color:var(--muted)">(pool of ${pool})</span></h3>
59526
59743
  <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
59744
  <label class="field" style="margin:8px 0"><span class="name">Focus Point pool</span>
59528
59745
  <span class="hint">1 + 1 per extra focus feat, up to 3</span>
59529
59746
  <select id="inFocusPool" onchange="setFocusPool(this.value)">
59530
59747
  ${[1,2,3].map(n=>`<option value="${n}" ${n===pool?"selected":""}>${n} Focus Point${n>1?"s":""}</option>`).join("")}
59531
59748
  </select></label>
59532
- <input type="text" id="focusFilter" placeholder="🔍 filter focus spells (name or trait)…" oninput="filterFocusChecklist(this.value)" aria-label="filter focus spells">
59749
+ <input type="text" id="focusFilter" placeholder="Filter focus spells (name or trait)…" oninput="filterFocusChecklist(this.value)" aria-label="filter focus spells">
59533
59750
  <div class="focus-checklist" id="focusChecklist">${rows}</div>
59534
59751
  </div>`;
59535
59752
  }
@@ -59547,7 +59764,7 @@ function gatherFocus(){
59547
59764
  function cantripPicksHTML(){
59548
59765
  const c=activeClass();
59549
59766
  const prev=(state.prepared&&state.prepared.cantrips)||[];
59550
- let html=`<div class="slotgroup"><h3>🪄 Cantrips (pick ${c.cantrips})</h3>`;
59767
+ let html=`<div class="slotgroup"><h3>Cantrips (pick ${c.cantrips})</h3>`;
59551
59768
  for(let i=0;i<c.cantrips;i++){
59552
59769
  const sel=prev[i]||"", pid="cprev"+i;
59553
59770
  html+=`<div class="prep-slot">
@@ -59570,10 +59787,10 @@ function preparedSlotsHTML(){
59570
59787
  const fontName=state.font==="heal"?"Heal":"Harm";
59571
59788
  const fontSpell=findSpell(state.font==="heal"?feat.healSlug:feat.harmSlug);
59572
59789
  html+=`<div class="slotgroup">
59573
- <h3>${state.font==="heal"?"":""} Divine Font — ${fontName} (free slots)</h3>
59790
+ <h3>${iconSvg(state.font==="heal"?"heal":"harm")} Divine Font — ${fontName} (free slots)</h3>
59574
59791
  <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
59792
  for(let i=0;i<fontCount();i++){
59576
- html+=`<div class="slotrow"><div class="num">${state.font==="heal"?"":""}</div>
59793
+ html+=`<div class="slotrow"><div class="num">${iconSvg(state.font==="heal"?"heal":"harm")}</div>
59577
59794
  <select disabled><option>${fontName} (rank ${hr})</option></select></div>`;
59578
59795
  }
59579
59796
  html+=`<div class="prep-preview">${spellPreviewHTML(fontSpell, hr)}</div></div>`;
@@ -59583,7 +59800,7 @@ function preparedSlotsHTML(){
59583
59800
  Object.keys(slots).map(Number).sort((a,b)=>b-a).forEach(r=>{
59584
59801
  const n=slots[r];
59585
59802
  const prev=prevSlots[r]||[];
59586
- html+=`<div class="slotgroup"><h3>📜 Rank ${r} slots (${n})</h3>`;
59803
+ html+=`<div class="slotgroup"><h3>Rank ${r} slots (${n})</h3>`;
59587
59804
  for(let i=0;i<n;i++){
59588
59805
  const sel=prev[i]||"", pid="sprev"+r+"_"+i;
59589
59806
  html+=`<div class="prep-slot">
@@ -59596,7 +59813,7 @@ function preparedSlotsHTML(){
59596
59813
  const ex=(prevExtra[f.key]&&prevExtra[f.key][r]&&prevExtra[f.key][r][0])||"";
59597
59814
  const pid="xprev"+f.key+r;
59598
59815
  html+=`<div class="prep-slot">
59599
- <div class="slotrow"><div class="num">${f.icon}</div>
59816
+ <div class="slotrow"><div class="num">${iconSvg(f.icon)}</div>
59600
59817
  <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
59818
  <div class="prep-preview" id="${pid}">${spellPreviewHTML(findSpell(ex),r)}</div></div>`;
59602
59819
  });
@@ -59605,6 +59822,33 @@ function preparedSlotsHTML(){
59605
59822
  return html;
59606
59823
  }
59607
59824
 
59825
+ /* Studious spells (magus, level 7+): a couple of extra fixed-rank slots that can
59826
+ only hold a short list of utility spells. Restricted <select> so the app stays
59827
+ honest about what's legal in these slots. */
59828
+ function studiousFeats(){ return getFeatures("studiousSlots").filter(f=>Number(state.level)>=(f.minLevel||1)); }
59829
+ function studiousSlotsHTML(){
59830
+ const feats=studiousFeats(); if(!feats.length) return "";
59831
+ const prevAll=(state.prepared&&state.prepared.studious)||{};
59832
+ let html="";
59833
+ feats.forEach(f=>{
59834
+ const prev=prevAll[f.key]||[];
59835
+ const opts=(f.spells||[]).map(findSpell).filter(Boolean).sort((a,b)=>a.name.localeCompare(b.name));
59836
+ 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>
59837
+ <p class="meta">${escapeHtml(f.note||"")}</p>`;
59838
+ for(let i=0;i<f.count;i++){
59839
+ const sel=prev[i]||"", pid="stud_"+f.key+"_"+i;
59840
+ const optionHtml=`<option value="">— choose —</option>`+
59841
+ opts.map(s=>`<option value="${s.slug}" ${s.slug===sel?"selected":""}>${escapeHtml(s.name)}</option>`).join("");
59842
+ html+=`<div class="prep-slot">
59843
+ <div class="slotrow"><div class="num">${iconSvg("school")}</div>
59844
+ <select class="studiousSel" data-key="${f.key}" data-castrank="${f.rank}" data-preview="${pid}" onchange="updatePreview(this)">${optionHtml}</select></div>
59845
+ <div class="prep-preview" id="${pid}">${spellPreviewHTML(findSpell(sel),f.rank)}</div></div>`;
59846
+ }
59847
+ html+=`</div>`;
59848
+ });
59849
+ return html;
59850
+ }
59851
+
59608
59852
  let repUID=0;
59609
59853
  function repRowHTML(rank, sel, sig){
59610
59854
  const pid="rprev"+(repUID++);
@@ -59628,7 +59872,7 @@ function repertoireHTML(){
59628
59872
  const count=Math.max(known.length, slots[r]||0);
59629
59873
  if(!count) continue; // no slots of this rank (e.g. a summoner's shed low ranks) → no repertoire of that rank
59630
59874
  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>
59875
+ <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
59876
  <div class="repRows" id="repRows-${r}">`;
59633
59877
  for(let i=0;i<count;i++){
59634
59878
  const e=known[i]||{};
@@ -59668,8 +59912,11 @@ function savePrep(){
59668
59912
  document.querySelectorAll(".slotSel").forEach(sel=>{ const r=sel.dataset.rank; (slots[r]=slots[r]||[]).push(sel.value); });
59669
59913
  const extra={};
59670
59914
  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); });
59915
+ const studious={};
59916
+ document.querySelectorAll(".studiousSel").forEach(sel=>{ const k=sel.dataset.key; (studious[k]=studious[k]||[]).push(sel.value); });
59671
59917
  const hr=highestRank(state.level);
59672
59918
  state.prepared={ type:"prepared", cantrips, slots, extra };
59919
+ if(Object.keys(studious).length) state.prepared.studious=studious;
59673
59920
  if(hasFeature("divineFont")){
59674
59921
  const feat=getFeature("divineFont");
59675
59922
  state.prepared.divineFont={ font:state.font, fontName:state.font==="heal"?"Heal":"Harm",
@@ -59678,7 +59925,7 @@ function savePrep(){
59678
59925
  }
59679
59926
  if(focus) state.prepared.focus=focus;
59680
59927
  saveState();
59681
- toast("Prepared for the day!");
59928
+ toast("Prepared for the day!");
59682
59929
  go("today");
59683
59930
  }
59684
59931
 
@@ -59691,7 +59938,7 @@ function todayHeaderHTML(){
59691
59938
  return `<div class="card" style="text-align:center">
59692
59939
  <div style="font-size:1.1rem"><b>${escapeHtml(state.name||c.name)}</b> · ${c.name} · Level ${state.level}</div>
59693
59940
  <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>
59941
+ <button class="btn secondary" style="margin-top:12px" onclick="newDay()">Rest &amp; reset the day</button>
59695
59942
  </div>`;
59696
59943
  }
59697
59944
  function dailyResourceHTML(){
@@ -59701,14 +59948,34 @@ function dailyResourceHTML(){
59701
59948
  const spent=state.cast[key]||0, remaining=f.uses-spent, done=remaining<=0;
59702
59949
  let pips=""; for(let k=0;k<f.uses;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
59703
59950
  const btn=done?`<button class="castbtn zero" onclick="uncast('${key}')">↩ undo</button>`
59704
- :`<button class="castbtn" onclick="doCast('${key}',${f.uses})">Use</button>`;
59951
+ :`<button class="castbtn" onclick="doCast('${key}',${f.uses})">${f.verb||"Use"}</button>`;
59705
59952
  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>
59953
+ <div class="cast-top"><div class="nm">${iconSvg(f.icon)} ${f.label}</div><div class="uses">${pips} ${btn}</div></div>
59707
59954
  <div class="meta">${escapeHtml(f.note||"")}</div></div>`;
59708
59955
  });
59709
59956
  return html;
59710
59957
  }
59711
59958
 
59959
+ /* Escalating-stage tracker (e.g. the oracle's cursebound curse). A simple
59960
+ 0..max stepper that persists in state.cast and resets on a new day. */
59961
+ function stageTrackerHTML(){
59962
+ let html="";
59963
+ getFeatures("stageTracker").forEach(f=>{
59964
+ const k="stage:"+f.key;
59965
+ const cur=Math.max(0, Math.min(f.max, state.cast[k]||0));
59966
+ let pips=""; for(let i=1;i<=f.max;i++){ pips+=`<span class="pip ${i<=cur?"full":"spent"}"></span>`; }
59967
+ html+=`<div class="cast-card">
59968
+ <div class="cast-top"><div class="nm">${iconSvg(f.icon)} ${f.label} <span class="meta">stage ${cur} / ${f.max}</span></div>
59969
+ <div class="uses">${pips}
59970
+ <button class="castbtn zero" onclick="stageDown('${f.key}')" ${cur<=0?"disabled":""} aria-label="lower ${escapeHtml(f.label)} stage">−</button>
59971
+ <button class="castbtn" onclick="stageUp('${f.key}')" ${cur>=f.max?"disabled":""} aria-label="raise ${escapeHtml(f.label)} stage">+</button></div></div>
59972
+ <div class="meta">${escapeHtml(f.note||"")}</div></div>`;
59973
+ });
59974
+ return html;
59975
+ }
59976
+ 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(); }
59977
+ function stageDown(key){ const k="stage:"+key; state.cast[k]=Math.max(0,(state.cast[k]||0)-1); saveState(); renderToday(); }
59978
+
59712
59979
  function renderToday(){
59713
59980
  renderHeader();
59714
59981
  const p=state.prepared;
@@ -59729,41 +59996,58 @@ function focusSectionHTML(p){
59729
59996
  const fr=focusRank();
59730
59997
  let pips=""; for(let k=0;k<pool;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
59731
59998
  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>
59999
+ let html=`<div class="sectionhead">Focus spells <span class="count">${remaining}/${pool} point${pool>1?"s":""}</span></div>
59733
60000
  <div style="margin:2px 0 8px"><span class="uses">${pips}${refocus}</span></div><div class="divider"></div>`;
60001
+ const psychic = !!(activeClass() && activeClass().id==="psychic");
59734
60002
  f.spells.forEach((slug,i)=>{
59735
60003
  const s=findSpell(slug); if(!s) return;
59736
60004
  const atWill = s.rank===0;
59737
60005
  const out = !atWill && remaining<=0;
59738
60006
  const castRank=fr;
59739
60007
  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>`);
60008
+ // Psychic psi cantrips are free at will, but can be Amped by spending a Focus Point.
60009
+ let btn;
60010
+ if(atWill && psychic){
60011
+ const amp = remaining>0
60012
+ ? `<button class="castbtn" onclick="castFocus(${pool})" title="Amp — spend 1 Focus Point">Amp (1 pt)</button>`
60013
+ : `<button class="castbtn zero" disabled>No points</button>`;
60014
+ btn = `<span class="unlim">∞ at will</span> ${amp}`;
60015
+ } else if(atWill){
60016
+ btn = `<span class="unlim">∞ at will</span>`;
60017
+ } else {
60018
+ btn = out ? `<button class="castbtn zero" disabled>No points</button>`
60019
+ : `<button class="castbtn" onclick="castFocus(${pool})">Cast</button>`;
60020
+ }
59743
60021
  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>`:""}
60022
+ html+=`<div class="cast-card ${out?"spent":""}" style="border-left-color:var(--accent)">
60023
+ <div class="cast-top"><div class="nm">${escapeHtml(s.name)} ${heightNote}</div><div class="uses">${btn}</div></div>
60024
+ <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
60025
  <button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
59748
60026
  <div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s)}</div></div>`;
59749
60027
  });
59750
60028
  return html;
59751
60029
  }
59752
60030
  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"); }
60031
+ function refocus(){
60032
+ const key="focuspool"; state.cast[key]=Math.max(0,(state.cast[key]||0)-1);
60033
+ // Some resources (e.g. the psychic's Unleash Psyche) recharge when you Refocus.
60034
+ getFeatures("dailyResource").forEach(f=>{ if(f.recharge==="refocus") delete state.cast["resource:"+f.key]; });
60035
+ saveState(); renderToday(); toast("Refocused — +1 Focus Point");
60036
+ }
59754
60037
 
59755
60038
  function renderTodayPrepared(p){
59756
60039
  let html=todayHeaderHTML();
59757
60040
  html+=dailyResourceHTML();
60041
+ html+=stageTrackerHTML();
59758
60042
 
59759
60043
  if(p.divineFont){
59760
60044
  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>`;
60045
+ 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
60046
  html+=castableCard(fontSpell,"font",f.fontRank,f.fontCount,f.fontRank,0,{cls:f.font});
59763
60047
  }
59764
60048
 
59765
60049
  if(p.cantrips && p.cantrips.length){
59766
- html+=`<div class="sectionhead">🪄 Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
60050
+ html+=`<div class="sectionhead">Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
59767
60051
  p.cantrips.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"cantrip",0,Infinity,0,i); });
59768
60052
  }
59769
60053
 
@@ -59773,19 +60057,26 @@ function renderTodayPrepared(p){
59773
60057
  const extras=[];
59774
60058
  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
60059
  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>`;
60060
+ 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
60061
  (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
60062
  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
60063
  });
60064
+ studiousFeats().forEach(f=>{
60065
+ const arr=((p.studious&&p.studious[f.key])||[]).filter(Boolean);
60066
+ if(!arr.length) return;
60067
+ html+=`<div class="sectionhead">${f.label} <span class="count">${arr.length} slot${arr.length>1?"s":""}</span></div><div class="divider"></div>`;
60068
+ arr.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"studious",f.rank,1,f.rank,i,{label:"school"}); });
60069
+ });
59780
60070
  return html;
59781
60071
  }
59782
60072
 
59783
60073
  function renderTodaySpontaneous(p){
59784
60074
  let html=todayHeaderHTML();
59785
60075
  html+=dailyResourceHTML();
60076
+ html+=stageTrackerHTML();
59786
60077
 
59787
60078
  if(p.cantrips && p.cantrips.length){
59788
- html+=`<div class="sectionhead">🪄 Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
60079
+ html+=`<div class="sectionhead">Cantrips <span class="count">unlimited</span></div><div class="divider"></div>`;
59789
60080
  p.cantrips.forEach((slug,i)=>{ const s=findSpell(slug); if(!s) return; html+=castableCard(s,"cantrip",0,Infinity,0,i); });
59790
60081
  }
59791
60082
 
@@ -59802,7 +60093,7 @@ function renderTodaySpontaneous(p){
59802
60093
 
59803
60094
  let pips=""; for(let k=0;k<max;k++){ pips+=`<span class="pip ${k<remaining?"full":"spent"}"></span>`; }
59804
60095
  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>
60096
+ html+=`<div class="sectionhead">Rank ${R} <span class="count">${remaining}/${max} slot${max>1?"s":""} left</span></div>
59806
60097
  <div style="margin:2px 0 8px"><span class="uses">${pips}${undo}</span></div><div class="divider"></div>`;
59807
60098
 
59808
60099
  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 +60113,7 @@ function castableCard(s, kind, rank, uses, castRank, idx, opts){
59822
60113
  const isSpent=!unlimited && remaining<=0;
59823
60114
  const cls = opts.cls || (isSpent?"":cardClass(s));
59824
60115
  const heightNote=(castRank>s.rank)?`<span class="meta"> · cast at rank ${castRank}</span>`:"";
59825
- const tag = opts.label?`<span class="slottag">${opts.label}</span> `:"";
60116
+ const tag = opts.label?`<span class="slottag">${iconSvg(opts.label)}</span> `:"";
59826
60117
 
59827
60118
  let usesHtml="";
59828
60119
  if(unlimited){ usesHtml=`<span class="unlim">∞ at will</span>`; }
@@ -59841,7 +60132,7 @@ function castableCard(s, kind, rank, uses, castRank, idx, opts){
59841
60132
  <div class="nm">${tag}${escapeHtml(s.name)} ${heightNote}</div>
59842
60133
  <div class="uses">${usesHtml} ${btn} ${sustainBtn(s)}</div>
59843
60134
  </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>`:""}
60135
+ <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
60136
  <button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
59846
60137
  <div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s,{cls})}</div>
59847
60138
  </div>`;
@@ -59862,7 +60153,7 @@ function poolSpellCard(s, poolRank, castRank, remaining, max, uid, sig){
59862
60153
  <div class="nm">${star}${escapeHtml(s.name)} ${heightNote}</div>
59863
60154
  <div class="uses">${btn} ${sustainBtn(s)}</div>
59864
60155
  </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>`:""}
60156
+ <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
60157
  <button class="detailsbtn" onclick="toggleDetails('${detailsId}',this)">Show details ▾</button>
59867
60158
  <div id="${detailsId}" class="hide" style="margin-top:10px">${spellCardHTML(s)}</div>
59868
60159
  </div>`;
@@ -59873,22 +60164,22 @@ function doCast(key,max){ state.cast[key]=(state.cast[key]||0)+1; if(state.cast[
59873
60164
  function uncast(key){ state.cast[key]=Math.max(0,(state.cast[key]||0)-1); saveState(); renderToday(); }
59874
60165
  function castPool(rank,max){ const key="pool:"+rank; state.cast[key]=Math.min(max,(state.cast[key]||0)+1); saveState(); renderToday(); }
59875
60166
  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!"); } }
60167
+ 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
60168
 
59878
60169
  /* ---- Sustained-spell tracker ---- */
59879
60170
  function sustainBtn(s){
59880
60171
  if(!s || !s.sustained) return "";
59881
60172
  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>`;
60173
+ return on ? `<button class="susbtn on" onclick="endSustain('${s.slug}')">End</button>`
60174
+ : `<button class="susbtn" onclick="startSustain('${s.slug}')">Sustain</button>`;
59884
60175
  }
59885
- function startSustain(slug){ state.sustaining=state.sustaining||[]; if(!state.sustaining.includes(slug)){ state.sustaining.push(slug); saveState(); renderToday(); toast("Now sustaining"); } }
60176
+ function startSustain(slug){ state.sustaining=state.sustaining||[]; if(!state.sustaining.includes(slug)){ state.sustaining.push(slug); saveState(); renderToday(); toast("Now sustaining"); } }
59886
60177
  function endSustain(slug){ state.sustaining=(state.sustaining||[]).filter(x=>x!==slug); saveState(); renderToday(); }
59887
60178
  function sustainingBarHTML(){
59888
60179
  const list=(state.sustaining||[]).filter(slug=>findSpell(slug));
59889
60180
  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>
60181
+ return `<div class="card" style="border-left:5px solid var(--info)">
60182
+ <div class="sectionhead" style="margin:0 0 6px">Sustaining now <span class="count">spend an action each turn</span></div>
59892
60183
  ${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
60184
  </div>`;
59894
60185
  }
@@ -59931,8 +60222,8 @@ function renderBrowseFilters(){
59931
60222
  function renderRankChips(){
59932
60223
  const ranks=[["all","All"],["0","Cantrips"]];
59933
60224
  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"]);
60225
+ if(hasFocus()) ranks.push(["focus","Focus"]);
60226
+ ranks.push(["rituals","Rituals"]);
59936
60227
  document.getElementById("rankChips").innerHTML=
59937
60228
  ranks.map(([v,l])=>`<button class="chip ${browseRank===v?"on":""}" onclick="setBrowseRank('${v}')">${l}</button>`).join("");
59938
60229
  }
@@ -59967,7 +60258,7 @@ function renderBrowse(){
59967
60258
  }
59968
60259
  /* Lightweight collapsed row; the full card renders on demand when tapped. */
59969
60260
  function browseRowHTML(s){
59970
- const save=s.save?` · 🛡 ${escapeHtml(s.save)}`:"";
60261
+ const save=s.save?` · ${escapeHtml(s.save)}`:"";
59971
60262
  const note=legacyNote(s)?` <span class="formerly">${escapeHtml(legacyNote(s))}</span>`:"";
59972
60263
  return `<div class="browse-row rank${s.rank}">
59973
60264
  <button class="browse-row-head" onclick="toggleBrowse('${s.slug}',this)">
@@ -60078,6 +60369,14 @@ function downloadOffline(){
60078
60369
 
60079
60370
  function init(){
60080
60371
  setupInstall();
60372
+ applyTheme();
60373
+ try{
60374
+ if(window.matchMedia){
60375
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{
60376
+ if(((library.settings&&library.settings.themeMode)||"auto")==="auto") applyTheme();
60377
+ });
60378
+ }
60379
+ }catch(e){}
60081
60380
  if(!state.classId){ showClassPicker(); }
60082
60381
  else { renderHeader(); go(state.prepared?"today":"prepare"); }
60083
60382
  }