sitemend 1.2.1__py3-none-any.whl

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.
sitemend/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """sitemend (the seo-repair skill), packaged. The audit scripts are run by path (see seo_repair.py), so this file only makes the directory a package."""
@@ -0,0 +1,466 @@
1
+ <title>SEO Repair Dashboard</title>
2
+ <link rel="preconnect" href="https://fonts.googleapis.com">
3
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
4
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Archivo:wght@600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
5
+
6
+ <style>
7
+ /* ---- Light palette: complete token set on bare :root ---- */
8
+ :root{
9
+ --ground:#f4f6f8;
10
+ --surface:#ffffff;
11
+ --surface-2:#eceff3;
12
+ --ink:#171d26;
13
+ --ink-2:#4a5666;
14
+ --ink-3:#78859a;
15
+ --line:#dbe1e9;
16
+ --line-2:#c3ccd8;
17
+ --accent:#2c3fa8;
18
+ --accent-soft:#e6e9f8;
19
+ --accent-ink:#ffffff;
20
+
21
+ --sev-critical:#a4193d;
22
+ --sev-high:#c2521a;
23
+ --sev-medium:#8a6512;
24
+ --sev-low:#5c6b7e;
25
+ --sev-critical-bg:#fae7ec;
26
+ --sev-high-bg:#fbebe1;
27
+ --sev-medium-bg:#f8f0dc;
28
+ --sev-low-bg:#e9edf2;
29
+
30
+ --ok:#1f6b4a;
31
+ --ok-bg:#e2f0e9;
32
+ --block:#a4193d;
33
+ --block-bg:#fae7ec;
34
+
35
+ --radius:3px;
36
+ --measure:68ch;
37
+ }
38
+ @media (prefers-color-scheme:dark){
39
+ :root:not([data-theme="light"]){
40
+ --ground:#10141a;
41
+ --surface:#171c24;
42
+ --surface-2:#1e242e;
43
+ --ink:#e7ecf3;
44
+ --ink-2:#a9b4c3;
45
+ --ink-3:#7a8698;
46
+ --line:#28303b;
47
+ --line-2:#39434f;
48
+ --accent:#8ea0f5;
49
+ --accent-soft:#1c2340;
50
+ --accent-ink:#0d1017;
51
+
52
+ --sev-critical:#f2809b;
53
+ --sev-high:#f0a06f;
54
+ --sev-medium:#ddbc63;
55
+ --sev-low:#96a4b6;
56
+ --sev-critical-bg:#331520;
57
+ --sev-high-bg:#332014;
58
+ --sev-medium-bg:#2e2717;
59
+ --sev-low-bg:#222932;
60
+
61
+ --ok:#6cc39a;
62
+ --ok-bg:#14291f;
63
+ --block:#f2809b;
64
+ --block-bg:#331520;
65
+ }
66
+ }
67
+ :root[data-theme="dark"]{
68
+ --ground:#10141a;
69
+ --surface:#171c24;
70
+ --surface-2:#1e242e;
71
+ --ink:#e7ecf3;
72
+ --ink-2:#a9b4c3;
73
+ --ink-3:#7a8698;
74
+ --line:#28303b;
75
+ --line-2:#39434f;
76
+ --accent:#8ea0f5;
77
+ --accent-soft:#1c2340;
78
+ --accent-ink:#0d1017;
79
+
80
+ --sev-critical:#f2809b;
81
+ --sev-high:#f0a06f;
82
+ --sev-medium:#ddbc63;
83
+ --sev-low:#96a4b6;
84
+ --sev-critical-bg:#331520;
85
+ --sev-high-bg:#332014;
86
+ --sev-medium-bg:#2e2717;
87
+ --sev-low-bg:#222932;
88
+
89
+ --ok:#6cc39a;
90
+ --ok-bg:#14291f;
91
+ --block:#f2809b;
92
+ --block-bg:#331520;
93
+ }
94
+
95
+ *{box-sizing:border-box}
96
+ body{
97
+ margin:0; background:var(--ground); color:var(--ink);
98
+ font-family:"IBM Plex Sans",system-ui,-apple-system,Segoe UI,sans-serif;
99
+ font-size:15px; line-height:1.55;
100
+ -webkit-font-smoothing:antialiased;
101
+ }
102
+ .wrap{max-width:1120px;margin:0 auto;padding:40px 24px 80px;display:flex;flex-direction:column;gap:38px}
103
+ h1,h2,h3{font-family:Archivo,"IBM Plex Sans",system-ui,sans-serif;text-wrap:balance;margin:0}
104
+ h1{font-size:clamp(28px,4.2vw,42px);font-weight:700;letter-spacing:-.022em;line-height:1.08}
105
+ h2{font-size:19px;font-weight:600;letter-spacing:-.012em}
106
+ h3{font-size:15px;font-weight:600}
107
+ a{color:var(--accent)}
108
+ code,.mono{font-family:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
109
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
110
+
111
+ .eyebrow{
112
+ font-family:"IBM Plex Mono",monospace;font-size:11px;font-weight:500;
113
+ letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);
114
+ }
115
+
116
+ /* ---- masthead ---- */
117
+ .masthead{display:flex;flex-direction:column;gap:14px;
118
+ border-bottom:1px solid var(--line-2);padding-bottom:26px}
119
+ .meta-row{display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13px;color:var(--ink-2)}
120
+ .meta-row .mono{color:var(--ink)}
121
+
122
+ /* ---- summary tiles ---- */
123
+ .tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}
124
+ .tile{
125
+ background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
126
+ padding:16px 16px 14px;display:flex;flex-direction:column;gap:6px;
127
+ border-left:3px solid var(--stripe,var(--line-2));
128
+ }
129
+ .tile .n{font-family:Archivo,sans-serif;font-size:32px;font-weight:700;line-height:1;
130
+ font-variant-numeric:tabular-nums;color:var(--stripe,var(--ink))}
131
+ .tile.is-zero .n{color:var(--ink-3)}
132
+ .tile .lbl{font-family:"IBM Plex Mono",monospace;font-size:11px;letter-spacing:.12em;
133
+ text-transform:uppercase;color:var(--ink-3)}
134
+
135
+ /* ---- the stage rail: the pipeline is the thesis ---- */
136
+ .rail{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:22px}
137
+ .rail-scroll{overflow-x:auto;padding-bottom:4px}
138
+ .stages{display:flex;align-items:stretch;gap:0;min-width:660px}
139
+ .stage{
140
+ flex:1;padding:12px 14px;border-top:3px solid var(--stripe,var(--line-2));
141
+ display:flex;flex-direction:column;gap:5px;min-width:0;
142
+ }
143
+ .stage .num{font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--ink-3);letter-spacing:.1em}
144
+ .stage .nm{font-size:13px;font-weight:600;line-height:1.25}
145
+ .stage .ct{font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--ink-2);
146
+ font-variant-numeric:tabular-nums}
147
+ .stage.clean .ct{color:var(--ok)}
148
+ .stage .ct.unmeasured{color:var(--ink-3);font-style:normal;opacity:.85}
149
+ .gatemark{
150
+ display:flex;align-items:center;padding:0 2px;flex:0 0 auto;
151
+ }
152
+ .gatemark span{
153
+ font-family:"IBM Plex Mono",monospace;font-size:10px;font-weight:600;letter-spacing:.08em;
154
+ writing-mode:vertical-rl;transform:rotate(180deg);
155
+ padding:6px 3px;border-radius:2px;
156
+ }
157
+ .gate-clear span{background:var(--ok-bg);color:var(--ok)}
158
+ .gate-blocked span{background:var(--block-bg);color:var(--block)}
159
+ /* A gate nobody could assess reads as neutral, never as a pass -- same rule as the
160
+ stage rail's "not measured" state. */
161
+ .gate-unassessed span{background:var(--surface-2);color:var(--ink-3)}
162
+
163
+ /* ---- gates ---- */
164
+ .gates{display:grid;grid-template-columns:repeat(auto-fit,minmax(268px,1fr));gap:12px}
165
+ .gate{
166
+ background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);
167
+ padding:16px;display:flex;flex-direction:column;gap:8px;
168
+ }
169
+ .gate-head{display:flex;align-items:center;gap:10px}
170
+ .pill{
171
+ font-family:"IBM Plex Mono",monospace;font-size:10.5px;font-weight:600;
172
+ letter-spacing:.1em;text-transform:uppercase;padding:3px 8px;border-radius:2px;white-space:nowrap;
173
+ }
174
+ .pill.clear{background:var(--ok-bg);color:var(--ok)}
175
+ .pill.blocked{background:var(--block-bg);color:var(--block)}
176
+ .pill.unassessed{background:var(--surface-2);color:var(--ink-3)}
177
+ .gate h3{flex:1;min-width:0}
178
+ .gate p{margin:0;font-size:13.5px;color:var(--ink-2);max-width:var(--measure)}
179
+
180
+ /* ---- findings ---- */
181
+ .stage-block{display:flex;flex-direction:column;gap:12px}
182
+ .stage-hd{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;
183
+ border-bottom:1px solid var(--line-2);padding-bottom:8px}
184
+ .stage-hd .cnt{font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--ink-3);
185
+ font-variant-numeric:tabular-nums}
186
+ .f{
187
+ background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--stripe);
188
+ border-radius:var(--radius);padding:13px 15px;display:flex;flex-direction:column;gap:7px;
189
+ }
190
+ .f-top{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
191
+ .f-id{font-family:"IBM Plex Mono",monospace;font-size:12px;font-weight:500;color:var(--ink-2)}
192
+ .f-msg{font-size:14px;max-width:var(--measure)}
193
+ .f-pages{display:flex;flex-wrap:wrap;gap:5px}
194
+ .u{font-family:"IBM Plex Mono",monospace;font-size:11.5px;background:var(--surface-2);
195
+ color:var(--ink-2);padding:2px 6px;border-radius:2px;border:1px solid var(--line)}
196
+ .ev{font-family:"IBM Plex Mono",monospace;font-size:11.5px;color:var(--ink-3);
197
+ background:var(--surface-2);border:1px solid var(--line);border-radius:2px;
198
+ padding:7px 9px;overflow-x:auto;white-space:pre-wrap;word-break:break-word}
199
+ .ev b{color:var(--ink-2);font-weight:500}
200
+
201
+ /* ---- tables ---- */
202
+ .tbl-scroll{overflow-x:auto;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}
203
+ table{border-collapse:collapse;width:100%;font-size:13.5px}
204
+ th,td{text-align:left;padding:9px 13px;border-bottom:1px solid var(--line);vertical-align:top}
205
+ th{font-family:"IBM Plex Mono",monospace;font-size:10.5px;letter-spacing:.1em;
206
+ text-transform:uppercase;color:var(--ink-3);font-weight:500;white-space:nowrap}
207
+ tr:last-child td{border-bottom:none}
208
+ td.num{font-family:"IBM Plex Mono",monospace;font-variant-numeric:tabular-nums;white-space:nowrap}
209
+ td .u{font-size:11.5px}
210
+ .dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:7px;vertical-align:1px}
211
+
212
+ /* ---- handoff ---- */
213
+ .handoffs{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:12px}
214
+ .ho{background:var(--surface);border:1px dashed var(--line-2);border-radius:var(--radius);
215
+ padding:15px;display:flex;flex-direction:column;gap:6px}
216
+ .ho .where{font-family:"IBM Plex Mono",monospace;font-size:12px;color:var(--accent)}
217
+ .ho p{margin:0;font-size:13px;color:var(--ink-2);max-width:var(--measure)}
218
+
219
+ .note{font-size:13.5px;color:var(--ink-2);max-width:var(--measure)}
220
+ .foot{border-top:1px solid var(--line-2);padding-top:18px;font-size:12.5px;color:var(--ink-3);
221
+ max-width:var(--measure)}
222
+ section{display:flex;flex-direction:column;gap:14px}
223
+ [hidden]{display:none !important}
224
+ @media (max-width:640px){
225
+ .wrap{padding:26px 15px 60px;gap:30px}
226
+ .tile .n{font-size:26px}
227
+ }
228
+ </style>
229
+
230
+ <div class="wrap">
231
+ <header class="masthead">
232
+ <div class="eyebrow">SEO Repair &middot; crawl-driven audit</div>
233
+ <h1 id="site">&nbsp;</h1>
234
+ <div class="meta-row" id="meta"></div>
235
+ </header>
236
+
237
+ <section>
238
+ <div class="tiles" id="tiles"></div>
239
+ <p class="note" id="tiernote">
240
+ Every figure here was measured directly from the crawl. Search volume, keyword
241
+ difficulty, backlink data and field Core Web Vitals are absent by design &mdash; they
242
+ cannot be observed from a codebase, so they appear as handoff actions below rather
243
+ than as estimates.
244
+ </p>
245
+ </section>
246
+
247
+ <section>
248
+ <div class="eyebrow">Repair pipeline</div>
249
+ <div class="rail">
250
+ <div class="rail-scroll"><div class="stages" id="rail"></div></div>
251
+ </div>
252
+ <div class="gates" id="gates"></div>
253
+ </section>
254
+
255
+ <section id="vitals-sec" hidden>
256
+ <div class="eyebrow">Core Web Vitals &middot; measured, lab</div>
257
+ <h2>Lighthouse</h2>
258
+ <p class="note" id="vitals-caveat"></p>
259
+ <div class="tbl-scroll"><table id="vitals-tbl"></table></div>
260
+ </section>
261
+
262
+ <section>
263
+ <div class="eyebrow">Findings</div>
264
+ <h2>By stage, in repair order</h2>
265
+ <div id="findings" style="display:flex;flex-direction:column;gap:30px"></div>
266
+ </section>
267
+
268
+ <section>
269
+ <div class="eyebrow">Pages</div>
270
+ <h2>Crawled</h2>
271
+ <div class="tbl-scroll"><table id="pages-tbl"></table></div>
272
+ <div id="intentional"></div>
273
+ </section>
274
+
275
+ <section>
276
+ <div class="eyebrow">Handoff</div>
277
+ <h2>Requires tools outside this environment</h2>
278
+ <div class="handoffs" id="handoffs"></div>
279
+ </section>
280
+
281
+ <footer class="foot" id="foot"></footer>
282
+ </div>
283
+
284
+ <script>
285
+ const DATA = /*__DATA__*/ null;
286
+
287
+ const SEV = ["critical","high","medium","low"];
288
+ const sevVar = s => `var(--sev-${s})`;
289
+ const el = (t,c,x) => { const n=document.createElement(t); if(c)n.className=c;
290
+ if(x!==undefined)n.textContent=x; return n; };
291
+ const STAGE_NAMES = {1:"Crawl & Indexing",2:"Architecture",3:"Content & On-Page",
292
+ 4:"Performance",5:"Structured Data",6:"Authority",7:"QA & Monitoring"};
293
+ // Three states, not two. Anything that is not an explicit CLEAR must not be painted
294
+ // as a pass -- "NOT ASSESSED" used to fall through to the green pill.
295
+ const gateClass = s => s==="BLOCKED" ? "gate-blocked"
296
+ : s==="CLEAR" ? "gate-clear"
297
+ : "gate-unassessed";
298
+
299
+ function render(d){
300
+ document.getElementById("site").textContent = d.site;
301
+ document.title = d.site + " \u2014 SEO Repair";
302
+
303
+ const meta = document.getElementById("meta");
304
+ [["Generated",d.generated],["Pages",String(d.meta.page_count)],
305
+ ["Mode",d.meta.mode],["Source",d.meta.source]].forEach(([k,v])=>{
306
+ const s=el("div"); s.append(el("span","",k+" "), el("span","mono",v)); meta.append(s);
307
+ });
308
+
309
+ // severity tiles
310
+ const tiles=document.getElementById("tiles");
311
+ const total=SEV.reduce((a,s)=>a+(d.counts[s]||0),0);
312
+ SEV.forEach(s=>{
313
+ const n=d.counts[s]||0, t=el("div","tile"+(n?"":" is-zero"));
314
+ t.style.setProperty("--stripe", n?sevVar(s):"var(--line-2)");
315
+ t.append(el("div","n",String(n)), el("div","lbl",s));
316
+ tiles.append(t);
317
+ });
318
+ const t=el("div","tile"); t.style.setProperty("--stripe","var(--accent)");
319
+ t.append(el("div","n",String(total)), el("div","lbl","total"));
320
+ tiles.append(t);
321
+
322
+ // stage rail with gates interleaved \u2014 the ordering is the point
323
+ const rail=document.getElementById("rail");
324
+ const gateAfter={1:d.gates[0],2:d.gates[1],5:d.gates[2]};
325
+ for(let i=1;i<=7;i++){
326
+ const items=d.findings.filter(f=>f.stage===i);
327
+ const worst=SEV.find(s=>items.some(f=>f.severity===s));
328
+ const st=el("div","stage");
329
+ st.style.setProperty("--stripe", worst?sevVar(worst):"var(--line-2)");
330
+ const a = (d.assessed && d.assessed[String(i)]) || {state:"assessed"};
331
+ let label, cls = "";
332
+ if (items.length) {
333
+ label = items.length + (items.length===1 ? " finding" : " findings");
334
+ } else if (a.state === "assessed") {
335
+ label = "clear"; cls = " clean";
336
+ } else if (a.state === "advisory") {
337
+ label = "advisory";
338
+ } else if (a.state === "not-run") {
339
+ label = "not run";
340
+ } else {
341
+ label = "not measured";
342
+ }
343
+ if (cls) st.className = "stage clean";
344
+ const ct = el("div","ct",label);
345
+ if (a.state !== "assessed") { ct.classList.add("unmeasured"); if (a.note) ct.title = a.note; }
346
+ st.append(el("div","num",String(i).padStart(2,"0")),
347
+ el("div","nm",STAGE_NAMES[i]), ct);
348
+ rail.append(st);
349
+ const g=gateAfter[i];
350
+ if(g){
351
+ const m=el("div","gatemark "+gateClass(g.status));
352
+ m.append(el("span","","GATE "+g.gate));
353
+ m.title=g.name+" \u2014 "+g.status;
354
+ rail.append(m);
355
+ }
356
+ }
357
+
358
+ // gates
359
+ const gw=document.getElementById("gates");
360
+ d.gates.forEach(g=>{
361
+ const c=el("div","gate"), h=el("div","gate-head");
362
+ h.append(el("h3","","Gate "+g.gate+" \u00b7 "+g.name),
363
+ el("span","pill "+gateClass(g.status).replace("gate-",""),g.status));
364
+ c.append(h, el("p","",g.rule), el("div","eyebrow",g.detail));
365
+ gw.append(c);
366
+ });
367
+
368
+ // vitals
369
+ if(d.vitals && d.vitals.runs && d.vitals.runs.length){
370
+ document.getElementById("vitals-sec").hidden=false;
371
+ document.getElementById("vitals-caveat").textContent=d.vitals.caveat;
372
+ const tb=document.getElementById("vitals-tbl");
373
+ tb.innerHTML="<thead><tr><th>Page</th><th>Profile</th><th>LCP</th><th>CLS</th>"+
374
+ "<th>TBT</th><th>Score</th></tr></thead>";
375
+ const bd=el("tbody");
376
+ d.vitals.runs.forEach(r=>{
377
+ const m=r.metrics, tr=el("tr");
378
+ tr.append(cellU(r.page), td(r.profile),
379
+ num(m["largest-contentful-paint"], v=>Math.round(v)+" ms", v=>v>4000?"critical":v>2500?"medium":null),
380
+ num(m["cumulative-layout-shift"], v=>v.toFixed(3), v=>v>0.25?"critical":v>0.1?"medium":null),
381
+ num(m["total-blocking-time"], v=>Math.round(v)+" ms", v=>v>600?"high":v>200?"medium":null),
382
+ num(m._score, v=>Math.round(v*100), v=>v<0.5?"high":v<0.9?"medium":null));
383
+ bd.append(tr);
384
+ });
385
+ tb.append(bd);
386
+ }
387
+
388
+ // findings by stage
389
+ const fw=document.getElementById("findings");
390
+ for(let i=1;i<=7;i++){
391
+ const items=d.findings.filter(f=>f.stage===i);
392
+ if(!items.length) continue;
393
+ items.sort((a,b)=>SEV.indexOf(a.severity)-SEV.indexOf(b.severity));
394
+ const blk=el("div","stage-block"), hd=el("div","stage-hd");
395
+ hd.append(el("h2","","Stage "+i+" \u00b7 "+STAGE_NAMES[i]),
396
+ el("span","cnt", items.length+" \u00b7 batch B"+i));
397
+ blk.append(hd);
398
+ items.forEach(f=>{
399
+ const c=el("div","f"); c.style.setProperty("--stripe",sevVar(f.severity));
400
+ const top=el("div","f-top");
401
+ const p=el("span","pill",f.severity);
402
+ p.style.background=`var(--sev-${f.severity}-bg)`; p.style.color=sevVar(f.severity);
403
+ top.append(p, el("span","f-id",f.id));
404
+ if(f.affects) top.append(el("span","eyebrow", f.affects+" pages"));
405
+ c.append(top, el("div","f-msg",f.message));
406
+ const pg=el("div","f-pages");
407
+ f.pages.slice(0,8).forEach(u=>pg.append(el("span","u",u)));
408
+ if(f.pages.length>8) pg.append(el("span","eyebrow","+"+(f.pages.length-8)+" more"));
409
+ c.append(pg);
410
+ if(f.evidence){ const e=el("div","ev"); e.append(el("b","","evidence "), document.createTextNode(f.evidence)); c.append(e); }
411
+ blk.append(c);
412
+ });
413
+ fw.append(blk);
414
+ }
415
+
416
+ // pages
417
+ const pt=document.getElementById("pages-tbl");
418
+ pt.innerHTML="<thead><tr><th>URL</th><th>Title</th><th>Words</th><th>Schema</th></tr></thead>";
419
+ const pb=el("tbody");
420
+ d.pages.forEach(p=>{
421
+ const tr=el("tr");
422
+ const c1=el("td");
423
+ const dot=el("span","dot"); dot.style.background=p.indexable?"var(--ok)":"var(--ink-3)";
424
+ dot.title=p.indexable?"indexable":"not indexed (deliberate)";
425
+ c1.append(dot, el("span","u",p.url));
426
+ tr.append(c1, td(p.title||"\u2014"), num(p.words, v=>v, v=>v<250?"medium":null),
427
+ td(p.jsonld && p.jsonld.length ? p.jsonld.join(", ") : "\u2014"));
428
+ pb.append(tr);
429
+ });
430
+ pt.append(pb);
431
+
432
+ if(d.intentional && d.intentional.length){
433
+ const w=document.getElementById("intentional");
434
+ w.append(el("div","eyebrow","Deliberate exclusions \u2014 not defects"));
435
+ const ul=el("div","f-pages"); ul.style.marginTop="8px";
436
+ d.intentional.forEach(i=>{
437
+ const s=el("span","u", i.url+" \u00b7 "+i.why.join("; ")); ul.append(s);
438
+ });
439
+ w.append(ul);
440
+ }
441
+
442
+ // handoffs
443
+ const hw=document.getElementById("handoffs");
444
+ d.handoffs.forEach(h=>{
445
+ const c=el("div","ho");
446
+ c.append(el("h3","",h.name), el("div","where",h.where), el("p","",h.why));
447
+ hw.append(c);
448
+ });
449
+
450
+ document.getElementById("foot").textContent =
451
+ "Generated by the seo-repair skill on "+d.generated+". Every finding cites evidence "+
452
+ "from the crawl. Nothing in this dashboard was inferred from data the tool could not observe.";
453
+ }
454
+
455
+ function td(x){ return el("td","",x==null?"\u2014":String(x)); }
456
+ function cellU(u){ const c=el("td"); c.append(el("span","u",u)); return c; }
457
+ function num(v,fmt,sev){
458
+ const c=el("td","num");
459
+ if(v==null){ c.textContent="\u2014"; return c; }
460
+ c.textContent=fmt(v);
461
+ const s=sev&&sev(v); if(s) c.style.color=sevVar(s);
462
+ return c;
463
+ }
464
+
465
+ if(DATA) render(DATA);
466
+ </script>