research-agent 0.1.0

Long-term research assistant: index papers, articles, and PDFs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>research-agent</title>
<!-- Rich rendering (markdown / KaTeX / mermaid) — progressive enhancement:
     offline or blocked, the app falls back to escaped plain text. CDN assets
     are pinned and SRI-verified; a mismatch falls back to plain text. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" integrity="sha384-nB0miv6/jRmo5UMMR1wu3Gz6NLsoTkbqJghGIsx//Rlm+ZU03BU6SQNC66uf4l5+" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js" integrity="sha384-/TQbtLCAerC3jgaim+N78RZSDYV7ryeoBCVqTuzRrFec2akfBkHS7ACQ3PQhvMVi" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js" integrity="sha384-+VfUPEb0PdtChMwmBcBmykRMDd+v6D/oFmB3rZM/puCMDYcIvF968OimRh4KQY9a" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js" integrity="sha384-7zkQWkzuo3B5mTepMUcHkMB5jZaolc2xDwL6VFqjFALcbeS9Ggm/Yr2r3Dy4lfFg" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"></script>
<script type="module">
  // mermaid v11 ships as ESM only; pull it lazily and expose the same way
  // as the other UMD libs so the render helpers can feature-detect.
  // (ESM dynamic imports cannot carry SRI integrity — pinned instead.)
  import("https://cdn.jsdelivr.net/npm/mermaid@11.4.1/+esm")
    .then(m => { window.mermaid = m.default; window.mermaid.initialize({ startOnLoad: false }); })
    .catch(() => {});
</script>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%2314120E'/%3E%3Cpath d='M4 4.5h5.5L11 6v5.5H4z' fill='none' stroke='%23F0BE3A' stroke-width='1.3'/%3E%3C/svg%3E">
<style>
  /* ── Night reading room ───────────────────────────────────────────
     Warm ink-dark base; literature in serif, instrument in sans.
     Reading depth = one amber ramp used everywhere depth appears.   */
  :root {
    --bg: #14120E;
    --bg-deep: #100E0B;
    --surface: #1B1813;
    --surface-2: #221E17;
    --line: #2C2720;
    --line-soft: #241F19;
    --ink: #EAE4D8;
    --ink-dim: #A69C8A;
    --ink-faint: #6E6656;

    /* categorical (validated): sources & streams, fixed order */
    --c-blue: #3B84BC;
    --c-amber: #B87F10;
    --c-purple: #A05ECF;
    --c-green: #2FA455;

    /* sequential depth ramp: discovered → deep_read */
    --d1: #52400F; --d2: #7A5E14; --d3: #A67D1B; --d4: #D09E24; --d5: #F0BE3A;

    --ok: #2FA455;
    --warn: #D69518;
    --bad: #C4553D;

    /* theme-dependent surfaces that were previously hardcoded */
    --overlay-bg: rgba(10,8,5,.55);
    --modal-shadow: 0 24px 60px rgba(0,0,0,.45);
    --read-body: #CFC7B8;
    --sel-bg: rgba(240,190,58,.25);
    --ramp-0: #3a352c; --ramp-1: #7d745f; --ramp-2: #B7AD97;

    --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
    --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;

    --r: 6px;
  }

  /* Light theme. Warm paper rather than plain white so the amber accents and
     the serif headings keep the same character they have on dark. */
  html[data-theme="light"] {
    --bg: #F5F1E8;
    --bg-deep: #EFE9DC;
    --surface: #FFFCF6;
    --surface-2: #F7F2E6;
    --line: #D8D0BE;
    --line-soft: #E4DDCC;
    --ink: #2A2620;
    --ink-dim: #5E5749;
    --ink-faint: #8A8271;

    /* Accents darkened for contrast against a light ground (WCAG AA on text). */
    --c-blue: #2A6791;
    --c-amber: #8F6208;
    --c-purple: #7B45A3;
    --c-green: #1F7C3F;

    --d1: #EBDCA8; --d2: #D9BE68; --d3: #C09A2E; --d4: #96751B; --d5: #6B5311;

    --ok: #1F7C3F;
    --warn: #A5710D;
    --bad: #A63D28;

    --overlay-bg: rgba(60,50,30,.34);
    --modal-shadow: 0 24px 60px rgba(90,75,45,.20);
    --read-body: #3A342B;
    --sel-bg: rgba(200,150,30,.22);
    --ramp-0: #E0D8C6; --ramp-1: #B3A98F; --ramp-2: #6E6553;
  }

  * { box-sizing: border-box; }
  html, body { height: 100%; }
  html { color-scheme: dark; }
  html[data-theme="light"] { color-scheme: light; }
  body {
    margin: 0; background: var(--bg); color: var(--ink);
    font: 14px/1.55 var(--sans);
    transition: background .18s ease, color .18s ease;
  }
  ::selection { background: var(--sel-bg); }
  a { color: inherit; }
  button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
  input, select {
    font: inherit; color: var(--ink); background: var(--surface);
    border: 1px solid var(--line); border-radius: var(--r);
  }
  input:focus-visible, select:focus-visible, button:focus-visible, [tabindex]:focus-visible {
    outline: 2px solid var(--d5); outline-offset: 1px;
  }
  .mono { font-family: var(--mono); font-size: 12px; }
  .dim { color: var(--ink-dim); }
  .faint { color: var(--ink-faint); }

  /* ── shell ── */
  .app { display: grid; grid-template-columns: 232px 1fr; grid-template-rows: auto 1fr; min-height: 100vh; }
  aside { grid-row: 1 / -1; grid-column: 1; }
  /* min-width:0 — grid items default to min-width:auto and would otherwise
     refuse to shrink below their content, pushing past the track. */
  .topbar { grid-column: 2; grid-row: 1; min-width: 0; }
  main { grid-column: 2; grid-row: 2; min-width: 0; }
  /* Desktop only: the burger belongs to the mobile drawer. */
  @media (min-width: 901px) {
    #burger { display: none; }
    .scrim { display: none !important; }
  }
  @media (max-width: 900px) {
    /* One column, so the bar and content both live in column 1 and the
       sidebar leaves the flow entirely (it is fixed-positioned below). */
    .app { grid-template-columns: 1fr; }
    .topbar { grid-column: 1; grid-row: 1; }
    main { grid-column: 1; grid-row: 2; }
  }
  aside {
    background: var(--bg-deep); border-right: 1px solid var(--line);
    padding: 22px 0 18px; display: flex; flex-direction: column;
    position: sticky; top: 0; height: 100vh; z-index: 5;
  }
  .wordmark { padding: 0 22px 20px; }
  .wordmark .t { font: 600 17px/1.2 var(--serif); letter-spacing: .01em; }
  .wordmark .s { color: var(--ink-faint); font-size: 11.5px; margin-top: 3px; }
  nav { display: flex; flex-direction: column; padding: 6px 12px; gap: 1px; }
  nav a {
    display: flex; align-items: center; gap: 10px;
    padding: 7px 10px; border-radius: var(--r);
    color: var(--ink-dim); text-decoration: none; font-size: 13.5px;
  }
  nav a svg { width: 15px; height: 15px; flex: none; opacity: .8; }
  nav a:hover { color: var(--ink); background: var(--surface); }
  nav a[aria-current="page"] { color: var(--ink); background: var(--surface-2); }
  nav a .count {
    margin-left: auto; font-size: 11px; color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
  }
  nav a[aria-current="page"] .count { color: var(--ink-dim); }
  .side-foot { margin-top: auto; padding: 14px 22px 0; border-top: 1px solid var(--line-soft); }
  .side-foot .row { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--ink-dim); padding: 3px 0; }
  .dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
  .dot.ok { background: var(--ok); }
  .dot.bad { background: var(--bad); }
  .dot.dim2 { background: var(--ink-faint); }

  main { padding: 30px 36px 60px; max-width: 1160px; width: 100%; min-width: 0; }
  .page-head { display: flex; align-items: baseline; gap: 16px; margin-bottom: 6px; }
  h1 { font: 600 24px/1.2 var(--serif); margin: 0; letter-spacing: .005em; }
  .page-sub { color: var(--ink-dim); font-size: 13px; }
  .head-actions { margin-left: auto; display: flex; gap: 8px; align-items: center; }
  .section-h {
    font-size: 13px; color: var(--ink-dim); margin: 34px 0 12px;
    padding-bottom: 8px; border-bottom: 1px solid var(--line);
  }

  /* ── tiles ── */
  .tiles { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin: 22px 0 6px; }
  .tile { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px 13px; }
  .tile .n { font: 500 30px/1.1 var(--serif); font-variant-numeric: tabular-nums; }
  .tile .l { color: var(--ink-dim); font-size: 12.5px; margin-top: 2px; }
  .tile .x { margin-top: 10px; }
  .depth-strip { display: flex; gap: 2px; height: 6px; }
  .depth-strip i { flex: 1; border-radius: 2px; background: var(--line); }
  .microbar { height: 5px; background: var(--line); border-radius: 2px; overflow: hidden; }
  .microbar i { display: block; height: 100%; border-radius: 2px; }
  .tile .cap { font-size: 11px; color: var(--ink-faint); margin-top: 5px; }

  .cols { display: grid; grid-template-columns: 3fr 2fr; gap: 26px; align-items: start; }

  /* ── funnel ── */
  .funnel-row { display: grid; grid-template-columns: 108px 1fr 118px; align-items: center; gap: 12px; padding: 7px 0; }
  .funnel-row .name { font-size: 13px; color: var(--ink-dim); text-align: right; }
  .funnel-row .bar { height: 16px; display: flex; }
  .funnel-row .bar i { height: 100%; border-radius: 0 4px 4px 0; min-width: 2px; }
  .funnel-row .val { font-size: 12.5px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
  .funnel-row .val b { color: var(--ink); font-weight: 600; }
  .conv { font-size: 11.5px; color: var(--ink-faint); padding: 0 0 6px 120px; }

  /* ── activity / history ── */
  .day { margin: 20px 0 6px; display: flex; align-items: baseline; gap: 10px; }
  .day .d { font: 500 13.5px var(--serif); color: var(--ink); }
  .day .n { font-size: 11px; color: var(--ink-faint); }
  .day::after { content: ""; flex: 1; border-top: 1px solid var(--line-soft); transform: translateY(-3px); }
  .ev { display: flex; align-items: baseline; gap: 10px; padding: 4.5px 0; font-size: 13px; }
  .ev .tick { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); flex: none; width: 36px; }
  .ev .mark { width: 8px; height: 8px; border-radius: 50%; flex: none; transform: translateY(-.5px); }
  .ev a { text-decoration: none; }
  .ev a:hover { text-decoration: underline; text-underline-offset: 3px; }
  .ev .trail { color: var(--ink-faint); font-size: 12px; }

  /* ── toolbar / tables ── */
  .toolbar { display: flex; gap: 10px; align-items: center; margin: 18px 0 12px; flex-wrap: wrap; }
  .search { position: relative; flex: 1; min-width: 220px; max-width: 380px; }
  .search input { width: 100%; padding: 7px 30px 7px 32px; }
  .search svg { position: absolute; left: 10px; top: 8px; width: 15px; height: 15px; color: var(--ink-faint); }
  .search kbd {
    position: absolute; right: 8px; top: 7px; font: 11px var(--mono);
    color: var(--ink-faint); border: 1px solid var(--line); border-radius: 4px; padding: 0 5px;
  }
  select { padding: 6.5px 8px; }
  .chips { display: flex; gap: 6px; }
  .chip {
    font-size: 12px; padding: 5px 11px; border-radius: 99px;
    border: 1px solid var(--line); color: var(--ink-dim);
  }
  .chip:hover { color: var(--ink); }
  .chip.on { background: var(--surface-2); color: var(--ink); border-color: var(--ink-faint); }
  .chip .cn { color: var(--ink-faint); font-size: 11px; margin-left: 4px; }

  table { width: 100%; border-collapse: collapse; }
  thead th {
    text-align: left; font-weight: 400; font-size: 12px; color: var(--ink-faint);
    padding: 8px 10px; border-bottom: 1px solid var(--line);
  }
  tbody td { padding: 10px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
  tbody tr.rowlink { cursor: pointer; }
  tbody tr.rowlink:hover { background: var(--surface); }
  .pt { font: 400 15px/1.35 var(--serif); color: var(--ink); }
  .pt-sub { font-size: 12px; color: var(--ink-faint); margin-top: 2px; }
  .pt-abs {
    font: 400 12.5px/1.5 var(--serif); color: var(--ink-dim); margin-top: 5px;
    max-width: 58ch; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }
  .rowlink-ext { color: var(--ink-faint); padding: 2px; vertical-align: 1px; }
  .rowlink-ext:hover { color: var(--d5); }
  .sort-h { cursor: pointer; user-select: none; }
  .sort-h:hover { color: var(--ink-dim); }
  .sort-h .arr { font-size: 10px; }

  /* depth mark: 5 segments, filled up to stage */
  .dmark { display: inline-flex; gap: 2px; }
  .dmark i { width: 9px; height: 5px; border-radius: 2px; background: var(--line); }
  .dmark i.f1 { background: var(--d1); } .dmark i.f2 { background: var(--d2); }
  .dmark i.f3 { background: var(--d3); } .dmark i.f4 { background: var(--d4); }
  .dmark i.f5 { background: var(--d5); }

  .stars { display: inline-flex; gap: 1px; }
  .stars button { padding: 1px 2px; font-size: 14px; line-height: 1; color: var(--ink-faint); }
  .stars button.on { color: var(--d5); }
  .stars button:hover { color: var(--d4); }

  .rbar { width: 64px; height: 5px; background: var(--line); border-radius: 2px; overflow: hidden; }
  .rbar i { display: block; height: 100%; background: var(--c-blue); border-radius: 2px; }

  .tag {
    display: inline-block; font-size: 11px; color: var(--ink-dim);
    border: 1px solid var(--line); border-radius: 4px; padding: 1px 7px; margin-right: 4px;
  }
  .gtype { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; }
  .gtype .sw { width: 8px; height: 8px; border-radius: 2px; flex: none; }

  /* select in table */
  .stepper {
    appearance: none; -webkit-appearance: none;
    font-size: 12px; padding: 4px 22px 4px 9px; border-radius: var(--r);
    background: var(--surface-2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='6'%3E%3Cpath d='M1 1l3 3 3-3' stroke='%236E6656' fill='none' stroke-width='1.4'/%3E%3C/svg%3E") no-repeat right 8px center;
    border: 1px solid var(--line); color: var(--ink-dim); cursor: pointer;
  }
  .stepper:hover { color: var(--ink); }

  /* ── rings ── */
  .ring { transform: rotate(-90deg); }
  .ring .track { stroke: var(--line); }
  .ring .fill { stroke: var(--d4); stroke-linecap: round; transition: stroke-dashoffset .3s ease; }
  .ringwrap { position: relative; width: 44px; height: 44px; flex: none; }
  .ringwrap .pct {
    position: absolute; inset: 0; display: grid; place-items: center;
    font-size: 10.5px; color: var(--ink-dim); font-variant-numeric: tabular-nums;
  }
  .topic-row { display: flex; align-items: center; gap: 14px; padding: 10px 0; border-bottom: 1px solid var(--line-soft); }
  .topic-row:last-child { border-bottom: none; }
  .topic-row .indent { width: calc(var(--depth, 0) * 16px); flex: none; }
  .topic-row .nm { font: 400 15px var(--serif); }
  .topic-row .meta { margin-left: auto; text-align: right; font-size: 12px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }

  /* ── reports ── */
  .rep-row { display: flex; align-items: baseline; gap: 14px; padding: 13px 0; border-bottom: 1px solid var(--line-soft); }
  .rep-row .rt { font: 400 17px var(--serif); cursor: pointer; }
  .rep-row .rt:hover { text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--ink-faint); }
  .rep-row .rm { margin-left: auto; flex: none; color: var(--ink-faint); font-size: 12px; }
  .readbtn {
    font-size: 12px; padding: 4px 12px; border: 1px solid var(--line);
    border-radius: var(--r); color: var(--ink-dim); flex: none;
  }
  .readbtn:hover { color: var(--ink); border-color: var(--ink-faint); }

  /* ── drawer ── */
  .overlay {
    position: fixed; inset: 0; background: var(--overlay-bg);
    opacity: 0; pointer-events: none; visibility: hidden;
    /* Above the sticky sidebar, which creates its own stacking context and
       would otherwise stay undimmed while the rest of the page darkens. */
    transition: opacity .18s ease, visibility .18s ease; z-index: 40;
  }
  .overlay.open { opacity: 1; pointer-events: auto; visibility: visible; }
  .drawer {
    /* Centred modal, not a right-edge drawer: a paper's abstract is the
       reason to open this, and 440px of edge column made it a narrow ribbon.
       Full viewport height — the card fills the screen top to bottom, and
       overflow-y keeps a long abstract scrollable inside instead of pushing
       the page. 100vh first for browsers without dvh. */
    /* inset:0 + margin:auto centres against the visual viewport; left:50%
       measures the scrollbar-less width and lands a few px off. */
    position: fixed; inset: 0; margin: auto;
    width: min(760px, 92vw); height: 100vh; height: 100dvh;
    transform: scale(.985);
    background: var(--bg-deep); border: 1px solid var(--line); border-radius: 10px;
    box-shadow: var(--modal-shadow);
    opacity: 0; pointer-events: none; visibility: hidden;
    transition: transform .18s ease, opacity .18s ease, visibility .18s ease;
    z-index: 41; overflow-y: auto; padding: 30px 34px 36px;
  }
  .drawer.open { transform: none; opacity: 1; pointer-events: auto; visibility: visible; }
  .drawer h2 { font: 500 21px/1.3 var(--serif); margin: 4px 0 6px; }
  .drawer .close, .reader .close { position: absolute; top: 16px; right: 16px; color: var(--ink-faint); padding: 6px; }
  .drawer .close:hover, .reader .close:hover { color: var(--ink); }
  /* ── markdown bodies (papers, reports, drawer abstract/notes) ── */
  .md { white-space: normal; }
  .md p { margin: 0 0 .9em; }
  .md h1, .md h2 { font: 500 20px/1.3 var(--serif); margin: 1.4em 0 .5em; }
  .md h3, .md h4, .md h5 { font: 500 17px/1.3 var(--serif); margin: 1.2em 0 .4em; }
  .md ul, .md ol { margin: 0 0 .9em; padding-left: 1.5em; }
  .md li { margin: .25em 0; }
  .md code {
    font: 12.5px var(--mono); background: var(--surface);
    border: 1px solid var(--line-soft); border-radius: 4px; padding: 1px 5px;
  }
  .md pre {
    background: var(--bg-deep); border: 1px solid var(--line);
    border-radius: var(--r); padding: 10px 12px; margin: 0 0 .9em; overflow-x: auto;
  }
  .md pre code { background: none; border: none; padding: 0; font: 12.5px/1.6 var(--mono); }
  .md blockquote { margin: 0 0 .9em; padding: 2px 14px; border-left: 3px solid var(--line); color: var(--ink-dim); }
  .md table { width: auto; border-collapse: collapse; margin: 0 0 1em; }
  .md th, .md td { border: 1px solid var(--line); padding: 5px 10px; font-size: 13px; text-align: left; }
  .md img { max-width: 100%; border-radius: var(--r); }
  .md hr { border: none; border-top: 1px solid var(--line); margin: 1.4em 0; }
  .md a { color: var(--d5); }
  .md .katex-display { overflow-x: auto; padding: 4px 0; }
  .md pre.mermaid {
    display: flex; justify-content: center; background: var(--surface);
  }
  .md pre.mermaid svg { max-width: 100%; height: auto; }
  .kv { width: 100%; margin: 6px 0; }
  .kv td { padding: 4px 0; border: none; vertical-align: top; }
  .kv td:first-child { color: var(--ink-faint); width: 96px; font-size: 12px; padding-top: 6px; }
  .abstract { font: 400 15px/1.7 var(--serif); color: var(--ink-dim); max-width: 68ch; margin: 16px 0; }
  .drawer .sec { border-top: 1px solid var(--line-soft); margin-top: 18px; padding-top: 14px; }
  .drawer .sec .h { font-size: 12px; color: var(--ink-faint); margin-bottom: 8px; }

  /* ── report reading modal ── */
  /* Same centred card language as the paper modal (.drawer): full viewport
     height, dimmed page behind via the shared overlay, X button top-right.
     .inner is a flex column so a PDF iframe fills the leftover height exactly. */
  .reader {
    position: fixed; inset: 0; margin: auto;
    width: min(760px, 92vw); height: 100vh; height: 100dvh;
    background: var(--bg-deep); border: 1px solid var(--line); border-radius: 10px;
    box-shadow: var(--modal-shadow);
    z-index: 50; display: none;
    overflow-y: auto; padding: 30px 34px 36px;
  }
  .reader.open { display: block; }
  .reader .inner { display: flex; flex-direction: column; min-height: 100%; }
  .reader h2 { font: 500 21px/1.3 var(--serif); margin: 4px 0 6px; }
  .reader .meta { color: var(--ink-faint); font-size: 13px; margin-bottom: 26px; }
  .reader h3 { font: 500 18px var(--serif); margin: 30px 0 10px; flex: none; }
  .reader .body { font: 400 15.5px/1.75 var(--serif); color: var(--read-body); max-width: 66ch; white-space: pre-wrap; }
  .reader .page-anchor { display: flex; align-items: center; gap: 12px; margin: 26px 0 14px; font: 500 11px var(--sans); letter-spacing: .08em; color: var(--ink-dim); }
  .reader .pdf { width: 100%; flex: 1; min-height: 0; border: 1px solid var(--line); border-radius: var(--r); background: #525659; }
  .reader .page-anchor::before, .reader .page-anchor::after { content: ""; flex: 1; height: 1px; background: var(--line-soft); }

  /* ── config ── */
  .panel { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 18px 20px; margin-bottom: 14px; }
  .form { display: grid; grid-template-columns: repeat(3, minmax(160px, 1fr)); gap: 12px; }
  .form label { display: block; font-size: 12px; color: var(--ink-dim); }
  .form input { display: block; width: 100%; margin-top: 5px; padding: 7px 10px; }
  .btn {
    background: var(--surface-2); border: 1px solid var(--ink-faint);
    border-radius: var(--r); padding: 7px 16px; font-size: 13px; color: var(--ink);
  }
  .btn:hover { border-color: var(--d4); }
  .btn.ghost { border-color: var(--line); color: var(--ink-dim); }
  .btn.ghost:hover { color: var(--bad); border-color: var(--bad); }
  .panel .ph { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; font-size: 13.5px; }
  .panel .ph .copy { margin-left: auto; color: var(--ink-faint); padding: 3px; }
  .panel .ph .copy:hover { color: var(--ink); }
  .alert {
    display: flex; gap: 10px; align-items: baseline;
    border: 1px solid rgba(196,85,61,.4); background: rgba(196,85,61,.08);
    border-radius: var(--r); padding: 11px 14px; font-size: 13px; margin-bottom: 16px;
  }
  .alert .code { font-family: var(--mono); font-size: 12px; color: var(--ink-dim); background: var(--bg-deep); border-radius: 4px; padding: 1px 6px; }

  /* ── empty / onboarding ── */
  .empty-block { border: 1px dashed var(--line); border-radius: 8px; padding: 34px 28px; margin-top: 26px; }
  .empty-block h3 { font: 500 19px var(--serif); margin: 0 0 4px; }
  .empty-block p { color: var(--ink-dim); margin: 0 0 20px; }
  .steps { display: grid; gap: 0; }
  .step { display: flex; gap: 14px; padding: 11px 0; border-bottom: 1px solid var(--line-soft); align-items: baseline; }
  .step:last-child { border-bottom: none; }
  .step .no { font: 500 13px var(--serif); color: var(--d4); width: 44px; flex: none; }
  .step .cmd { font-family: var(--mono); font-size: 12px; color: var(--ink-dim); }
  .step .cmd b { color: var(--ink); font-weight: 400; }

  /* ── toast ── */
  #toasts { position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 60; }
  .toast {
    background: var(--surface-2); border: 1px solid var(--line); border-left: 3px solid var(--ok);
    border-radius: var(--r); padding: 10px 16px; font-size: 13px; max-width: 340px;
    animation: toast-in .18s ease;
  }
  .toast.err { border-left-color: var(--bad); }
  @keyframes toast-in { from { transform: translateY(6px); opacity: 0; } }

  /* ── skeletons ── */
  .skel { background: var(--surface); border-radius: 4px; height: 14px; margin: 10px 0; }
  .skel.w60 { width: 60%; } .skel.w40 { width: 40%; } .skel.w80 { width: 80%; }

  @media (prefers-reduced-motion: reduce) {
    * { transition: none !important; animation: none !important; }
  }
  @media (max-width: 900px) {
    .app { grid-template-columns: 1fr; }
    /* Off-canvas drawer rather than a horizontal strip: the nav was scrolling
       sideways and the labels were the first thing to get cut off. */
    aside {
      position: fixed; top: 0; left: 0; bottom: 0; width: 250px; height: 100vh;
      transform: translateX(-100%); transition: transform .2s ease;
      z-index: 45; overflow-y: auto;
    }
    body.nav-open aside { transform: none; }
    .scrim {
      position: fixed; inset: 0; background: var(--overlay-bg); z-index: 44;
      opacity: 0; pointer-events: none; transition: opacity .2s ease;
    }
    body.nav-open .scrim { opacity: 1; pointer-events: auto; }
    #burger { display: inline-flex; }
    main { padding: 20px 14px 60px; }
    .tiles { grid-template-columns: repeat(2, 1fr); }
    .cols { grid-template-columns: 1fr; }
  }

  /* ── keep content inside the viewport on narrow screens ── */
  @media (max-width: 620px) {
    .tiles { grid-template-columns: 1fr; }
    main { padding: 16px 12px 60px; }
    h1 { font-size: 21px; }
    .page-head { flex-wrap: wrap; gap: 6px 12px; }
    .head-actions { margin-left: 0; width: 100%; flex-wrap: wrap; }
    .toolbar { flex-wrap: wrap; gap: 8px; }
    .chips { flex-wrap: wrap; }
    /* Wide tables must not stretch the page. `display:block` + overflow turns
       each table into its own scroll box; applied to the table itself rather
       than a wrapper because these tables are rendered from several different
       places with no common container. */
    main table {
      display: block; overflow-x: auto; -webkit-overflow-scrolling: touch;
      white-space: nowrap; max-width: 100%;
    }
    /* The key/value table inside the paper modal is narrow and should wrap. */
    main table.kv { display: table; white-space: normal; }
    /* Config form: three 160px-minimum columns need 480px+gap, so it cannot
       fit a phone. Collapse to one column. */
    .form { grid-template-columns: 1fr; }
    main input, main select, main textarea { max-width: 100%; min-width: 0; }
    main label { max-width: 100%; min-width: 0; }
    /* Manual: command samples are pre-formatted and scroll on their own. */
    .manual pre { max-width: 100%; overflow-x: auto; }
    .panel, .manual div { min-width: 0; }
    .drawer, .reader { padding: 22px 18px 28px; width: 94vw; }
    .drawer h2, .reader h2 { font-size: 18px; }
  }

  /* Long unbroken strings (DOIs, paths, ids) must not widen the page. */
  .mono, .abstract, td, .ev span { overflow-wrap: anywhere; }
  /* sidebar GitHub badge */
  .side-foot a.gh { display:flex; align-items:center; gap:8px; text-decoration:none; color:var(--ink-dim); }
  .side-foot a.gh svg { width:14px; height:14px; flex:none; }
  .side-foot a.gh:hover { color:var(--ink); }
  /* manual page */
  .manual pre { font:12px/1.6 var(--mono); background:var(--bg-deep); border:1px solid var(--line); border-radius:var(--r); padding:10px 12px; overflow-x:auto; margin:4px 0 10px; white-space:pre; }
  .manual p { margin:2px 0 10px; color:var(--ink-dim); font-size:13px; }
  .manual ul { margin:0 0 8px; padding-left:18px; color:var(--ink-dim); font-size:13px; }
  .manual li { margin:3px 0; }
  .manual b { color:var(--ink); }
  .manual a { color:var(--d5); }
  /* gap-table topic pills; click scopes the table to that topic */
  button.gtopic { font:11px var(--sans); color:var(--ink-dim); background:var(--surface-2); border:1px solid var(--line); border-radius:10px; padding:1px 8px; cursor:pointer; white-space:nowrap; }
  button.gtopic:hover { color:var(--ink); border-color:var(--ink-faint); }

  /* ── top bar: hamburger (mobile) + icon actions (always) ── */
  .topbar {
    position: sticky; top: 0; z-index: 30;
    display: flex; align-items: center; gap: 8px;
    padding: 8px 14px; background: var(--bg-deep);
    border-bottom: 1px solid var(--line);
  }
  .topbar .tb-title { font: 600 14px var(--serif); letter-spacing: .01em; }
  .topbar .spacer { flex: 1; }
  .iconbtn {
    display: inline-flex; align-items: center; justify-content: center; gap: 5px;
    width: 32px; height: 32px; padding: 0;
    background: none; border: 1px solid transparent; border-radius: var(--r);
    color: var(--ink-dim); cursor: pointer; text-decoration: none;
    font: 500 11.5px var(--sans);
  }
  .iconbtn:hover { color: var(--ink); border-color: var(--line); background: var(--surface); }
  .iconbtn svg { width: 16px; height: 16px; flex: none; }
  .iconbtn.wide { width: auto; padding: 0 9px; letter-spacing: .02em; }
  /* ── language dropdown ── */
  .actions { display: flex; gap: 6px; align-items: center; }
  #slot-side { padding: 12px 22px 14px; }
  .lang { position: relative; }
  .lang-menu {
    position: absolute; top: calc(100% + 6px); right: 0; min-width: 168px;
    background: var(--surface); border: 1px solid var(--line); border-radius: 8px;
    box-shadow: var(--modal-shadow); padding: 5px; z-index: 70;
    display: none;
  }
  .lang-menu.open { display: block; }
  .lang-menu button {
    display: flex; align-items: center; gap: 8px; width: 100%;
    padding: 6px 10px; border-radius: 5px; font-size: 13px;
    color: var(--ink-dim); text-align: left;
  }
  .lang-menu button:hover { background: var(--surface-2); color: var(--ink); }
  .lang-menu button[aria-selected="true"] { color: var(--ink); background: var(--surface-2); }
  .lang-menu button[aria-selected="true"]::after {
    content: ""; margin-left: auto; width: 8px; height: 5px;
    border-left: 1.6px solid var(--d5); border-bottom: 1.6px solid var(--d5);
    transform: rotate(-45deg) translateY(-1px);
  }
  .lang-menu button .lg-code {
    margin-left: auto; font: 10px var(--mono); color: var(--ink-faint);
    text-transform: uppercase;
  }
  .lang-menu button[aria-selected="true"] .lg-code { display: none; }
  /* In the sidebar the button hugs the left edge, so open down-and-right
     (the topbar version opens down-and-left) and cap the height so a short
     window still scrolls the list instead of clipping it. */
  #slot-side .lang-menu {
    right: auto; left: 0;
    max-height: 55vh; overflow-y: auto;
  }
</style>
</head>
<body>
<div class="scrim" id="scrim"></div>
<div class="app">
  <aside>
    <div class="wordmark">
      <div class="t">research-agent</div>
      <div class="s">local research desk</div>
    </div>
    <nav id="nav"></nav>
    <!-- Desktop: the lang/theme/github cluster lives here, just above the
         status block. On mobile the JS moves it back into the topbar. -->
    <div id="slot-side"></div>
    <div class="side-foot">
      <div class="row" title="database path"><span class="dot ok"></span><span class="mono" id="foot-db" style="overflow:hidden;text-overflow:ellipsis"></span></div>
      <div class="row" id="foot-llm"><span class="dot dim2"></span><span>LLM not configured</span></div>
    </div>
  </aside>
  <div class="topbar">
    <button class="iconbtn" id="burger" aria-label="Menu" aria-expanded="false">
      <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M2 4h12M2 8h12M2 12h12"/></svg>
    </button>
    <span class="tb-title" id="tb-title"></span>
    <span class="spacer"></span>
    <span id="slot-topbar"></span>
  </div>
  <main id="main"><div class="skel w40"></div><div class="skel w80"></div><div class="skel w60"></div></main>
</div>

<div class="overlay" id="overlay"></div>
<div class="drawer" id="drawer" role="dialog" aria-label="Paper detail"></div>
<div class="reader" id="reader" role="dialog" aria-label="Report"></div>
<div id="toasts"></div>

<script>
"use strict";

/* ───────────────────────── helpers ───────────────────────── */
const $ = id => document.getElementById(id);
const esc = s => String(s ?? "").replace(/[&<>"']/g, c =>
  ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
const safeUrl = u => /^https?:\/\//i.test(u || "") ? esc(u) : null;
const nfmt = n => (n ?? 0).toLocaleString("en-US");
const pct = f => (100 * (f || 0)).toFixed(0);
const day = iso => (iso || "").slice(0, 10);
const clock = iso => (iso || "").slice(11, 16);
/* t() + {var} substitution: fmt("cap_depth", {n: 3}) */
const fmt = (key, vars) => t(key).replace(/\{(\w+)\}/g, (m, k) => (vars && k in vars) ? vars[k] : m);

const DEPTHS = ["discovered","abstract_read","skimmed","read","deep_read"];
const READS = ["unread","queued","in_progress","completed","abandoned"];
/* stage / type labels are language-keyed: DL[LANG].read -> "Read" | "읽음" */
const DL = {
  en:{discovered:"Discovered","abstract_read":"Abstract read",skimmed:"Skimmed",read:"Read","deep_read":"Deep read"},
  ko:{discovered:"발견","abstract_read":"초록 읽음",skimmed:"훑어봄",read:"읽음","deep_read":"정독"},
  ja:{discovered:"発見","abstract_read":"要約を読んだ",skimmed:"流し読み",read:"読んだ","deep_read":"精読"},
  "zh-Hans":{discovered:"已发现","abstract_read":"读过摘要",skimmed:"略读",read:"读过","deep_read":"精读"},
  "zh-Hant":{discovered:"已發現","abstract_read":"讀過摘要",skimmed:"略讀",read:"讀過","deep_read":"精讀"},
  es:{discovered:"Descubierto","abstract_read":"Resumen leído",skimmed:"Ojeado",read:"Leído","deep_read":"Leído a fondo"},
  fr:{discovered:"Découvert","abstract_read":"Résumé lu",skimmed:"Parcouru",read:"Lu","deep_read":"Lu en profondeur"},
  de:{discovered:"Entdeckt","abstract_read":"Abstract gelesen",skimmed:"Überflogen",read:"Gelesen","deep_read":"Tief gelesen"},
  pt:{discovered:"Descoberto","abstract_read":"Resumo lido",skimmed:"Folheado",read:"Lido","deep_read":"Lido a fundo"},
  ru:{discovered:"Найдена","abstract_read":"Аннотация прочитана",skimmed:"Просмотрена",read:"Прочитана","deep_read":"Изучена глубоко"},
  it:{discovered:"Trovato","abstract_read":"Abstract letto",skimmed:"Sfogliato",read:"Letto","deep_read":"Letto a fondo"},
};
const RL = {
  en:{unread:"Unread",queued:"Queued","in_progress":"In progress",completed:"Completed",abandoned:"Abandoned"},
  ko:{unread:"읽기 전",queued:"읽을 예정","in_progress":"읽는 중",completed:"다 읽음",abandoned:"중단"},
  ja:{unread:"未読",queued:"読む予定","in_progress":"読んでいる最中",completed:"読了",abandoned:"中断"},
  "zh-Hans":{unread:"未读",queued:"待读","in_progress":"阅读中",completed:"已读完",abandoned:"中途放弃"},
  "zh-Hant":{unread:"未讀",queued:"待讀","in_progress":"閱讀中",completed:"已讀完",abandoned:"中途放棄"},
  es:{unread:"Sin leer",queued:"En cola","in_progress":"Leyendo",completed:"Terminado",abandoned:"Abandonado"},
  fr:{unread:"Non lu",queued:"En file","in_progress":"En cours",completed:"Terminé",abandoned:"Abandonné"},
  de:{unread:"Ungelesen",queued:"Vorgemerkt","in_progress":"Wird gelesen",completed:"Durchgelesen",abandoned:"Abgebrochen"},
  pt:{unread:"Não lido",queued:"Na fila","in_progress":"Lendo",completed:"Concluído",abandoned:"Abandonado"},
  ru:{unread:"Не прочитана",queued:"В очереди","in_progress":"Читаю",completed:"Прочитана",abandoned:"Брошена"},
  it:{unread:"Da leggere",queued:"In coda","in_progress":"In lettura",completed:"Letto tutto",abandoned:"Abbandonato"},
};
const GL = {
  en:{missing_literature:"Missing literature",unanswered_question:"Unanswered question",methodology_gap:"Methodology gap",connection_gap:"Connection gap"},
  ko:{missing_literature:"문헌 부족",unanswered_question:"미해결 질문",methodology_gap:"방법론 갭",connection_gap:"연결 갭"},
  ja:{missing_literature:"文献の不足",unanswered_question:"未解決の問い",methodology_gap:"手法のギャップ",connection_gap:"つながりのギャップ"},
  "zh-Hans":{missing_literature:"文献缺失",unanswered_question:"尚无答案的问题",methodology_gap:"方法缺口",connection_gap:"关联缺口"},
  "zh-Hant":{missing_literature:"文獻缺失",unanswered_question:"尚無答案的問題",methodology_gap:"方法缺口",connection_gap:"關聯缺口"},
  es:{missing_literature:"Falta literatura",unanswered_question:"Pregunta sin responder",methodology_gap:"Laguna de método",connection_gap:"Falta de conexión"},
  fr:{missing_literature:"Littérature manquante",unanswered_question:"Question sans réponse",methodology_gap:"Écart de méthode",connection_gap:"Écart de liaison"},
  de:{missing_literature:"Fehlende Literatur",unanswered_question:"Offene Frage",methodology_gap:"Methodenlücke",connection_gap:"Verbindungslücke"},
  pt:{missing_literature:"Falta de literatura",unanswered_question:"Pergunta sem resposta",methodology_gap:"Lacuna de método",connection_gap:"Lacuna de conexão"},
  ru:{missing_literature:"Не хватает литературы",unanswered_question:"Открытый вопрос",methodology_gap:"Пробел в методологии",connection_gap:"Пробел связей"},
  it:{missing_literature:"Letteratura mancante",unanswered_question:"Domanda senza risposta",methodology_gap:"Lacuna di metodo",connection_gap:"Lacuna di collegamento"},
};
const SL = {
  en:{papers:"Papers",gaps:"Gaps",reports:"Reports"},
  ko:{papers:"논문",gaps:"갭",reports:"리포트"},
  ja:{papers:"論文",gaps:"ギャップ",reports:"レポート"},
  "zh-Hans":{papers:"论文",gaps:"缺口",reports:"报告"},
  "zh-Hant":{papers:"論文",gaps:"缺口",reports:"報告"},
  es:{papers:"Artículos",gaps:"Lagunas",reports:"Informes"},
  fr:{papers:"Articles",gaps:"Écarts",reports:"Rapports"},
  de:{papers:"Artikel",gaps:"Lücken",reports:"Berichte"},
  pt:{papers:"Artigos",gaps:"Lacunas",reports:"Relatórios"},
  ru:{papers:"Статьи",gaps:"Пробелы",reports:"Отчёты"},
  it:{papers:"Articoli",gaps:"Lacune",reports:"Rapporti"},
};
/* sequential amber ramp, one per depth stage */
const DEPTH_COLOR = ["#52400F","#7A5E14","#A67D1B","#D09E24","#F0BE3A"];
/* categorical, fixed order: papers stream, gaps stream, reports stream */
const STREAM_COLOR = { papers:"#3B84BC", gaps:"#B87F10", reports:"#A05ECF" };
const GAP_COLOR = { missing_literature:"#3B84BC", unanswered_question:"#B87F10", methodology_gap:"#A05ECF", connection_gap:"#2FA455" };

const ICON = {
  overview:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M2 8.5 8 3l6 5.5"/><path d="M3.5 8v5h9V8"/></svg>',
  papers:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M3 2.5h7.5L13 5v8.5H3z"/><path d="M5.5 6.5h5M5.5 9h5M5.5 11.5h3"/></svg>',
  pipeline:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M2 3h12M4 8h8M6.5 13h3"/></svg>',
  history:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><circle cx="8" cy="8" r="5.5"/><path d="M8 5v3.2l2.2 1.4"/></svg>',
  results:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><circle cx="8" cy="8" r="5.5"/><circle cx="8" cy="8" r="2.2"/></svg>',
  config:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><circle cx="8" cy="8" r="2"/><path d="M8 2v2M8 12v2M2 8h2M12 8h2M3.8 3.8l1.4 1.4M10.8 10.8l1.4 1.4M12.2 3.8l-1.4 1.4M5.2 10.8l-1.4 1.4"/></svg>',
  manual:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M2.5 3.5c2-1 4-1 5.5.5 1.5-1.5 3.5-1.5 5.5-.5v9c-2-1-4-1-5.5.5-1.5-1.5-3.5-1.5-5.5-.5z"/><path d="M8 4v9"/></svg>',
  search:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="7" cy="7" r="4.4"/><path d="m10.4 10.4 3 3"/></svg>',
  x:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" width="14" height="14"><path d="m4 4 8 8M12 4l-8 8"/></svg>',
  ext:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" width="11" height="11"><path d="M6 3h7v7M13 3 7 9"/><path d="M11 9v4H3V5h4"/></svg>',
  copy:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3" width="13" height="13"><rect x="5" y="5" width="8" height="8" rx="1.5"/><path d="M11 5V4a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 4v5.5A1.5 1.5 0 0 0 4 11h1"/></svg>',
  warn:'<svg viewBox="0 0 16 16" fill="none" stroke="#C4553D" stroke-width="1.3" width="14" height="14"><path d="M8 2.5 14 13H2z"/><path d="M8 6.5v3M8 11.4v.2"/></svg>'
};

function toast(msg, isErr) {
  const t = document.createElement("div");
  t.className = "toast" + (isErr ? " err" : "");
  t.textContent = msg;
  $("toasts").appendChild(t);
  setTimeout(() => t.remove(), 3400);
}
async function copyText(text, label) {
  try { await navigator.clipboard.writeText(text); toast(label + " " + t("copied")); }
  catch { toast(t("copy_failed"), true); }
}
async function api(path, opts) {
  const r = await fetch(path, opts);
  const body = await r.json().catch(() => ({}));
  if (!r.ok) throw new Error(body.error || (path + " failed (" + r.status + ")"));
  return body;
}

/* ───────────────────────── rich text ───────────────────────── */
/* markdown + KaTeX + mermaid as progressive enhancement: when the CDN libs
   are absent (offline), everything degrades to escaped plain text. */
const mdActive = () => !!(window.marked && window.DOMPurify);
/* inline spans: titles, descriptions, snippets — no block elements */
function mdInline(text) {
  const s = String(text ?? "");
  return s ? (mdActive() ? DOMPurify.sanitize(marked.parseInline(s)) : esc(s)) : "";
}
/* block bodies: paper texts, report sections — full markdown */
function mdBlock(text) {
  const s = String(text ?? "");
  return s ? (mdActive() ? DOMPurify.sanitize(marked.parse(s, { breaks: true })) : esc(s)) : "";
}
/* class suffix for containers whose content went through mdBlock */
const mdCls = () => (mdActive() ? " md" : "");
/* math ($…$, $$…$$, \(…\), \[…\]) + ```mermaid fences on a rendered node */
function enhance(el) {
  if (!el) return;
  if (window.renderMathInElement) {
    renderMathInElement(el, { delimiters: [
      { left: "$$", right: "$$", display: true },
      { left: "$", right: "$", display: false },
      { left: "\\[", right: "\\]", display: true },
      { left: "\\(", right: "\\)", display: false },
    ]});
  }
  if (window.mermaid) {
    el.querySelectorAll("pre > code.language-mermaid, pre > code.lang-mermaid").forEach(code => {
      const div = document.createElement("pre");
      div.className = "mermaid";
      div.textContent = code.textContent;
      code.parentElement.replaceWith(div);
    });
    const diagrams = el.querySelectorAll("pre.mermaid");
    if (diagrams.length) mermaid.run({ nodes: diagrams }).catch(() => {});
  }
}

/* ───────────────────────── state & router ───────────────────────── */
const state = {
  papers: [], papersLoaded: false,
  paperFilter: "all", paperSort: "recent", query: "",
  paperTopic: "all",
  historyFilter: "all",
  topics: [], topicQuery: "", topicSort: "name", topicFilter: "all",
  showSubtopics: false, reportsShowAll: false,
};
/* Results page payload cache: re-rendering on filter changes must not refetch. */
let resultsCache = null;
const TOPIC_COLLAPSE_AT = 30;
const REPORTS_CAP = 20;

/* ───────────────────────── theme + language ───────────────────────── */
/* Both default to the system: prefers-color-scheme for the theme, the browser
   language for the copy. An explicit toggle is remembered in localStorage and
   from then on wins over the system value. */
const I18N = {
  en: {
    nav_overview:"Overview", nav_papers:"Papers", nav_pipeline:"Pipeline",
    nav_history:"History", nav_results:"Results", nav_config:"Config", nav_manual:"Manual",
    tagline:"local research desk",
    menu:"Menu", paper_detail:"Paper detail", report_detail:"Report",
    all:"All", all_topics:"All topics", none_yet:"none yet",
    close:"Close", read_btn:"Read", read_paper:"Read paper", open_paper:"Open paper",
    search_ph:"Search title, author, tag", scope_topic:"Scope to one topic",
    sort_recent:"Recently added", sort_relevance:"Relevance", sort_rating:"Rating", sort_title:"Title",
    col_depth:"Depth", col_paper:"Paper", col_reading:"Reading",
    col_rating:"Rating", col_relevance:"Relevance",
    col_priority:"Priority", col_desc:"Description", col_topic:"Topic",
    col_type:"Type", col_found:"Found",
    t_papers:"Papers", t_completed:"Completed", t_gaps:"Gaps", t_reports:"Reports", t_avgcov:"Avg coverage",
    cap_depth:"{n} read at depth",
    cap_completed:"{p}% of library · {n} queued",
    cap_gaps:"{n} high priority",
    cap_topics:"{n} topics tracked",
    sec_depth:"Reading depth", sec_reading:"Reading pipeline", sec_coverage:"Topic coverage",
    sec_stage:"Stage detail", sec_topics:"Topics", sec_gaps:"Knowledge gaps", sec_reports:"Reports",
    ov_sub:"{p} papers across {t} topics",
    pipe_sub:"how papers move from discovery to deep reading",
    hist_sub:"{n} events", res_sub:"{g} gaps · {r} reports · {t} topics",
    conv:"{p}% advance to {next}",
    conv_aband:"{n} abandoned along the way",
    stage_depth:"Depth", stage_reading:"Reading",
    topic_read:"{n} read · {m} queued", topic_none:"no analysis yet", topic_gaps:"{n} gaps",
    shown_count:"{n} of {m} shown",
    no_match_h:"Nothing matches", no_match_p:"Clear the search or pick another filter.",
    topics_filter_ph:"Filter topics",
    sort_name:"By name", sort_coverage:"By coverage", sort_gaps:"By gaps",
    no_topics_match:"No topics match.",
    show_subs:"Show {n} sub-topics",
    no_topics:"No topics yet — create one from the command line.",
    scope_gaps:"Scope gaps to one topic", scope_gaps_btn:"Scope gaps to this topic",
    no_gaps:"No gaps recorded — run gap analysis on a topic.", no_gaps_topic:"No gaps for this topic.",
    rep_meta:"{n} sections · {d}",
    show_all_reps:"Show all {n} reports",
    no_reports:"No reports yet — generate one for a topic.",
    no_activity_h:"No activity yet", no_activity_p:"Ingest papers and run analysis to build a history.",
    ev_topic:"topic {id}",
    report_sections:"{n} sections", report_no_sections:"This report has no sections stored.",
    reader_loading:"Loading…",
    et_al:"et al.",
    reading_progress:"Reading progress", tags:"Tags", notes:"Notes",
    added:"Added", updated:"Updated", id:"ID",
    publisher:"Publisher",
    rate:"Rate {n}", depth_title:"Depth: {x}", read_status_aria:"Reading status",
    moved:"Moved to {x}", rated:"Rated {n} of 5", rating_cleared:"Rating cleared",
    llm_not_configured:"LLM not configured", llm_configured:"LLM configured",
    theme_light:"Light", theme_dark:"Dark",
    cfg_sub:"workspace settings — saved to <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> is not set in this shell. Export it, then restart the dashboard so the CLI tools pick it up too.",
    cfg_llm_desc:"Used by gap analysis and report generation. The key itself stays in your environment — only its name is stored.",
    cfg_provider:"Provider", cfg_model:"Model", cfg_env:"API key env",
    cfg_base:"Base URL", cfg_base_hint:"(OpenAI-compatible; blank = provider default)",
    cfg_load_models:"Load model list", cfg_save:"Save LLM settings", cfg_remove:"Remove",
    key_ok:"key resolved", key_missing:"key missing from environment", key_unset:"not configured",
    p_llm:"LLM", p_workspace:"Workspace", p_server:"Dashboard server",
    ws_desc:"Where the library database lives. Takes effect when the dashboard restarts.",
    ws_db:"Database file",
    srv_desc:"Serving on <span class=\"mono\">{host}</span>. Exposing to the network requires the access token below; MCP is unaffected — it runs over stdio inside your agent process and never opens a port.",
    srv_bind:"Bind address", srv_local:"localhost only",
    srv_net:"0.0.0.0 — network (token required)", srv_port:"Port",
    srv_token:"Access token", srv_token_gen:"(generated on network bind)",
    srv_token_none:"(none — localhost only)",
    srv_save:"Save settings", srv_restart:"Save &amp; restart dashboard",
    toast_required:"Provider, model, and key name are all required",
    toast_llm_saved:"LLM settings saved", toast_llm_removed:"LLM settings removed",
    toast_db_required:"Database path is required",
    loading_models:"loading models…", models_loaded:"{n} models loaded",
    restarting:"restarting…",
    restart_fail:"Dashboard did not come back — start it manually with `research dashboard`",
    copied:"copied", copy_failed:"Copy failed — select it manually",
    err_api:"Could not reach the dashboard API.", srv_unreachable:"server unreachable",
    onb_h:"Welcome", onb_sub:"your workspace is empty — three commands to a first survey",
    onb_h2:"Start your library",
    onb_p:"research-agent indexes papers, tracks what you've read, and finds what you've missed.",
    onb_s1:"Add a topic you care about",
    onb_s2:"Pull papers in from arXiv or Semantic Scholar",
    onb_s3:"Find your gaps, then generate a report",
    onb_foot:"This dashboard refreshes as the CLI works",
    onb_foot_llm:" — and once an LLM is configured, gap analysis and reports come alive",
    manual_sub:"driving research-agent from the terminal and from this desk",
  },
  ko: {
    nav_overview:"개요", nav_papers:"논문", nav_pipeline:"파이프라인",
    nav_history:"기록", nav_results:"결과", nav_config:"설정", nav_manual:"매뉴얼",
    tagline:"로컬 연구 데스크",
    menu:"메뉴", paper_detail:"논문 상세", report_detail:"리포트",
    all:"전체", all_topics:"전체 주제", none_yet:"아직 없음",
    close:"닫기", read_btn:"보기", read_paper:"본문 보기", open_paper:"원문 열기",
    search_ph:"제목·저자·태그 검색", scope_topic:"특정 주제만 보기",
    sort_recent:"최근 추가순", sort_relevance:"관련도", sort_rating:"평점", sort_title:"제목",
    col_depth:"깊이", col_paper:"논문", col_reading:"상태",
    col_rating:"평점", col_relevance:"관련도",
    col_priority:"우선순위", col_desc:"설명", col_topic:"주제",
    col_type:"유형", col_found:"발견일",
    t_papers:"논문", t_completed:"완료", t_gaps:"갭", t_reports:"리포트", t_avgcov:"평균 커버리지",
    cap_depth:"정독 {n}편",
    cap_completed:"전체의 {p}% · 읽을 예정 {n}편",
    cap_gaps:"우선순위 높은 갭 {n}건",
    cap_topics:"추적 중인 주제 {n}개",
    sec_depth:"읽은 정도", sec_reading:"읽기 파이프라인", sec_coverage:"주제 커버리지",
    sec_stage:"단계별 현황", sec_topics:"주제", sec_gaps:"지식 갭", sec_reports:"리포트",
    ov_sub:"논문 {p}편 · 주제 {t}개",
    pipe_sub:"발견부터 정독까지 논문이 거치는 단계",
    hist_sub:"이벤트 {n}건", res_sub:"갭 {g}개 · 리포트 {r}개 · 주제 {t}개",
    conv:"{next} 단계로 {p}%",
    conv_aband:"중간에 중단된 논문 {n}건",
    stage_depth:"읽은 정도", stage_reading:"읽기 상태",
    topic_read:"읽음 {n} · 예정 {m}", topic_none:"아직 분석 전", topic_gaps:"갭 {n}개",
    shown_count:"{m}편 중 {n}편 표시",
    no_match_h:"맞는 논문이 없습니다", no_match_p:"검색어를 지우거나 다른 필터를 골라 보세요.",
    topics_filter_ph:"주제 필터",
    sort_name:"이름순", sort_coverage:"커버리지순", sort_gaps:"갭 많은순",
    no_topics_match:"맞는 주제가 없습니다.",
    show_subs:"하위 주제 {n}개 더 보기",
    no_topics:"아직 주제가 없습니다 — CLI에서 만들어 보세요.",
    scope_gaps:"특정 주제의 갭만 보기", scope_gaps_btn:"이 주제의 갭만 보기",
    no_gaps:"기록된 갭이 없습니다 — 주제에 대해 갭 분석을 실행해 보세요.", no_gaps_topic:"이 주제에는 갭이 없습니다.",
    rep_meta:"섹션 {n}개 · {d}",
    show_all_reps:"리포트 {n}개 모두 보기",
    no_reports:"아직 리포트가 없습니다 — 주제를 정해 생성해 보세요.",
    no_activity_h:"아직 활동이 없습니다", no_activity_p:"논문을 수집하고 분석을 돌리면 기록이 쌓입니다.",
    ev_topic:"주제 {id}",
    report_sections:"섹션 {n}개", report_no_sections:"저장된 섹션이 없습니다.",
    reader_loading:"불러오는 중…",
    et_al:"외",
    reading_progress:"읽기 상태 · 평점", tags:"태그", notes:"메모",
    added:"추가한 날", updated:"마지막 수정", id:"ID",
    publisher:"출판사 페이지",
    rate:"{n}점으로 평가", depth_title:"읽은 정도: {x}", read_status_aria:"읽기 상태",
    moved:"상태 변경: {x}", rated:"별점 {n}점", rating_cleared:"별점을 지웠습니다",
    llm_not_configured:"LLM 미설정", llm_configured:"LLM 설정됨",
    theme_light:"라이트", theme_dark:"다크",
    cfg_sub:"워크스페이스 설정 — <span class=\"mono\">~/.research/config.toml</span>에 저장됩니다",
    cfg_alert:"<b>{env}</b> 환경변수가 이 셸에 없습니다. 내보낸 뒤 대시보드를 재시작하면 CLI에서도 인식합니다.",
    cfg_llm_desc:"갭 분석과 리포트 생성에 사용됩니다. 키 값은 환경 변수에 그대로 두고 이름만 저장합니다.",
    cfg_provider:"프로바이더", cfg_model:"모델", cfg_env:"API 키 환경변수",
    cfg_base:"Base URL", cfg_base_hint:"(OpenAI 호환 · 빈 칸이면 기본값 사용)",
    cfg_load_models:"모델 목록 불러오기", cfg_save:"LLM 설정 저장", cfg_remove:"설정 삭제",
    key_ok:"키 있음", key_missing:"환경에 키 없음", key_unset:"미설정",
    p_llm:"LLM", p_workspace:"워크스페이스", p_server:"대시보드 서버",
    ws_desc:"라이브러리 데이터베이스 위치입니다. 대시보드 재시작 시 적용됩니다.",
    ws_db:"데이터베이스 파일",
    srv_desc:"<span class=\"mono\">{host}</span>에서 서빙 중입니다. 외부 네트워크에 열려면 아래 접근 토큰이 필요합니다. MCP는 포트를 열지 않고 에이전트 프로세스 안에서 stdio로 동작하므로 영향이 없습니다.",
    srv_bind:"바인드 주소", srv_local:"로컬 전용 (localhost)",
    srv_net:"0.0.0.0 — 외부 네트워크 (토큰 필요)", srv_port:"포트",
    srv_token:"접근 토큰", srv_token_gen:"(네트워크 바인드 시 자동 생성)",
    srv_token_none:"(없음 — 로컬 전용)",
    srv_save:"설정 저장", srv_restart:"저장 후 대시보드 재시작",
    toast_required:"프로바이더·모델·키 이름을 모두 입력해 주세요",
    toast_llm_saved:"LLM 설정을 저장했습니다", toast_llm_removed:"LLM 설정을 삭제했습니다",
    toast_db_required:"데이터베이스 경로를 입력해 주세요",
    loading_models:"모델 목록을 불러오는 중…", models_loaded:"모델 {n}개를 불러왔습니다",
    restarting:"재시작하는 중…",
    restart_fail:"대시보드가 돌아오지 않습니다 — `research dashboard`를 직접 실행해 주세요",
    copied:"복사됨", copy_failed:"복사에 실패했습니다 — 직접 선택해 복사해 주세요",
    err_api:"대시보드 API에 연결할 수 없습니다.", srv_unreachable:"서버에 연결할 수 없습니다",
    onb_h:"시작하기", onb_sub:"워크스페이스가 비어 있습니다 — 명령 세 줄로 첫 조사를 시작해 보세요",
    onb_h2:"라이브러리 만들기",
    onb_p:"research-agent는 논문을 색인하고, 읽은 것을 기록하고, 놓친 부분을 찾아줍니다.",
    onb_s1:"관심 주제 추가하기",
    onb_s2:"arXiv·Semantic Scholar에서 논문 가져오기",
    onb_s3:"갭을 찾아 리포트로 정리하기",
    onb_foot:"CLI가 작업하면 대시보드도 따라 갱신됩니다",
    onb_foot_llm:" — LLM을 설정하면 갭 분석과 리포트가 동작합니다",
    manual_sub:"터미널과 대시보드에서 research-agent 사용하기",
  },
  ja: {
    nav_overview:"概要", nav_papers:"論文", nav_pipeline:"パイプライン",
    nav_history:"履歴", nav_results:"結果", nav_config:"設定", nav_manual:"マニュアル",
    tagline:"ローカル研究デスク",
    menu:"メニュー", paper_detail:"論文の詳細", report_detail:"レポート",
    all:"すべて", all_topics:"すべてのトピック", none_yet:"まだありません",
    close:"閉じる", read_btn:"表示", read_paper:"本文を読む", open_paper:"原文を開く",
    search_ph:"タイトル・著者・タグを検索", scope_topic:"特定のトピックだけ表示",
    sort_recent:"追加が新しい順", sort_relevance:"関連度", sort_rating:"評価", sort_title:"タイトル",
    col_depth:"深度", col_paper:"論文", col_reading:"状態",
    col_rating:"評価", col_relevance:"関連度",
    col_priority:"優先度", col_desc:"説明", col_topic:"トピック",
    col_type:"種類", col_found:"発見日",
    t_papers:"論文", t_completed:"読了", t_gaps:"ギャップ", t_reports:"レポート", t_avgcov:"平均カバレッジ",
    cap_depth:"精読 {n}本",
    cap_completed:"全体の {p}% · 待ち {n}本",
    cap_gaps:"優先度の高いギャップ {n}件",
    cap_topics:"追跡中のトピック {n}件",
    sec_depth:"読みの深度", sec_reading:"読書パイプライン", sec_coverage:"トピックのカバレッジ",
    sec_stage:"段階ごとの内訳", sec_topics:"トピック", sec_gaps:"知識ギャップ", sec_reports:"レポート",
    ov_sub:"論文 {p}本 · トピック {t}件",
    pipe_sub:"発見から精読まで、論文がたどる流れ",
    hist_sub:"イベント {n}件", res_sub:"ギャップ {g}件 · レポート {r}本 · トピック {t}件",
    conv:"次の「{next}」へ {p}%",
    conv_aband:"途中で中断した論文 {n}本",
    stage_depth:"読みの深度", stage_reading:"読書状態",
    topic_read:"既読 {n} · 待ち {m}", topic_none:"まだ解析なし", topic_gaps:"ギャップ {n}件",
    shown_count:"{m}本中 {n}本を表示",
    no_match_h:"該当する論文がありません", no_match_p:"検索語を消すか、別のフィルターを試してください。",
    topics_filter_ph:"トピックを絞り込み",
    sort_name:"名前順", sort_coverage:"カバレッジ順", sort_gaps:"ギャップが多い順",
    no_topics_match:"該当するトピックがありません。",
    show_subs:"サブトピック {n}件を表示",
    no_topics:"トピックがまだありません — CLIで作成してください。",
    scope_gaps:"特定トピックのギャップだけ表示", scope_gaps_btn:"このトピックのギャップだけ表示",
    no_gaps:"記録されたギャップはありません — トピックに対してギャップ分析を実行してください。", no_gaps_topic:"このトピックにギャップはありません。",
    rep_meta:"セクション {n}個 · {d}",
    show_all_reps:"レポート {n}本をすべて表示",
    no_reports:"レポートはまだありません — トピックを指定して生成してください。",
    no_activity_h:"まだ履歴がありません", no_activity_p:"論文を取り込み解析すれば、ここに記録が積もります。",
    ev_topic:"トピック {id}",
    report_sections:"セクション {n}個", report_no_sections:"保存されたセクションがありません。",
    reader_loading:"読み込み中…",
    et_al:"ほか",
    reading_progress:"読書状態 · 評価", tags:"タグ", notes:"メモ",
    added:"追加日", updated:"最終更新", id:"ID",
    publisher:"出版社ページ",
    rate:"{n}点で評価", depth_title:"読みの深度: {x}", read_status_aria:"読書状態",
    moved:"状態を「{x}」に変更", rated:"評価 {n}点", rating_cleared:"評価を消しました",
    llm_not_configured:"LLM未設定", llm_configured:"LLM設定済み",
    theme_light:"ライト", theme_dark:"ダーク",
    cfg_sub:"ワークスペース設定 — <span class=\"mono\">~/.research/config.toml</span> に保存されます",
    cfg_alert:"<b>{env}</b> がこのシェルにありません。エクスポートしてダッシュボードを再起動すれば、CLI側にも反映されます。",
    cfg_llm_desc:"ギャップ分析とレポート生成で使用します。キー本体は環境変数に残し、名前だけを保存します。",
    cfg_provider:"プロバイダー", cfg_model:"モデル", cfg_env:"APIキーの環境変数",
    cfg_base:"Base URL", cfg_base_hint:"(OpenAI互換 · 空欄ならデフォルト)",
    cfg_load_models:"モデル一覧を取得", cfg_save:"LLM設定を保存", cfg_remove:"設定を削除",
    key_ok:"キーあり", key_missing:"環境変数にキーなし", key_unset:"未設定",
    p_llm:"LLM", p_workspace:"ワークスペース", p_server:"ダッシュボードサーバー",
    ws_desc:"ライブラリのデータベースの場所です。反映はダッシュボード再起動後です。",
    ws_db:"データベースファイル",
    srv_desc:"<span class=\"mono\">{host}</span> で待ち受けています。外部ネットワークに公開するには下のアクセストークンが必要です。MCPはポートを開かずエージェントプロセス内のstdioで動くため影響ありません。",
    srv_bind:"バインドアドレス", srv_local:"ローカルのみ (localhost)",
    srv_net:"0.0.0.0 — ネットワーク公開 (トークン必須)", srv_port:"ポート",
    srv_token:"アクセストークン", srv_token_gen:"(ネットワークバインド時に自動生成)",
    srv_token_none:"(なし — ローカルのみ)",
    srv_save:"設定を保存", srv_restart:"保存してダッシュボードを再起動",
    toast_required:"プロバイダー・モデル・キー名はすべて必須です",
    toast_llm_saved:"LLM設定を保存しました", toast_llm_removed:"LLM設定を削除しました",
    toast_db_required:"データベースのパスは必須です",
    loading_models:"モデル一覧を取得中…", models_loaded:"モデル {n}件を取得しました",
    restarting:"再起動中…",
    restart_fail:"ダッシュボードが戻ってきません — `research dashboard` を手動で実行してください",
    copied:"コピーしました", copy_failed:"コピーできませんでした — 手動で選択してください",
    err_api:"ダッシュボードAPIに接続できません。", srv_unreachable:"サーバーに接続できません",
    onb_h:"はじめに", onb_sub:"ワークスペースが空です — コマンド3つで最初のサーベイを始めましょう",
    onb_h2:"ライブラリを作る",
    onb_p:"research-agentは論文を索引化し、読んだ記録を残し、読み漏れを見つけ出します。",
    onb_s1:"関心のあるトピックを追加",
    onb_s2:"arXivやSemantic Scholarから論文を取り込む",
    onb_s3:"ギャップを見つけてレポートにまとめる",
    onb_foot:"CLIが動くとダッシュボードも自動で更新されます",
    onb_foot_llm:" — LLMを設定すればギャップ分析とレポートが動きます",
    manual_sub:"ターミナルとダッシュボードからresearch-agentを使う",
  },
  "zh-Hans": {
    nav_overview:"概览", nav_papers:"论文", nav_pipeline:"流水线",
    nav_history:"历史", nav_results:"结果", nav_config:"设置", nav_manual:"手册",
    tagline:"本地研究台",
    menu:"菜单", paper_detail:"论文详情", report_detail:"报告",
    all:"全部", all_topics:"全部主题", none_yet:"暂无",
    close:"关闭", read_btn:"查看", read_paper:"阅读正文", open_paper:"打开原文",
    search_ph:"搜索标题、作者、标签", scope_topic:"只看某个主题",
    sort_recent:"最近添加", sort_relevance:"相关度", sort_rating:"评分", sort_title:"标题",
    col_depth:"深度", col_paper:"论文", col_reading:"状态",
    col_rating:"评分", col_relevance:"相关度",
    col_priority:"优先级", col_desc:"描述", col_topic:"主题",
    col_type:"类型", col_found:"发现日期",
    t_papers:"论文", t_completed:"已读完", t_gaps:"缺口", t_reports:"报告", t_avgcov:"平均覆盖率",
    cap_depth:"精读 {n} 篇",
    cap_completed:"占全库 {p}% · 待读 {n} 篇",
    cap_gaps:"高优先级缺口 {n} 个",
    cap_topics:"跟踪中的主题 {n} 个",
    sec_depth:"阅读深度", sec_reading:"阅读流水线", sec_coverage:"主题覆盖率",
    sec_stage:"各阶段明细", sec_topics:"主题", sec_gaps:"知识缺口", sec_reports:"报告",
    ov_sub:"论文 {p} 篇 · 主题 {t} 个",
    pipe_sub:"论文从发现到精读的完整路径",
    hist_sub:"{n} 条事件", res_sub:"缺口 {g} 个 · 报告 {r} 篇 · 主题 {t} 个",
    conv:"{p}% 进入「{next}」",
    conv_aband:"中途放弃 {n} 篇",
    stage_depth:"阅读深度", stage_reading:"阅读状态",
    topic_read:"已读 {n} · 待读 {m}", topic_none:"尚未分析", topic_gaps:"缺口 {n} 个",
    shown_count:"显示 {n} / {m} 篇",
    no_match_h:"没有匹配的论文", no_match_p:"清空搜索词,或换个筛选条件试试。",
    topics_filter_ph:"筛选主题",
    sort_name:"按名称", sort_coverage:"按覆盖率", sort_gaps:"按缺口数",
    no_topics_match:"没有匹配的主题。",
    show_subs:"显示 {n} 个子主题",
    no_topics:"还没有主题 — 先在命令行里创建一个吧。",
    scope_gaps:"只看某个主题的缺口", scope_gaps_btn:"只看这个主题的缺口",
    no_gaps:"还没有记录缺口 — 对某个主题运行缺口分析吧。", no_gaps_topic:"这个主题暂时没有缺口。",
    rep_meta:"{n} 个小节 · {d}",
    show_all_reps:"显示全部 {n} 篇报告",
    no_reports:"还没有报告 — 为某个主题生成一份吧。",
    no_activity_h:"还没有动态", no_activity_p:"导入论文、跑一次分析,这里就会开始积累记录。",
    ev_topic:"主题 {id}",
    report_sections:"{n} 个小节", report_no_sections:"没有保存任何小节。",
    reader_loading:"加载中…",
    et_al:"等",
    reading_progress:"阅读状态 · 评分", tags:"标签", notes:"笔记",
    added:"添加时间", updated:"最后修改", id:"ID",
    publisher:"出版方页面",
    rate:"评为 {n} 分", depth_title:"阅读深度:{x}", read_status_aria:"阅读状态",
    moved:"已改为「{x}」", rated:"评分 {n} 分", rating_cleared:"已清除评分",
    llm_not_configured:"LLM 未配置", llm_configured:"LLM 已配置",
    theme_light:"浅色", theme_dark:"深色",
    cfg_sub:"工作区设置 — 保存到 <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> 在当前 shell 里不存在。先导出它,再重启仪表盘,命令行工具也会一并识别。",
    cfg_llm_desc:"用于缺口分析和报告生成。密钥本身留在环境变量里,这里只保存变量名。",
    cfg_provider:"提供商", cfg_model:"模型", cfg_env:"API 密钥环境变量",
    cfg_base:"Base URL", cfg_base_hint:"(OpenAI 兼容 · 留空用默认值)",
    cfg_load_models:"获取模型列表", cfg_save:"保存 LLM 设置", cfg_remove:"删除设置",
    key_ok:"密钥可用", key_missing:"环境里没有密钥", key_unset:"未配置",
    p_llm:"LLM", p_workspace:"工作区", p_server:"仪表盘服务器",
    ws_desc:"文献库数据库的位置。重启仪表盘后生效。",
    ws_db:"数据库文件",
    srv_desc:"正在 <span class=\"mono\">{host}</span> 上提供服务。要暴露到网络需要下面的访问令牌;MCP 不受影响 — 它在代理进程内通过 stdio 运行,不开放端口。",
    srv_bind:"监听地址", srv_local:"仅本机 (localhost)",
    srv_net:"0.0.0.0 — 局域网/外网(需要令牌)", srv_port:"端口",
    srv_token:"访问令牌", srv_token_gen:"(绑定到网络时自动生成)",
    srv_token_none:"(无 — 仅本机)",
    srv_save:"保存设置", srv_restart:"保存并重启仪表盘",
    toast_required:"提供商、模型和密钥变量名都要填写",
    toast_llm_saved:"LLM 设置已保存", toast_llm_removed:"LLM 设置已删除",
    toast_db_required:"数据库路径不能为空",
    loading_models:"正在获取模型列表…", models_loaded:"已获取 {n} 个模型",
    restarting:"正在重启…",
    restart_fail:"仪表盘没有恢复 — 请手动运行 `research dashboard`",
    copied:"已复制", copy_failed:"复制失败 — 请手动选中复制",
    err_api:"无法连接仪表盘 API。", srv_unreachable:"无法连接服务器",
    onb_h:"开始使用", onb_sub:"工作区还是空的 — 三条命令开启第一次调研",
    onb_h2:"建起你的文献库",
    onb_p:"research-agent 会为论文建立索引,记录你读过什么,并找出你错过的部分。",
    onb_s1:"添加一个感兴趣的主题",
    onb_s2:"从 arXiv 或 Semantic Scholar 导入论文",
    onb_s3:"找出缺口,生成一份报告",
    onb_foot:"命令行这边工作,仪表盘这边就会同步更新",
    onb_foot_llm:" — 配置好 LLM 后,缺口分析和报告就能跑起来",
    manual_sub:"在终端和仪表盘里使用 research-agent",
  },
  "zh-Hant": {
    nav_overview:"總覽", nav_papers:"論文", nav_pipeline:"流程",
    nav_history:"歷史", nav_results:"結果", nav_config:"設定", nav_manual:"手冊",
    tagline:"本地研究桌",
    menu:"選單", paper_detail:"論文詳情", report_detail:"報告",
    all:"全部", all_topics:"全部主題", none_yet:"還沒有",
    close:"關閉", read_btn:"檢視", read_paper:"閱讀內文", open_paper:"開啟原文",
    search_ph:"搜尋標題、作者、標籤", scope_topic:"只看某個主題",
    sort_recent:"最近加入", sort_relevance:"相關度", sort_rating:"評分", sort_title:"標題",
    col_depth:"深度", col_paper:"論文", col_reading:"狀態",
    col_rating:"評分", col_relevance:"相關度",
    col_priority:"優先順序", col_desc:"說明", col_topic:"主題",
    col_type:"類型", col_found:"發現日期",
    t_papers:"論文", t_completed:"已讀完", t_gaps:"缺口", t_reports:"報告", t_avgcov:"平均覆蓋率",
    cap_depth:"精讀 {n} 篇",
    cap_completed:"佔全庫 {p}% · 待讀 {n} 篇",
    cap_gaps:"高優先缺口 {n} 個",
    cap_topics:"追蹤中的主題 {n} 個",
    sec_depth:"閱讀深度", sec_reading:"閱讀流程", sec_coverage:"主題覆蓋率",
    sec_stage:"各階段明細", sec_topics:"主題", sec_gaps:"知識缺口", sec_reports:"報告",
    ov_sub:"論文 {p} 篇 · 主題 {t} 個",
    pipe_sub:"論文從發現到精讀的完整路徑",
    hist_sub:"{n} 筆事件", res_sub:"缺口 {g} 個 · 報告 {r} 篇 · 主題 {t} 個",
    conv:"{p}% 進入「{next}」",
    conv_aband:"中途放棄 {n} 篇",
    stage_depth:"閱讀深度", stage_reading:"閱讀狀態",
    topic_read:"已讀 {n} · 待讀 {m}", topic_none:"尚未分析", topic_gaps:"缺口 {n} 個",
    shown_count:"顯示 {n} / {m} 篇",
    no_match_h:"沒有符合的論文", no_match_p:"清空搜尋詞,或換個篩選條件試試。",
    topics_filter_ph:"篩選主題",
    sort_name:"按名稱", sort_coverage:"按覆蓋率", sort_gaps:"按缺口數",
    no_topics_match:"沒有符合的主題。",
    show_subs:"顯示 {n} 個子主題",
    no_topics:"還沒有主題 — 先在命令列建立一個吧。",
    scope_gaps:"只看某個主題的缺口", scope_gaps_btn:"只看這個主題的缺口",
    no_gaps:"還沒有記錄缺口 — 對某個主題執行缺口分析吧。", no_gaps_topic:"這個主題暫時沒有缺口。",
    rep_meta:"{n} 個小節 · {d}",
    show_all_reps:"顯示全部 {n} 篇報告",
    no_reports:"還沒有報告 — 為某個主題產生一份吧。",
    no_activity_h:"還沒有動態", no_activity_p:"匯入論文、跑一次分析,這裡就會開始累積記錄。",
    ev_topic:"主題 {id}",
    report_sections:"{n} 個小節", report_no_sections:"沒有儲存任何小節。",
    reader_loading:"載入中…",
    et_al:"等",
    reading_progress:"閱讀狀態 · 評分", tags:"標籤", notes:"筆記",
    added:"加入時間", updated:"最後修改", id:"ID",
    publisher:"出版方頁面",
    rate:"評為 {n} 分", depth_title:"閱讀深度:{x}", read_status_aria:"閱讀狀態",
    moved:"已改為「{x}」", rated:"評分 {n} 分", rating_cleared:"已清除評分",
    llm_not_configured:"LLM 未設定", llm_configured:"LLM 已設定",
    theme_light:"淺色", theme_dark:"深色",
    cfg_sub:"工作區設定 — 儲存到 <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> 在目前的 shell 裡不存在。先匯出它,再重啟儀表板,命令列工具也會一併套用。",
    cfg_llm_desc:"用於缺口分析和報告產生。金鑰本身留在環境變數裡,這裡只儲存變數名。",
    cfg_provider:"供應商", cfg_model:"模型", cfg_env:"API 金鑰環境變數",
    cfg_base:"Base URL", cfg_base_hint:"(OpenAI 相容 · 留白用預設值)",
    cfg_load_models:"取得模型清單", cfg_save:"儲存 LLM 設定", cfg_remove:"刪除設定",
    key_ok:"金鑰可用", key_missing:"環境裡沒有金鑰", key_unset:"未設定",
    p_llm:"LLM", p_workspace:"工作區", p_server:"儀表板伺服器",
    ws_desc:"書庫資料庫的位置。重啟儀表板後生效。",
    ws_db:"資料庫檔案",
    srv_desc:"正在 <span class=\"mono\">{host}</span> 上提供服務。要對外開放需要下面的存取權杖;MCP 不受影響 — 它在代理程式內透過 stdio 運作,不開連接埠。",
    srv_bind:"監聽位址", srv_local:"僅本機 (localhost)",
    srv_net:"0.0.0.0 — 對外網路(需要權杖)", srv_port:"連接埠",
    srv_token:"存取權杖", srv_token_gen:"(綁定對外網路時自動產生)",
    srv_token_none:"(無 — 僅本機)",
    srv_save:"儲存設定", srv_restart:"儲存並重啟儀表板",
    toast_required:"供應商、模型和金鑰變數名都要填寫",
    toast_llm_saved:"LLM 設定已儲存", toast_llm_removed:"LLM 設定已刪除",
    toast_db_required:"資料庫路徑不能留空",
    loading_models:"正在取得模型清單…", models_loaded:"已取得 {n} 個模型",
    restarting:"正在重啟…",
    restart_fail:"儀表板沒有恢復 — 請手動執行 `research dashboard`",
    copied:"已複製", copy_failed:"複製失敗 — 請手動選取複製",
    err_api:"無法連上儀表板 API。", srv_unreachable:"無法連上伺服器",
    onb_h:"開始使用", onb_sub:"工作區還是空的 — 三條命令開始第一份調查",
    onb_h2:"建立你的書庫",
    onb_p:"research-agent 會為論文建立索引,記錄你讀過什麼,並找出你錯過的部分。",
    onb_s1:"新增一個感興趣的主題",
    onb_s2:"從 arXiv 或 Semantic Scholar 匯入論文",
    onb_s3:"找出缺口,產出一份報告",
    onb_foot:"命令列這邊工作,儀表板這邊就會同步更新",
    onb_foot_llm:" — 設定好 LLM 後,缺口分析和報告就能跑起來",
    manual_sub:"在終端機與儀表板使用 research-agent",
  },
  es: {
    nav_overview:"Resumen", nav_papers:"Artículos", nav_pipeline:"Flujo",
    nav_history:"Historial", nav_results:"Resultados", nav_config:"Ajustes", nav_manual:"Manual",
    tagline:"escritorio de investigación local",
    menu:"Menú", paper_detail:"Detalle del artículo", report_detail:"Informe",
    all:"Todos", all_topics:"Todos los temas", none_yet:"todavía ninguno",
    close:"Cerrar", read_btn:"Ver", read_paper:"Leer el texto", open_paper:"Abrir original",
    search_ph:"Buscar por título, autor o etiqueta", scope_topic:"Ver solo un tema",
    sort_recent:"Añadidos recientemente", sort_relevance:"Relevancia", sort_rating:"Valoración", sort_title:"Título",
    col_depth:"Profundidad", col_paper:"Artículo", col_reading:"Estado",
    col_rating:"Valoración", col_relevance:"Relevancia",
    col_priority:"Prioridad", col_desc:"Descripción", col_topic:"Tema",
    col_type:"Tipo", col_found:"Detectado",
    t_papers:"Artículos", t_completed:"Leídos", t_gaps:"Lagunas", t_reports:"Informes", t_avgcov:"Cobertura media",
    cap_depth:"{n} leídos a fondo",
    cap_completed:"{p}% de la biblioteca · {n} en cola",
    cap_gaps:"{n} de prioridad alta",
    cap_topics:"{n} temas en seguimiento",
    sec_depth:"Profundidad de lectura", sec_reading:"Flujo de lectura", sec_coverage:"Cobertura por tema",
    sec_stage:"Detalle por etapa", sec_topics:"Temas", sec_gaps:"Lagunas de conocimiento", sec_reports:"Informes",
    ov_sub:"{p} artículos en {t} temas",
    pipe_sub:"el camino de cada artículo, del descubrimiento a la lectura profunda",
    hist_sub:"{n} eventos", res_sub:"{g} lagunas · {r} informes · {t} temas",
    conv:"{p}% pasa a «{next}»",
    conv_aband:"{n} abandonados por el camino",
    stage_depth:"Profundidad", stage_reading:"Lectura",
    topic_read:"{n} leídos · {m} en cola", topic_none:"aún sin analizar", topic_gaps:"{n} lagunas",
    shown_count:"{n} de {m} mostrados",
    no_match_h:"Nada coincide", no_match_p:"Borra la búsqueda o prueba otro filtro.",
    topics_filter_ph:"Filtrar temas",
    sort_name:"Por nombre", sort_coverage:"Por cobertura", sort_gaps:"Por lagunas",
    no_topics_match:"Ningún tema coincide.",
    show_subs:"Mostrar {n} subtemas",
    no_topics:"Aún no hay temas — créalos desde la línea de comandos.",
    scope_gaps:"Ver lagunas de un solo tema", scope_gaps_btn:"Ver solo las lagunas de este tema",
    no_gaps:"No hay lagunas registradas — ejecuta el análisis de lagunas sobre un tema.", no_gaps_topic:"Este tema no tiene lagunas.",
    rep_meta:"{n} secciones · {d}",
    show_all_reps:"Mostrar los {n} informes",
    no_reports:"Aún no hay informes — genera uno para un tema.",
    no_activity_h:"Todavía sin actividad", no_activity_p:"Importa artículos y lanza análisis para llenar el historial.",
    ev_topic:"tema {id}",
    report_sections:"{n} secciones", report_no_sections:"Este informe no tiene secciones guardadas.",
    reader_loading:"Cargando…",
    et_al:"et al.",
    reading_progress:"Progreso de lectura", tags:"Etiquetas", notes:"Notas",
    added:"Añadido", updated:"Última edición", id:"ID",
    publisher:"Página del editor",
    rate:"Valorar con {n}", depth_title:"Profundidad: {x}", read_status_aria:"Estado de lectura",
    moved:"Pasa a «{x}»", rated:"Valorado con {n}", rating_cleared:"Valoración borrada",
    llm_not_configured:"LLM sin configurar", llm_configured:"LLM configurado",
    theme_light:"Claro", theme_dark:"Oscuro",
    cfg_sub:"ajustes del espacio — se guardan en <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> no está definido en esta shell. Exporta la variable y reinicia el panel para que el CLI también la vea.",
    cfg_llm_desc:"Se usa para el análisis de lagunas y la generación de informes. La clave queda en tu entorno — solo se guarda su nombre.",
    cfg_provider:"Proveedor", cfg_model:"Modelo", cfg_env:"Variable de la clave API",
    cfg_base:"URL base", cfg_base_hint:"(compatible con OpenAI; vacío = valor por defecto)",
    cfg_load_models:"Cargar lista de modelos", cfg_save:"Guardar ajustes del LLM", cfg_remove:"Eliminar",
    key_ok:"clave presente", key_missing:"falta la clave en el entorno", key_unset:"sin configurar",
    p_llm:"LLM", p_workspace:"Espacio de trabajo", p_server:"Servidor del panel",
    ws_desc:"Dónde vive la base de datos de la biblioteca. Se aplica al reiniciar el panel.",
    ws_db:"Archivo de base de datos",
    srv_desc:"Sirviendo en <span class=\"mono\">{host}</span>. Para exponerlo a la red hace falta el token de abajo; MCP no se ve afectado — corre por stdio dentro del proceso de tu agente y nunca abre puertos.",
    srv_bind:"Dirección de escucha", srv_local:"solo local (localhost)",
    srv_net:"0.0.0.0 — red (token obligatorio)", srv_port:"Puerto",
    srv_token:"Token de acceso", srv_token_gen:"(se genera al abrir a la red)",
    srv_token_none:"(ninguno — solo local)",
    srv_save:"Guardar ajustes", srv_restart:"Guardar y reiniciar el panel",
    toast_required:"Proveedor, modelo y nombre de la clave son obligatorios",
    toast_llm_saved:"Ajustes del LLM guardados", toast_llm_removed:"Ajustes del LLM eliminados",
    toast_db_required:"La ruta de la base de datos es obligatoria",
    loading_models:"cargando modelos…", models_loaded:"{n} modelos cargados",
    restarting:"reiniciando…",
    restart_fail:"El panel no volvió — arráncalo a mano con `research dashboard`",
    copied:"copiado", copy_failed:"No se pudo copiar — selecciónalo a mano",
    err_api:"No se puede conectar con la API del panel.", srv_unreachable:"servidor inalcanzable",
    onb_h:"Bienvenido", onb_sub:"tu espacio está vacío — tres comandos hasta el primer sondeo",
    onb_h2:"Arranca tu biblioteca",
    onb_p:"research-agent indexa artículos, registra lo que lees y descubre lo que se te escapó.",
    onb_s1:"Añade un tema que te interese",
    onb_s2:"Trae artículos de arXiv o Semantic Scholar",
    onb_s3:"Encuentra tus lagunas y genera un informe",
    onb_foot:"El panel se actualiza mientras trabaja el CLI",
    onb_foot_llm:" — y al configurar un LLM, el análisis de lagunas y los informes cobran vida",
    manual_sub:"usa research-agent desde la terminal y desde este escritorio",
  },
  fr: {
    nav_overview:"Aperçu", nav_papers:"Articles", nav_pipeline:"Pipeline",
    nav_history:"Historique", nav_results:"Résultats", nav_config:"Réglages", nav_manual:"Manuel",
    tagline:"bureau de recherche local",
    menu:"Menu", paper_detail:"Détail de l'article", report_detail:"Rapport",
    all:"Tous", all_topics:"Tous les sujets", none_yet:"aucun pour l'instant",
    close:"Fermer", read_btn:"Voir", read_paper:"Lire le texte", open_paper:"Ouvrir l'original",
    search_ph:"Rechercher par titre, auteur, tag", scope_topic:"Ne voir qu'un sujet",
    sort_recent:"Ajoutés récemment", sort_relevance:"Pertinence", sort_rating:"Note", sort_title:"Titre",
    col_depth:"Profondeur", col_paper:"Article", col_reading:"Statut",
    col_rating:"Note", col_relevance:"Pertinence",
    col_priority:"Priorité", col_desc:"Description", col_topic:"Sujet",
    col_type:"Type", col_found:"Détecté le",
    t_papers:"Articles", t_completed:"Lus", t_gaps:"Écarts", t_reports:"Rapports", t_avgcov:"Couverture moyenne",
    cap_depth:"{n} lus en profondeur",
    cap_completed:"{p}% de la bibliothèque · {n} en file",
    cap_gaps:"{n} de priorité haute",
    cap_topics:"{n} sujets suivis",
    sec_depth:"Profondeur de lecture", sec_reading:"Pipeline de lecture", sec_coverage:"Couverture par sujet",
    sec_stage:"Détail par étape", sec_topics:"Sujets", sec_gaps:"Écarts de connaissance", sec_reports:"Rapports",
    ov_sub:"{p} articles répartis sur {t} sujets",
    pipe_sub:"le chemin de chaque article, de la découverte à la lecture approfondie",
    hist_sub:"{n} événements", res_sub:"{g} écarts · {r} rapports · {t} sujets",
    conv:"{p}% passent à « {next} »",
    conv_aband:"{n} abandonnés en route",
    stage_depth:"Profondeur", stage_reading:"Lecture",
    topic_read:"{n} lus · {m} en file", topic_none:"pas encore analysé", topic_gaps:"{n} écarts",
    shown_count:"{n} sur {m} affichés",
    no_match_h:"Aucun résultat", no_match_p:"Effacez la recherche ou essayez un autre filtre.",
    topics_filter_ph:"Filtrer les sujets",
    sort_name:"Par nom", sort_coverage:"Par couverture", sort_gaps:"Par écarts",
    no_topics_match:"Aucun sujet ne correspond.",
    show_subs:"Afficher {n} sous-sujets",
    no_topics:"Aucun sujet pour l'instant — créez-en un en ligne de commande.",
    scope_gaps:"Voir les écarts d'un seul sujet", scope_gaps_btn:"Voir uniquement les écarts de ce sujet",
    no_gaps:"Aucun écart enregistré — lancez l'analyse des écarts sur un sujet.", no_gaps_topic:"Ce sujet n'a aucun écart.",
    rep_meta:"{n} sections · {d}",
    show_all_reps:"Afficher les {n} rapports",
    no_reports:"Aucun rapport pour l'instant — générez-en un pour un sujet.",
    no_activity_h:"Pas encore d'activité", no_activity_p:"Importez des articles et lancez des analyses pour remplir l'historique.",
    ev_topic:"sujet {id}",
    report_sections:"{n} sections", report_no_sections:"Aucune section enregistrée pour ce rapport.",
    reader_loading:"Chargement…",
    et_al:"et al.",
    reading_progress:"Avancement de lecture", tags:"Tags", notes:"Notes",
    added:"Ajouté le", updated:"Modifié le", id:"ID",
    publisher:"Page de l'éditeur",
    rate:"Noter {n}", depth_title:"Profondeur : {x}", read_status_aria:"Statut de lecture",
    moved:"Passé à « {x} »", rated:"Noté {n}", rating_cleared:"Note effacée",
    llm_not_configured:"LLM non configuré", llm_configured:"LLM configuré",
    theme_light:"Clair", theme_dark:"Sombre",
    cfg_sub:"réglages de l'espace — enregistrés dans <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> n'est pas défini dans ce shell. Exportez la variable puis redémarrez le tableau de bord pour que le CLI en profite aussi.",
    cfg_llm_desc:"Utilisé pour l'analyse des écarts et la génération de rapports. La clé reste dans votre environnement — seul son nom est enregistré.",
    cfg_provider:"Fournisseur", cfg_model:"Modèle", cfg_env:"Variable d'env. de la clé API",
    cfg_base:"URL de base", cfg_base_hint:"(compatible OpenAI ; vide = valeur par défaut)",
    cfg_load_models:"Charger la liste des modèles", cfg_save:"Enregistrer les réglages LLM", cfg_remove:"Supprimer",
    key_ok:"clé présente", key_missing:"clé absente de l'environnement", key_unset:"non configuré",
    p_llm:"LLM", p_workspace:"Espace de travail", p_server:"Serveur du tableau de bord",
    ws_desc:"Emplacement de la base de la bibliothèque. Pris en compte au redémarrage du tableau de bord.",
    ws_db:"Fichier de base de données",
    srv_desc:"En écoute sur <span class=\"mono\">{host}</span>. Une exposition réseau exige le jeton ci-dessous ; MCP n'est pas concerné — il passe par stdio dans le processus de votre agent et n'ouvre aucun port.",
    srv_bind:"Adresse d'écoute", srv_local:"local uniquement (localhost)",
    srv_net:"0.0.0.0 — réseau (jeton requis)", srv_port:"Port",
    srv_token:"Jeton d'accès", srv_token_gen:"(généré à l'ouverture réseau)",
    srv_token_none:"(aucun — local uniquement)",
    srv_save:"Enregistrer", srv_restart:"Enregistrer et redémarrer",
    toast_required:"Fournisseur, modèle et nom de la clé sont tous requis",
    toast_llm_saved:"Réglages LLM enregistrés", toast_llm_removed:"Réglages LLM supprimés",
    toast_db_required:"Le chemin de la base est requis",
    loading_models:"chargement des modèles…", models_loaded:"{n} modèles chargés",
    restarting:"redémarrage…",
    restart_fail:"Le tableau de bord ne revient pas — lancez `research dashboard` à la main",
    copied:"copié", copy_failed:"Copie impossible — sélectionnez-le manuellement",
    err_api:"Impossible de joindre l'API du tableau de bord.", srv_unreachable:"serveur injoignable",
    onb_h:"Bienvenue", onb_sub:"votre espace est vide — trois commandes jusqu'au premier état des lieux",
    onb_h2:"Lancez votre bibliothèque",
    onb_p:"research-agent indexe les articles, garde la trace de vos lectures et repère ce qui vous a échappé.",
    onb_s1:"Ajoutez un sujet qui vous tient à cœur",
    onb_s2:"Importez des articles depuis arXiv ou Semantic Scholar",
    onb_s3:"Repérez vos écarts, puis générez un rapport",
    onb_foot:"Le tableau de bord se met à jour au fil du travail du CLI",
    onb_foot_llm:" — et dès qu'un LLM est configuré, analyse des écarts et rapports s'activent",
    manual_sub:"pilotez research-agent depuis le terminal et ce bureau",
  },
  de: {
    nav_overview:"Übersicht", nav_papers:"Artikel", nav_pipeline:"Ablauf",
    nav_history:"Verlauf", nav_results:"Ergebnisse", nav_config:"Einstellungen", nav_manual:"Handbuch",
    tagline:"lokaler Forschungsschreibtisch",
    menu:"Menü", paper_detail:"Artikeldetails", report_detail:"Bericht",
    all:"Alle", all_topics:"Alle Themen", none_yet:"noch keine",
    close:"Schließen", read_btn:"Ansehen", read_paper:"Text lesen", open_paper:"Original öffnen",
    search_ph:"Titel, Autor, Tag durchsuchen", scope_topic:"Nur ein Thema anzeigen",
    sort_recent:"Zuletzt hinzugefügt", sort_relevance:"Relevanz", sort_rating:"Bewertung", sort_title:"Titel",
    col_depth:"Tiefe", col_paper:"Artikel", col_reading:"Status",
    col_rating:"Bewertung", col_relevance:"Relevanz",
    col_priority:"Priorität", col_desc:"Beschreibung", col_topic:"Thema",
    col_type:"Art", col_found:"Gefunden",
    t_papers:"Artikel", t_completed:"Gelesen", t_gaps:"Lücken", t_reports:"Berichte", t_avgcov:"Ø Abdeckung",
    cap_depth:"{n} gründlich gelesen",
    cap_completed:"{p}% der Bibliothek · {n} vorgemerkt",
    cap_gaps:"{n} mit hoher Priorität",
    cap_topics:"{n} Themen im Blick",
    sec_depth:"Lesetiefe", sec_reading:"Lese-Ablauf", sec_coverage:"Themenabdeckung",
    sec_stage:"Stufen im Detail", sec_topics:"Themen", sec_gaps:"Wissenslücken", sec_reports:"Berichte",
    ov_sub:"{p} Artikel in {t} Themen",
    pipe_sub:"der Weg jedes Artikels vom Fund bis zur vertieften Lektüre",
    hist_sub:"{n} Ereignisse", res_sub:"{g} Lücken · {r} Berichte · {t} Themen",
    conv:"{p}% gehen weiter zu „{next}“",
    conv_aband:"{n} unterwegs abgebrochen",
    stage_depth:"Tiefe", stage_reading:"Lesen",
    topic_read:"{n} gelesen · {m} vorgemerkt", topic_none:"noch nicht analysiert", topic_gaps:"{n} Lücken",
    shown_count:"{n} von {m} angezeigt",
    no_match_h:"Nichts gefunden", no_match_p:"Suche leeren oder einen anderen Filter wählen.",
    topics_filter_ph:"Themen filtern",
    sort_name:"Nach Name", sort_coverage:"Nach Abdeckung", sort_gaps:"Nach Lücken",
    no_topics_match:"Keine passenden Themen.",
    show_subs:"{n} Unterthemen einblenden",
    no_topics:"Noch keine Themen — leg eins in der Kommandozeile an.",
    scope_gaps:"Lücken eines einzelnen Themas zeigen", scope_gaps_btn:"Nur Lücken dieses Themas zeigen",
    no_gaps:"Keine Lücken erfasst — führe die Lückenanalyse für ein Thema aus.", no_gaps_topic:"Zu diesem Thema gibt es keine Lücken.",
    rep_meta:"{n} Abschnitte · {d}",
    show_all_reps:"Alle {n} Berichte zeigen",
    no_reports:"Noch keine Berichte — erstelle einen für ein Thema.",
    no_activity_h:"Noch keine Aktivität", no_activity_p:"Importiere Artikel und starte Analysen — der Verlauf füllt sich von selbst.",
    ev_topic:"Thema {id}",
    report_sections:"{n} Abschnitte", report_no_sections:"Dieser Bericht hat keine gespeicherten Abschnitte.",
    reader_loading:"Wird geladen…",
    et_al:"et al.",
    reading_progress:"Lesefortschritt", tags:"Tags", notes:"Notizen",
    added:"Hinzugefügt", updated:"Zuletzt geändert", id:"ID",
    publisher:"Verlagsseite",
    rate:"Mit {n} bewerten", depth_title:"Lesetiefe: {x}", read_status_aria:"Lesestatus",
    moved:"Verschoben zu „{x}“", rated:"{n} Sterne vergeben", rating_cleared:"Bewertung entfernt",
    llm_not_configured:"LLM nicht eingerichtet", llm_configured:"LLM eingerichtet",
    theme_light:"Hell", theme_dark:"Dunkel",
    cfg_sub:"Arbeitsbereich-Einstellungen — gespeichert in <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> ist in dieser Shell nicht gesetzt. Exportiere die Variable und starte das Dashboard neu, damit auch das CLI sie findet.",
    cfg_llm_desc:"Für Lückenanalyse und Berichte. Der Schlüssel selbst bleibt in deiner Umgebung — nur sein Name wird gespeichert.",
    cfg_provider:"Anbieter", cfg_model:"Modell", cfg_env:"Umgebungsvariable des API-Schlüssels",
    cfg_base:"Basis-URL", cfg_base_hint:"(OpenAI-kompatibel; leer = Standard)",
    cfg_load_models:"Modellliste laden", cfg_save:"LLM-Einstellungen speichern", cfg_remove:"Entfernen",
    key_ok:"Schlüssel vorhanden", key_missing:"Schlüssel fehlt in der Umgebung", key_unset:"nicht eingerichtet",
    p_llm:"LLM", p_workspace:"Arbeitsbereich", p_server:"Dashboard-Server",
    ws_desc:"Wo die Bibliotheksdatenbank liegt. Wirkt nach einem Dashboard-Neustart.",
    ws_db:"Datenbankdatei",
    srv_desc:"Läuft auf <span class=\"mono\">{host}</span>. Für den Netzwerkzugriff wird der Zugangsschlüssel unten gebraucht; MCP bleibt unberührt — es läuft per stdio im Agent-Prozess und öffnet nie einen Port.",
    srv_bind:"Bind-Adresse", srv_local:"nur lokal (localhost)",
    srv_net:"0.0.0.0 — Netzwerk (Schlüssel nötig)", srv_port:"Port",
    srv_token:"Zugangsschlüssel", srv_token_gen:"(bei Netzwerk-Bind erzeugt)",
    srv_token_none:"(keiner — nur lokal)",
    srv_save:"Einstellungen speichern", srv_restart:"Speichern &amp; Dashboard neu starten",
    toast_required:"Anbieter, Modell und Schlüsselname sind alle nötig",
    toast_llm_saved:"LLM-Einstellungen gespeichert", toast_llm_removed:"LLM-Einstellungen entfernt",
    toast_db_required:"Datenbankpfad ist nötig",
    loading_models:"Modelle werden geladen…", models_loaded:"{n} Modelle geladen",
    restarting:"Neustart…",
    restart_fail:"Dashboard kommt nicht zurück — starte `research dashboard` von Hand",
    copied:"kopiert", copy_failed:"Kopieren fehlgeschlagen — bitte manuell markieren",
    err_api:"Dashboard-API nicht erreichbar.", srv_unreachable:"Server nicht erreichbar",
    onb_h:"Willkommen", onb_sub:"dein Arbeitsbereich ist leer — drei Befehle bis zur ersten Übersicht",
    onb_h2:"Starte deine Bibliothek",
    onb_p:"research-agent indexiert Artikel, hält fest, was du gelesen hast, und zeigt dir, was dir entgangen ist.",
    onb_s1:"Lege ein Thema an, das dich interessiert",
    onb_s2:"Hol Artikel von arXiv oder Semantic Scholar",
    onb_s3:"Finde deine Lücken und erstelle einen Bericht",
    onb_foot:"Das Dashboard aktualisiert sich, während das CLI arbeitet",
    onb_foot_llm:" — und sobald ein LLM eingerichtet ist, leben Lückenanalyse und Berichte auf",
    manual_sub:"research-agent aus dem Terminal und von diesem Schreibtisch steuern",
  },
  pt: {
    nav_overview:"Visão geral", nav_papers:"Artigos", nav_pipeline:"Fluxo",
    nav_history:"Histórico", nav_results:"Resultados", nav_config:"Ajustes", nav_manual:"Manual",
    tagline:"mesa de pesquisa local",
    menu:"Menu", paper_detail:"Detalhes do artigo", report_detail:"Relatório",
    all:"Todos", all_topics:"Todos os tópicos", none_yet:"nenhum ainda",
    close:"Fechar", read_btn:"Ver", read_paper:"Ler o texto", open_paper:"Abrir original",
    search_ph:"Buscar por título, autor, tag", scope_topic:"Ver só um tópico",
    sort_recent:"Adicionados recentemente", sort_relevance:"Relevância", sort_rating:"Nota", sort_title:"Título",
    col_depth:"Profundidade", col_paper:"Artigo", col_reading:"Estado",
    col_rating:"Nota", col_relevance:"Relevância",
    col_priority:"Prioridade", col_desc:"Descrição", col_topic:"Tópico",
    col_type:"Tipo", col_found:"Encontrado",
    t_papers:"Artigos", t_completed:"Lidos", t_gaps:"Lacunas", t_reports:"Relatórios", t_avgcov:"Cobertura média",
    cap_depth:"{n} lidos a fundo",
    cap_completed:"{p}% do acervo · {n} na fila",
    cap_gaps:"{n} de prioridade alta",
    cap_topics:"{n} tópicos em acompanhamento",
    sec_depth:"Profundidade de leitura", sec_reading:"Fluxo de leitura", sec_coverage:"Cobertura por tópico",
    sec_stage:"Detalhe por etapa", sec_topics:"Tópicos", sec_gaps:"Lacunas de conhecimento", sec_reports:"Relatórios",
    ov_sub:"{p} artigos em {t} tópicos",
    pipe_sub:"o caminho de cada artigo, da descoberta à leitura a fundo",
    hist_sub:"{n} eventos", res_sub:"{g} lacunas · {r} relatórios · {t} tópicos",
    conv:"{p}% avança para “{next}”",
    conv_aband:"{n} abandonados no caminho",
    stage_depth:"Profundidade", stage_reading:"Leitura",
    topic_read:"{n} lidos · {m} na fila", topic_none:"ainda sem análise", topic_gaps:"{n} lacunas",
    shown_count:"{n} de {m} exibidos",
    no_match_h:"Nada encontrado", no_match_p:"Limpe a busca ou escolha outro filtro.",
    topics_filter_ph:"Filtrar tópicos",
    sort_name:"Por nome", sort_coverage:"Por cobertura", sort_gaps:"Por lacunas",
    no_topics_match:"Nenhum tópico corresponde.",
    show_subs:"Mostrar {n} subtópicos",
    no_topics:"Ainda sem tópicos — crie um pela linha de comando.",
    scope_gaps:"Ver lacunas de um só tópico", scope_gaps_btn:"Ver só as lacunas deste tópico",
    no_gaps:"Nenhuma lacuna registrada — rode a análise de lacunas num tópico.", no_gaps_topic:"Este tópico não tem lacunas.",
    rep_meta:"{n} seções · {d}",
    show_all_reps:"Mostrar todos os {n} relatórios",
    no_reports:"Ainda sem relatórios — gere um para um tópico.",
    no_activity_h:"Ainda sem atividade", no_activity_p:"Importe artigos e rode análises para encher o histórico.",
    ev_topic:"tópico {id}",
    report_sections:"{n} seções", report_no_sections:"Este relatório não tem seções guardadas.",
    reader_loading:"Carregando…",
    et_al:"et al.",
    reading_progress:"Progresso de leitura", tags:"Tags", notes:"Notas",
    added:"Adicionado", updated:"Última alteração", id:"ID",
    publisher:"Página da editora",
    rate:"Dar nota {n}", depth_title:"Profundidade: {x}", read_status_aria:"Estado de leitura",
    moved:"Passou para “{x}”", rated:"Nota {n} dada", rating_cleared:"Nota removida",
    llm_not_configured:"LLM não configurado", llm_configured:"LLM configurado",
    theme_light:"Claro", theme_dark:"Escuro",
    cfg_sub:"ajustes do espaço de trabalho — salvos em <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> não está definido neste shell. Exporte a variável e reinicie o painel para que o CLI também a enxergue.",
    cfg_llm_desc:"Usado na análise de lacunas e na geração de relatórios. A chave fica no seu ambiente — só o nome é guardado.",
    cfg_provider:"Provedor", cfg_model:"Modelo", cfg_env:"Variável da chave de API",
    cfg_base:"URL base", cfg_base_hint:"(compatível com OpenAI; vazio = padrão)",
    cfg_load_models:"Carregar lista de modelos", cfg_save:"Salvar ajustes do LLM", cfg_remove:"Remover",
    key_ok:"chave presente", key_missing:"faltando a chave no ambiente", key_unset:"não configurado",
    p_llm:"LLM", p_workspace:"Espaço de trabalho", p_server:"Servidor do painel",
    ws_desc:"Onde vive o banco do acervo. Vale após reiniciar o painel.",
    ws_db:"Arquivo do banco de dados",
    srv_desc:"Servindo em <span class=\"mono\">{host}</span>. Para expor à rede é preciso o token abaixo; o MCP não é afetado — roda por stdio dentro do processo do seu agente e nunca abre porta.",
    srv_bind:"Endereço de escuta", srv_local:"só local (localhost)",
    srv_net:"0.0.0.0 — rede (token obrigatório)", srv_port:"Porta",
    srv_token:"Token de acesso", srv_token_gen:"(gerado ao abrir para a rede)",
    srv_token_none:"(nenhum — só local)",
    srv_save:"Salvar ajustes", srv_restart:"Salvar e reiniciar o painel",
    toast_required:"Provedor, modelo e nome da chave são todos obrigatórios",
    toast_llm_saved:"Ajustes do LLM salvos", toast_llm_removed:"Ajustes do LLM removidos",
    toast_db_required:"O caminho do banco é obrigatório",
    loading_models:"carregando modelos…", models_loaded:"{n} modelos carregados",
    restarting:"reiniciando…",
    restart_fail:"O painel não voltou — inicie `research dashboard` na mão",
    copied:"copiado", copy_failed:"Não deu para copiar — selecione manualmente",
    err_api:"Não foi possível falar com a API do painel.", srv_unreachable:"servidor inacessível",
    onb_h:"Boas-vindas", onb_sub:"seu espaço está vazio — três comandos até o primeiro levantamento",
    onb_h2:"Comece seu acervo",
    onb_p:"O research-agent indexa artigos, registra o que você leu e descobre o que passou batido.",
    onb_s1:"Adicione um tópico do seu interesse",
    onb_s2:"Traga artigos do arXiv ou Semantic Scholar",
    onb_s3:"Encontre suas lacunas e gere um relatório",
    onb_foot:"O painel se atualiza enquanto o CLI trabalha",
    onb_foot_llm:" — e com um LLM configurado, análise de lacunas e relatórios ganham vida",
    manual_sub:"use o research-agent pelo terminal e por esta mesa",
  },
  ru: {
    nav_overview:"Обзор", nav_papers:"Статьи", nav_pipeline:"Конвейер",
    nav_history:"История", nav_results:"Результаты", nav_config:"Настройки", nav_manual:"Справка",
    tagline:"локальный исследовательский стол",
    menu:"Меню", paper_detail:"О статье", report_detail:"Отчёт",
    all:"Все", all_topics:"Все темы", none_yet:"пока нет",
    close:"Закрыть", read_btn:"Открыть", read_paper:"Читать текст", open_paper:"Открыть оригинал",
    search_ph:"Поиск по названию, автору, тегу", scope_topic:"Показать одну тему",
    sort_recent:"Сначала новые", sort_relevance:"Релевантность", sort_rating:"Оценка", sort_title:"Название",
    col_depth:"Глубина", col_paper:"Статья", col_reading:"Статус",
    col_rating:"Оценка", col_relevance:"Релевантность",
    col_priority:"Приоритет", col_desc:"Описание", col_topic:"Тема",
    col_type:"Тип", col_found:"Найдено",
    t_papers:"Статьи", t_completed:"Прочитано", t_gaps:"Пробелы", t_reports:"Отчёты", t_avgcov:"Среднее покрытие",
    cap_depth:"{n} изучены глубоко",
    cap_completed:"{p}% библиотеки · {n} в очереди",
    cap_gaps:"{n} с высоким приоритетом",
    cap_topics:"тем в работе: {n}",
    sec_depth:"Глубина чтения", sec_reading:"Конвейер чтения", sec_coverage:"Покрытие тем",
    sec_stage:"Разбивка по этапам", sec_topics:"Темы", sec_gaps:"Пробелы в знаниях", sec_reports:"Отчёты",
    ov_sub:"{p} статей в {t} темах",
    pipe_sub:"путь каждой статьи — от находки до внимательного изучения",
    hist_sub:"событий: {n}", res_sub:"пробелов: {g} · отчётов: {r} · тем: {t}",
    conv:"{p}% переходят к «{next}»",
    conv_aband:"{n} брошено по дороге",
    stage_depth:"Глубина", stage_reading:"Чтение",
    topic_read:"прочитано {n} · в очереди {m}", topic_none:"ещё не анализировалось", topic_gaps:"пробелов: {n}",
    shown_count:"показано {n} из {m}",
    no_match_h:"Ничего не найдено", no_match_p:"Очистите поиск или выберите другой фильтр.",
    topics_filter_ph:"Фильтр тем",
    sort_name:"По имени", sort_coverage:"По покрытию", sort_gaps:"По пробелам",
    no_topics_match:"Подходящих тем нет.",
    show_subs:"Показать подтемы: {n}",
    no_topics:"Тем пока нет — создайте первую в командной строке.",
    scope_gaps:"Пробелы одной темы", scope_gaps_btn:"Показать пробелы только этой темы",
    no_gaps:"Пробелов не записано — запустите анализ пробелов по теме.", no_gaps_topic:"У этой темы пробелов нет.",
    rep_meta:"разделов: {n} · {d}",
    show_all_reps:"Показать все отчёты ({n})",
    no_reports:"Отчётов пока нет — сгенерируйте по теме.",
    no_activity_h:"Активности пока нет", no_activity_p:"Импортируйте статьи и запустите анализ — история начнёт копиться.",
    ev_topic:"тема {id}",
    report_sections:"разделов: {n}", report_no_sections:"В отчёте нет сохранённых разделов.",
    reader_loading:"Загрузка…",
    et_al:"и др.",
    reading_progress:"Чтение", tags:"Теги", notes:"Заметки",
    added:"Добавлена", updated:"Обновлена", id:"ID",
    publisher:"Страница издания",
    rate:"Оценить на {n}", depth_title:"Глубина: {x}", read_status_aria:"Статус чтения",
    moved:"Статус: {x}", rated:"Оценка {n}", rating_cleared:"Оценка снята",
    llm_not_configured:"LLM не настроен", llm_configured:"LLM настроен",
    theme_light:"Светлая", theme_dark:"Тёмная",
    cfg_sub:"настройки рабочей области — хранятся в <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> не задан в этой оболочке. Экспортируйте переменную и перезапустите панель — CLI тоже её подхватит.",
    cfg_llm_desc:"Нужен для анализа пробелов и генерации отчётов. Сам ключ остаётся в окружении — хранится только имя переменной.",
    cfg_provider:"Провайдер", cfg_model:"Модель", cfg_env:"Переменная с ключом API",
    cfg_base:"Базовый URL", cfg_base_hint:"(совместимый с OpenAI; пусто = по умолчанию)",
    cfg_load_models:"Загрузить список моделей", cfg_save:"Сохранить настройки LLM", cfg_remove:"Удалить",
    key_ok:"ключ найден", key_missing:"ключа нет в окружении", key_unset:"не настроен",
    p_llm:"LLM", p_workspace:"Рабочая область", p_server:"Сервер панели",
    ws_desc:"Где лежит база библиотеки. Вступает в силу после перезапуска панели.",
    ws_db:"Файл базы данных",
    srv_desc:"Работает на <span class=\"mono\">{host}</span>. Для доступа по сети нужен токен ниже; MCP не затронут — он работает через stdio внутри процесса агента и не открывает портов.",
    srv_bind:"Адрес", srv_local:"только локально (localhost)",
    srv_net:"0.0.0.0 — по сети (нужен токен)", srv_port:"Порт",
    srv_token:"Токен доступа", srv_token_gen:"(создаётся при сетевом запуске)",
    srv_token_none:"(нет — только локально)",
    srv_save:"Сохранить настройки", srv_restart:"Сохранить и перезапустить панель",
    toast_required:"Нужны провайдер, модель и имя ключа",
    toast_llm_saved:"Настройки LLM сохранены", toast_llm_removed:"Настройки LLM удалены",
    toast_db_required:"Путь к базе обязателен",
    loading_models:"загружаем модели…", models_loaded:"моделей загружено: {n}",
    restarting:"перезапуск…",
    restart_fail:"Панель не вернулась — запустите `research dashboard` вручную",
    copied:"скопировано", copy_failed:"Не удалось скопировать — выделите вручную",
    err_api:"Не удаётся связаться с API панели.", srv_unreachable:"сервер недоступен",
    onb_h:"Добро пожаловать", onb_sub:"рабочая область пуста — три команды до первого обзора",
    onb_h2:"Создайте библиотеку",
    onb_p:"research-agent индексирует статьи, помнит прочитанное и находит то, что вы упустили.",
    onb_s1:"Добавьте интересующую тему",
    onb_s2:"Подтяните статьи с arXiv или Semantic Scholar",
    onb_s3:"Найдите пробелы и составьте отчёт",
    onb_foot:"Панель обновляется сама, пока работает CLI",
    onb_foot_llm:" — а с настроенным LLM оживают анализ пробелов и отчёты",
    manual_sub:"research-agent из терминала и с этого стола",
  },
  it: {
    nav_overview:"Panoramica", nav_papers:"Articoli", nav_pipeline:"Flusso",
    nav_history:"Cronologia", nav_results:"Risultati", nav_config:"Impostazioni", nav_manual:"Manuale",
    tagline:"scrivania di ricerca locale",
    menu:"Menu", paper_detail:"Dettagli articolo", report_detail:"Rapporto",
    all:"Tutti", all_topics:"Tutti gli argomenti", none_yet:"ancora nessuno",
    close:"Chiudi", read_btn:"Vedi", read_paper:"Leggi il testo", open_paper:"Apri l'originale",
    search_ph:"Cerca per titolo, autore, tag", scope_topic:"Mostra un solo argomento",
    sort_recent:"Aggiunti di recente", sort_relevance:"Rilevanza", sort_rating:"Voto", sort_title:"Titolo",
    col_depth:"Profondità", col_paper:"Articolo", col_reading:"Stato",
    col_rating:"Voto", col_relevance:"Rilevanza",
    col_priority:"Priorità", col_desc:"Descrizione", col_topic:"Argomento",
    col_type:"Tipo", col_found:"Trovato",
    t_papers:"Articoli", t_completed:"Letti", t_gaps:"Lacune", t_reports:"Rapporti", t_avgcov:"Copertura media",
    cap_depth:"{n} letti a fondo",
    cap_completed:"{p}% della libreria · {n} in coda",
    cap_gaps:"{n} ad alta priorità",
    cap_topics:"{n} argomenti seguiti",
    sec_depth:"Profondità di lettura", sec_reading:"Flusso di lettura", sec_coverage:"Copertura per argomento",
    sec_stage:"Dettaglio per fase", sec_topics:"Argomenti", sec_gaps:"Lacune di conoscenza", sec_reports:"Rapporti",
    ov_sub:"{p} articoli su {t} argomenti",
    pipe_sub:"il percorso di ogni articolo, dalla scoperta alla lettura approfondita",
    hist_sub:"{n} eventi", res_sub:"{g} lacune · {r} rapporti · {t} argomenti",
    conv:"{p}% passa a «{next}»",
    conv_aband:"{n} abbandonati lungo la strada",
    stage_depth:"Profondità", stage_reading:"Lettura",
    topic_read:"{n} letti · {m} in coda", topic_none:"non ancora analizzato", topic_gaps:"{n} lacune",
    shown_count:"{n} di {m} mostrati",
    no_match_h:"Nessun risultato", no_match_p:"Cancella la ricerca o prova un altro filtro.",
    topics_filter_ph:"Filtra argomenti",
    sort_name:"Per nome", sort_coverage:"Per copertura", sort_gaps:"Per lacune",
    no_topics_match:"Nessun argomento corrisponde.",
    show_subs:"Mostra {n} sotto-argomenti",
    no_topics:"Ancora nessun argomento — creane uno da riga di comando.",
    scope_gaps:"Vedi le lacune di un solo argomento", scope_gaps_btn:"Mostra solo le lacune di questo argomento",
    no_gaps:"Nessuna lacuna registrata — esegui l'analisi delle lacune su un argomento.", no_gaps_topic:"Questo argomento non ha lacune.",
    rep_meta:"{n} sezioni · {d}",
    show_all_reps:"Mostra tutti i {n} rapporti",
    no_reports:"Ancora nessun rapporto — generalo per un argomento.",
    no_activity_h:"Ancora nessuna attività", no_activity_p:"Importa articoli e lancia le analisi per riempire la cronologia.",
    ev_topic:"argomento {id}",
    report_sections:"{n} sezioni", report_no_sections:"Questo rapporto non ha sezioni salvate.",
    reader_loading:"Caricamento…",
    et_al:"et al.",
    reading_progress:"Avanzamento lettura", tags:"Tag", notes:"Note",
    added:"Aggiunto", updated:"Ultima modifica", id:"ID",
    publisher:"Pagina dell'editore",
    rate:"Vota {n}", depth_title:"Profondità: {x}", read_status_aria:"Stato di lettura",
    moved:"Passato a «{x}»", rated:"Votato {n}", rating_cleared:"Voto rimosso",
    llm_not_configured:"LLM non configurato", llm_configured:"LLM configurato",
    theme_light:"Chiaro", theme_dark:"Scuro",
    cfg_sub:"impostazioni dell'area di lavoro — salvate in <span class=\"mono\">~/.research/config.toml</span>",
    cfg_alert:"<b>{env}</b> non è impostato in questa shell. Esporta la variabile e riavvia il pannello: anche il CLI la riconoscerà.",
    cfg_llm_desc:"Usato per l'analisi delle lacune e la generazione dei rapporti. La chiave resta nel tuo ambiente — viene salvato solo il nome.",
    cfg_provider:"Provider", cfg_model:"Modello", cfg_env:"Variabile della chiave API",
    cfg_base:"URL di base", cfg_base_hint:"(compatibile OpenAI; vuoto = predefinito)",
    cfg_load_models:"Carica elenco modelli", cfg_save:"Salva impostazioni LLM", cfg_remove:"Rimuovi",
    key_ok:"chiave presente", key_missing:"chiave mancante nell'ambiente", key_unset:"non configurato",
    p_llm:"LLM", p_workspace:"Area di lavoro", p_server:"Server del pannello",
    ws_desc:"Dove vive il database della libreria. Ha effetto al riavvio del pannello.",
    ws_db:"File del database",
    srv_desc:"In servizio su <span class=\"mono\">{host}</span>. Per l'accesso di rete serve il token qui sotto; MCP non è toccato — gira via stdio nel processo del tuo agente e non apre mai porte.",
    srv_bind:"Indirizzo di ascolto", srv_local:"solo locale (localhost)",
    srv_net:"0.0.0.0 — rete (token richiesto)", srv_port:"Porta",
    srv_token:"Token di accesso", srv_token_gen:"(generato all'apertura di rete)",
    srv_token_none:"(nessuno — solo locale)",
    srv_save:"Salva impostazioni", srv_restart:"Salva e riavvia il pannello",
    toast_required:"Servono provider, modello e nome della chiave",
    toast_llm_saved:"Impostazioni LLM salvate", toast_llm_removed:"Impostazioni LLM rimosse",
    toast_db_required:"Il percorso del database è obbligatorio",
    loading_models:"caricamento modelli…", models_loaded:"{n} modelli caricati",
    restarting:"riavvio…",
    restart_fail:"Il pannello non torna — avvia `research dashboard` a mano",
    copied:"copiato", copy_failed:"Copia non riuscita — selezionalo manualmente",
    err_api:"Impossibile contattare l'API del pannello.", srv_unreachable:"server irraggiungibile",
    onb_h:"Benvenuto", onb_sub:"l'area di lavoro è vuota — tre comandi alla prima ricognizione",
    onb_h2:"Avvia la tua libreria",
    onb_p:"research-agent indicizza gli articoli, tiene traccia di ciò che leggi e scopre ciò che ti è sfuggito.",
    onb_s1:"Aggiungi un argomento che ti interessa",
    onb_s2:"Porta articoli da arXiv o Semantic Scholar",
    onb_s3:"Trova le lacune e genera un rapporto",
    onb_foot:"Il pannello si aggiorna mentre lavora il CLI",
    onb_foot_llm:" — e con un LLM configurato, analisi delle lacune e rapporti prendono vita",
    manual_sub:"usa research-agent dal terminale e da questa scrivania",
  },
};

/* UI languages mirror the README translations (docs/i18n/*): native names,
   README order. Anything untranslated falls back to English per key. */
const LANGS = [
  ["en","English"],["ko","한국어"],["ja","日本語"],["zh-Hans","简体中文"],["zh-Hant","繁體中文"],
  ["es","Español"],["fr","Français"],["de","Deutsch"],["pt","Português"],["ru","Русский"],["it","Italiano"],
];
const systemLang = () => {
  const nl = (navigator.language || "en").toLowerCase();
  if (nl.startsWith("ko")) return "ko";
  if (nl.startsWith("ja")) return "ja";
  if (nl.startsWith("zh")) return /tw|hk|hant/.test(nl) ? "zh-Hant" : "zh-Hans";
  for (const [code] of LANGS) if (nl.startsWith(code)) return code;
  return "en";
};
const systemTheme = () =>
  window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";

let LANG = localStorage.getItem("ra-lang") || systemLang();
let THEME = localStorage.getItem("ra-theme") || systemTheme();

/* Translate a key. Falls back to English, then to the key itself, so a missing
   entry degrades to readable text instead of blanking the UI. */
function t(key) {
  return (I18N[LANG] && I18N[LANG][key]) || I18N.en[key] || key;
}

/* Read a CSS custom property — chart colours live in the stylesheet so they
   follow the theme instead of being frozen at first paint. */
function cssVar(name) {
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}

function applyTheme() {
  document.documentElement.setAttribute("data-theme", THEME);
  const btn = $("theme-toggle");
  if (btn) {
    const sun = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><circle cx="8" cy="8" r="3.2"/><path d="M8 1v1.8M8 13.2V15M15 8h-1.8M2.8 8H1M12.9 3.1l-1.3 1.3M4.4 11.6l-1.3 1.3M12.9 12.9l-1.3-1.3M4.4 4.4L3.1 3.1"/></svg>';
    const moon = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.3"><path d="M13.5 9.6A5.8 5.8 0 0 1 6.4 2.5a5.8 5.8 0 1 0 7.1 7.1z"/></svg>';
    btn.innerHTML = THEME === "light" ? moon : sun;
    btn.title = THEME === "light" ? t("theme_dark") : t("theme_light");
  }
}

function applyLang() {
  document.documentElement.lang = LANG;
  const cur = $("lang-cur");
  if (cur) cur.textContent = (LANGS.find(([c]) => c === LANG) || LANGS[0])[1];
  const burger = $("burger");
  if (burger) burger.setAttribute("aria-label", t("menu"));
  $("drawer")?.setAttribute("aria-label", t("paper_detail"));
  $("reader")?.setAttribute("aria-label", t("report_detail"));
  const tag = document.querySelector(".wordmark .s");
  if (tag) tag.textContent = t("tagline");
  // The unconfigured-LLM row is static markup, so it needs translating here;
  // the configured row renders provider/model names and stays as-is.
  const llmRow = document.getElementById("foot-llm");
  if (llmRow && llmRow.dataset.configured !== "1") {
    const label = llmRow.querySelector("span:last-child");
    if (label) label.textContent = t("llm_not_configured");
  }
}

const ROUTES = ["overview","papers","pipeline","history","results","config","manual"];
function route() {
  const h = location.hash.replace(/^#\/?/, "");
  const [name, rest] = h.split("/");
  const page = ROUTES.includes(name) ? name : "overview";
  return { page, arg: rest || null };
}
window.addEventListener("hashchange", () => {
  document.body.classList.remove("nav-open");
  render();
});

/* ───────────────────────── shell ───────────────────────── */
function drawNav(counts) {
  const items = [
    ["overview",t("nav_overview"), ICON.overview, null],
    ["papers",t("nav_papers"), ICON.papers, counts.papers],
    ["pipeline",t("nav_pipeline"), ICON.pipeline, null],
    ["history",t("nav_history"), ICON.history, null],
    ["results",t("nav_results"), ICON.results, counts.gaps],
    ["config",t("nav_config"), ICON.config, null],
    ["manual",t("nav_manual"), ICON.manual, null],
  ];
  const cur = route().page;
  $("nav").innerHTML = items.map(([id, label, icon, n]) =>
    '<a href="#/' + id + '"' + (cur === id ? ' aria-current="page"' : "") + ">" +
    icon + "<span>" + label + "</span>" +
    (n != null ? '<span class="count">' + nfmt(n) + "</span>" : "") + "</a>"
  ).join("");
}
async function loadChrome() {
  try {
    const [ov, cf] = await Promise.all([api("/api/overview"), api("/api/config")]);
    $("foot-db").textContent = ov.db_path.split("/").slice(-1)[0] || ov.db_path;
    $("foot-db").title = ov.db_path;
    const llmRow = $("foot-llm");
    if (cf.llm) {
      llmRow.dataset.configured = "1";
      llmRow.innerHTML = '<span class="dot ' + (cf.llm.api_key_set ? "ok" : "bad") + '"></span>' +
        "<span>" + esc(cf.llm.provider) + " / " + esc(cf.llm.model) + "</span>";
      llmRow.title = "API key env: " + cf.llm.api_key_env + (cf.llm.api_key_set ? " (set)" : " (missing)");
    }
    return { ov, cf };
  } catch { return null; }
}

/* ───────────────────────── shared renderers ───────────────────────── */
function funnel(target, stages, counts, colors, labels) {
  const max = Math.max(1, ...stages.map(s => counts[s] || 0));
  const total = stages.reduce((a, s) => a + (counts[s] || 0), 0);
  let html = "";
  stages.forEach((s, i) => {
    const n = counts[s] || 0;
    const w = Math.round(100 * n / max);
    const share = total ? Math.round(100 * n / total) : 0;
    html += '<div class="funnel-row">' +
      '<div class="name">' + esc(labels[s]) + "</div>" +
      '<div class="bar"><i style="width:' + w + "%;background:" + colors[i] + '"></i></div>' +
      '<div class="val"><b>' + nfmt(n) + "</b> (" + share + "%)</div></div>";
    if (i < stages.length - 1 && n > 0) {
      const next = counts[stages[i + 1]] || 0;
      const conv = Math.round(100 * next / n);
      html += '<div class="conv">' + esc(fmt("conv", { p: conv, next: labels[stages[i + 1]] })) + "</div>";
    }
  });
  $(target).innerHTML = html;
}

function ring(p, radius, frac, cls) {
  const c = 2 * Math.PI * radius;
  const off = c * (1 - (frac || 0));
  return '<svg class="ring ' + (cls || "") + '" width="' + (radius * 2 + 6) + '" height="' + (radius * 2 + 6) + '">' +
    '<circle class="track" cx="' + (radius + 3) + '" cy="' + (radius + 3) + '" r="' + radius + '" fill="none" stroke-width="4"/>' +
    '<circle class="fill" cx="' + (radius + 3) + '" cy="' + (radius + 3) + '" r="' + radius + '" fill="none" stroke-width="4" ' +
    'stroke-dasharray="' + c.toFixed(1) + '" stroke-dashoffset="' + off.toFixed(1) + '"/></svg>';
}

function dmark(stage) {
  const idx = DEPTHS.indexOf(stage);
  return '<span class="dmark" title="' + esc(fmt("depth_title", { x: DL[LANG][stage] })) + '">' +
    DEPTHS.map((_, i) => "<i" + (i <= idx ? ' class="f' + (idx + 1) + '"' : "") + "></i>").join("") + "</span>";
}

function stars(paper) {
  const r = paper.rating || 0;
  return '<span class="stars" data-id="' + esc(paper.id) + '">' +
    [1,2,3,4,5].map(i => '<button data-star="' + i + '" aria-label="' + esc(fmt("rate", { n: i })) + '"' +
      (i <= r ? ' class="on"' : "") + ">★</button>").join("") + "</span>";
}

function statusSelect(paper) {
  return '<select class="stepper" data-id="' + esc(paper.id) + '" aria-label="' + esc(t("read_status_aria")) + '">' +
    READS.map(s => '<option value="' + s + '"' + (paper.reading_status === s ? " selected" : "") + ">" +
      esc(RL[LANG][s]) + "</option>").join("") + "</select>";
}

/* optimistic write with rollback */
async function patchPaper(id, body, papers) {
  const paper = papers.find(p => p.id === id);
  const before = paper ? { reading_status: paper.reading_status, rating: paper.rating } : null;
  if (paper) Object.assign(paper, body);
  try {
    const updated = await api("/api/papers/" + encodeURIComponent(id), {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (paper) Object.assign(paper, updated);
    return true;
  } catch (e) {
    if (paper && before) Object.assign(paper, before);
    toast(e.message, true);
    return false;
  }
}

/* ───────────────────────── pages ───────────────────────── */
/* ───────────────────────── results page ───────────────────────── */
/* Re-renders from the cached payload on every filter change; /api/results
   is fetched once per visit. */
function drawResults() {
  const { topics, gaps, reps } = resultsCache;
  // A filter naming a since-deleted topic would show a blank table with no
  // way to see why — drop it instead.
  if (state.topicFilter !== "all" && !topics.some(t => t.id === state.topicFilter)) {
    state.topicFilter = "all";
  }
  const tname = {};
  topics.forEach(t => { tname[t.id] = t.name; });

  // Topics: search, sort, and collapse deep levels on big lists.
  const tq = state.topicQuery.trim().toLowerCase();
  let tlist = topics.filter(t => !tq || t.name.toLowerCase().includes(tq));
  const sorter = {
    name: (a,b) => a.name.localeCompare(b.name),
    coverage: (a,b) => (b.state ? b.state.coverage_score : 0) - (a.state ? a.state.coverage_score : 0),
    gaps: (a,b) => (b.state ? b.state.gaps_identified : 0) - (a.state ? a.state.gaps_identified : 0),
  }[state.topicSort];
  tlist = tlist.slice().sort(sorter);
  const expanded = state.showSubtopics || tlist.length <= TOPIC_COLLAPSE_AT;
  const visible = expanded ? tlist : tlist.filter(t => (t.depth || 0) === 0);
  const hidden = tlist.length - visible.length;

  // Gaps: optional topic scope; the topic cell click-scopes the table too.
  const glist = state.topicFilter === "all" ? gaps : gaps.filter(g => g.topic_id === state.topicFilter);

  const shownReps = state.reportsShowAll ? reps : reps.slice(0, REPORTS_CAP);

  $("main").innerHTML = `
    <div class="page-head"><h1>${esc(t("nav_results"))}</h1>
      <span class="page-sub">${esc(fmt("res_sub", { g: nfmt(gaps.length), r: nfmt(reps.length), t: nfmt(topics.length) }))}</span></div>
    <div class="section-h" style="margin-top:24px">${esc(t("sec_topics"))}</div>
    ${topics.length ? `
      <div class="toolbar">
        <div class="search">${ICON.search}<input id="t-query" placeholder="${esc(t("topics_filter_ph"))}" value="${esc(state.topicQuery)}"></div>
        <select id="t-sort" style="margin-left:auto">
          <option value="name"${state.topicSort === "name" ? " selected" : ""}>${esc(t("sort_name"))}</option>
          <option value="coverage"${state.topicSort === "coverage" ? " selected" : ""}>${esc(t("sort_coverage"))}</option>
          <option value="gaps"${state.topicSort === "gaps" ? " selected" : ""}>${esc(t("sort_gaps"))}</option>
        </select>
      </div>
      ${visible.length ? topicRows(visible, false) : nothing(t("no_topics_match"))}
      ${hidden > 0 ? '<button class="btn ghost" id="t-toggle">' + esc(fmt("show_subs", { n: hidden })) + "</button>" : ""}`
    : nothing(t("no_topics"))}
    <div class="section-h">${esc(t("sec_gaps"))}</div>
    ${topics.length ? `<div class="toolbar"><select id="g-topic" title="${esc(t("scope_gaps"))}">
        <option value="all">${esc(t("all_topics"))}</option>
        ${topics.map(t => '<option value="' + esc(t.id) + '"' + (state.topicFilter === t.id ? " selected" : "") + ">" + esc(t.name) + "</option>").join("")}
      </select></div>` : ""}
    ${glist.length ? `<table><thead><tr><th style="width:130px">${esc(t("col_priority"))}</th><th>${esc(t("col_desc"))}</th><th style="width:160px">${esc(t("col_topic"))}</th><th style="width:190px">${esc(t("col_type"))}</th><th style="width:100px">${esc(t("col_found"))}</th></tr></thead><tbody>
        ${glist.map(g => '<tr><td><div class="rbar" style="width:100px"><i style="width:'+pct(g.priority)+'%;background:var(--c-amber)"></i></div><span class="faint mono">'+(g.priority||0).toFixed(2)+"</span></td>" +
          "<td>" + mdInline(g.description) + "</td>" +
          '<td><button class="gtopic" data-t="' + esc(g.topic_id) + '" title="' + esc(t("scope_gaps_btn")) + '">' + esc(tname[g.topic_id] || "—") + "</button></td>" +
          '<td><span class="gtype"><span class="sw" style="background:'+(GAP_COLOR[g.gap_type]||"var(--ink-faint)")+'"></span>'+esc(GL[LANG][g.gap_type] || (g.gap_type || "").replace(/_/g, " "))+"</span></td>" +
          "<td>" + esc(day(g.discovered_at)) + "</td></tr>").join("")}
      </tbody></table>` : nothing(state.topicFilter === "all" ? t("no_gaps") : t("no_gaps_topic"))}
    <div class="section-h">${esc(t("sec_reports"))}</div>
    ${reps.length ? shownReps.map((rep, i) =>
        '<div class="rep-row"><span class="rt" data-rep="' + i + '">' + mdInline(rep.title) + "</span>" +
        '<span class="rm">' + esc(fmt("rep_meta", { n: rep.sections.length, d: day(rep.generated_at) })) + "</span>" +
        '<button class="readbtn" data-rep="' + i + '">' + esc(t("read_btn")) + "</button></div>").join("") +
      (reps.length > REPORTS_CAP && !state.reportsShowAll ? '<button class="btn ghost" id="rep-all">' + esc(fmt("show_all_reps", { n: reps.length })) + "</button>" : "")
      : nothing(t("no_reports"))}`;

  const tquery = $("t-query");
  if (tquery) tquery.addEventListener("input", () => {
    const pos = tquery.selectionStart;
    state.topicQuery = tquery.value;
    drawResults();
    const again = $("t-query");
    again.focus();
    again.setSelectionRange(pos, pos);
  });
  const tsort = $("t-sort");
  if (tsort) tsort.addEventListener("change", () => { state.topicSort = tsort.value; drawResults(); });
  const ttoggle = $("t-toggle");
  if (ttoggle) ttoggle.addEventListener("click", () => { state.showSubtopics = true; drawResults(); });
  const gtopic = $("g-topic");
  if (gtopic) gtopic.addEventListener("change", () => { state.topicFilter = gtopic.value; drawResults(); });
  const repAll = $("rep-all");
  if (repAll) repAll.addEventListener("click", () => { state.reportsShowAll = true; drawResults(); });
  $("main").querySelectorAll(".gtopic").forEach(b =>
    b.addEventListener("click", () => { state.topicFilter = b.dataset.t; drawResults(); }));
  $("main").querySelectorAll("[data-rep]").forEach(el =>
    el.addEventListener("click", () => openReader(resultsCache.reps[+el.dataset.rep])));
}

/* ───────────────────────── manual (11 languages) ───────────────────────── */
/* CLI samples stay verbatim; only the prose is translated. Missing
   languages fall back to English. */
const MANUAL = {};
MANUAL.en = `
    <div>
      <div class="panel">
        <div class="ph">Quick start</div>
        <p>One-time setup, then a loop of ingest, index, query.</p>
        <pre>research init                        # workspace + config walkthrough
research ingest "query"              # pull papers (default: all sources)
research index                       # refresh the full-text index
research query "query" --evidence    # search metadata + full text</pre>
        <p class="dim">Every step is safe to re-run: ingest skips duplicates, the index syncs incrementally.</p>
      </div>
      <div class="panel">
        <div class="ph">Getting papers in</div>
        <ul>
          <li><b>Web sources</b> — <span class="mono">--source</span> accepts <span class="mono">arxiv</span>, <span class="mono">s2</span>, <span class="mono">openalex</span>, <span class="mono">europepmc</span>, <span class="mono">preprints</span>, or <span class="mono">all</span>. A failing source warns and the rest continue.</li>
          <li><b>Local PDFs</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>. Bodies are extracted with page anchors, so search results can cite a page.</li>
          <li><b>arXiv PDFs, automatic</b> — after ingest, papers with an arXiv id but no full text get their PDF downloaded to <span class="mono">pdf/</span> next to the database and indexed. Already-covered papers are skipped.</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> reads a running local Zotero API.</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span> (a Zotero library export works).</li>
          <li><b>Re-extract</b> — <span class="mono">research reingest --missing-pages</span> rebuilds bodies that predate page markers, downloading from arXiv when no local PDF exists.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Reading workflow</div>
        <p>Track progress in the <b>Papers</b> table: the stepper sets reading status (unread through abandoned), the stars set a 1–5 rating. Also scriptable:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # print stored full text</pre>
      </div>
      <div class="panel">
        <div class="ph">Topics, gaps, reports</div>
        <pre>research topics add "Topic name"
research topics add "Sub" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # LLM knowledge-gap analysis
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps and report need an <span class="mono">[llm]</span> section in the config; without one they return placeholders.</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">This dashboard</div>
        <ul>
          <li><b>Overview</b> — library size, reading depth, pipeline funnel, topic coverage.</li>
          <li><b>Papers</b> — the library table; click a row for the detail modal, open the built-in reader for page-anchored bodies or the stored PDF.</li>
          <li><b>Pipeline</b> — where every paper sits from discovered to deep read.</li>
          <li><b>History</b> — a reverse-chronological feed of everything that happened.</li>
          <li><b>Results</b> — knowledge gaps and generated reports.</li>
          <li><b>Config</b> — edit <span class="mono">[llm]</span>, workspace path, and dashboard bind settings. Binding off 127.0.0.1 requires a token.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Search index</div>
        <p>Search is FTS5 full-text over title, abstract, notes, tags, keywords, and extracted body. The index is maintained automatically on every write.</p>
        <pre>research index --rebuild     # force a full rebuild
research enrich             # add search keywords</pre>
        <p class="dim">Keywords cover the words a paper does <i>not</i> contain — synonyms, expanded acronyms, alternative phrasings — so a query worded differently from the abstract still finds it. <span class="mono">research enrich</span> uses <span class="mono">[llm]</span> when configured; without one it lists the papers needing keywords for an agent to fill in.</p>
      </div>
      <div class="panel">
        <div class="ph">Zotero write-back</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">Dry-run by default; tags are matched by DOI. CLI-only by design.</p>
      </div>
      <div class="panel">
        <div class="ph">Agent mode (MCP)</div>
        <pre>research mcp</pre>
        <p>Runs a stdio MCP server so hosts like Claude Code or Codex can drive the whole tool. Point the host at this binary and command.</p>
      </div>
      <div class="panel">
        <div class="ph">Config file</div>
        <p><span class="mono">~/.research/config.toml</span> — two sections: <span class="mono">[llm]</span> (provider, model, the key's <i>env var name</i>) and <span class="mono">[dashboard]</span> (host, port, token). Secrets are never written to the file.</p>
      </div>
      <div class="panel">
        <div class="ph">Source</div>
        <p>Open source at <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — issues and stars welcome.</p>
      </div>
    </div>`;
MANUAL.ko = `
    <div>
      <div class="panel">
        <div class="ph">빠른 시작</div>
        <p>한 번 설정한 뒤 수집 → 색인 → 검색 루프.</p>
        <pre>research init                        # 워크스페이스 + 설정 안내
research ingest "query"              # 논문 수집 (기본: 전체 소스)
research index                       # 전문 색인 갱신
research query "query" --evidence    # 메타데이터 + 본문 검색</pre>
        <p class="dim">모든 단계는 재실행해도 안전 — ingest는 중복을 건너뛰고, 색인은 증분 동기화합니다.</p>
      </div>
      <div class="panel">
        <div class="ph">논문 수집</div>
        <ul>
          <li><b>웹 소스</b> — <span class="mono">--source</span>에 <span class="mono">arxiv</span>, <span class="mono">s2</span>, <span class="mono">openalex</span>, <span class="mono">europepmc</span>, <span class="mono">preprints</span>, <span class="mono">all</span> 지정. 실패한 소스는 경고만 하고 나머지는 계속합니다.</li>
          <li><b>로컬 PDF</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>. 본문을 페이지 앵커와 함께 추출해 검색 결과가 페이지를 인용할 수 있습니다.</li>
          <li><b>arXiv PDF 자동 수집</b> — ingest 후 arXiv id는 있지만 본문이 없는 논문은 PDF를 DB 옆 <span class="mono">pdf/</span>에 내려받아 색인합니다. 이미 처리된 논문은 건너뜁니다.</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span>는 실행 중인 로컬 Zotero API를 읽습니다.</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span> (Zotero 라이브러리 내보내기도 됩니다).</li>
          <li><b>재추출</b> — <span class="mono">research reingest --missing-pages</span>는 페이지 마커 이전 본문을 다시 만들고, 로컬 PDF가 없으면 arXiv에서 내려받습니다.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">읽기 워크플로</div>
        <p><b>논문</b> 표에서 진행 상황을 기록합니다. 상태 선택 메뉴로 읽기 상태(읽기 전~중단)를, 별점으로 1~5점 평점을 남기세요. CLI로도 가능합니다:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # 저장된 본문 출력</pre>
      </div>
      <div class="panel">
        <div class="ph">주제·갭·리포트</div>
        <pre>research topics add "주제 이름"
research topics add "하위" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # LLM 지식 갭 분석
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps와 report는 설정에 <span class="mono">[llm]</span> 섹션이 필요합니다. 없으면 자리표시자를 반환합니다.</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">대시보드 둘러보기</div>
        <ul>
          <li><b>개요</b> — 라이브러리 규모, 읽은 정도, 파이프라인 퍼널, 주제 커버리지.</li>
          <li><b>논문</b> — 라이브러리 표. 행을 클릭하면 상세 모달이 열리고, 내장 리더로 페이지 앵커가 붙은 본문이나 저장된 PDF를 읽을 수 있습니다.</li>
          <li><b>파이프라인</b> — 발견부터 정독까지 모든 논문이 지금 어디에 있는지.</li>
          <li><b>기록</b> — 그동안 일어난 일을 최신순으로 보여주는 피드.</li>
          <li><b>결과</b> — 지식 갭과 생성된 리포트.</li>
          <li><b>설정</b> — <span class="mono">[llm]</span>, 워크스페이스 경로, 대시보드 바인드 설정 편집. 127.0.0.1 밖으로 열 때는 토큰이 필요합니다.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">검색 색인</div>
        <p>검색은 제목·초록·메모·태그·키워드·추출 본문에 대한 FTS5 전문 검색입니다. 색인은 모든 쓰기에서 자동으로 유지됩니다.</p>
        <pre>research index --rebuild     # 전체 재구축
research enrich             # 검색 키워드 추가</pre>
        <p class="dim">키워드는 논문에 <i>없는</i> 단어 — 동의어, 약어 풀네임, 다른 표현 — 를 보완해 주기 때문에, 초록과 다른 표현으로 검색해도 찾아집니다. <span class="mono">research enrich</span>는 <span class="mono">[llm]</span>이 설정되어 있으면 LLM을 쓰고, 없으면 키워드가 필요한 논문 목록을 보여줍니다(에이전트가 채워 넣습니다).</p>
      </div>
      <div class="panel">
        <div class="ph">Zotero 쓰기</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">기본은 드라이런이며 태그는 DOI로 매칭합니다. CLI 전용입니다.</p>
      </div>
      <div class="panel">
        <div class="ph">에이전트 모드 (MCP)</div>
        <pre>research mcp</pre>
        <p>stdio MCP 서버를 띄워 Claude Code·Codex 같은 호스트가 도구 전체를 구동할 수 있습니다. 호스트가 이 바이너리와 명령을 가리키게 하세요.</p>
      </div>
      <div class="panel">
        <div class="ph">설정 파일</div>
        <p><span class="mono">~/.research/config.toml</span> — 두 섹션: <span class="mono">[llm]</span> (프로바이더, 모델, 키의 <i>환경변수 이름</i>)과 <span class="mono">[dashboard]</span> (호스트, 포트, 토큰). 비밀값은 파일에 쓰지 않습니다.</p>
      </div>
      <div class="panel">
        <div class="ph">소스</div>
        <p><a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a>에서 오픈소스 — 이슈와 스타 환영합니다.</p>
      </div>
    </div>`;
MANUAL.ja = `
    <div>
      <div class="panel">
        <div class="ph">クイックスタート</div>
        <p>最初の設定を済ませたら、あとは取り込み → 索引 → 検索のループです。</p>
        <pre>research init                        # ワークスペース + 設定ガイド
research ingest "query"              # 論文を取り込む(既定: 全ソース)
research index                       # 全文索引を更新
research query "query" --evidence    # メタデータ + 本文を検索</pre>
        <p class="dim">どのステップも再実行して安全 — ingestは重複をスキップし、索引は差分だけ同期します。</p>
      </div>
      <div class="panel">
        <div class="ph">論文の取り込み</div>
        <ul>
          <li><b>ウェブソース</b> — <span class="mono">--source</span> には <span class="mono">arxiv</span>・<span class="mono">s2</span>・<span class="mono">openalex</span>・<span class="mono">europepmc</span>・<span class="mono">preprints</span>・<span class="mono">all</span> を指定できます。失敗したソースは警告だけ出して、残りは続行します。</li>
          <li><b>ローカルPDF</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>。本文はページ位置の目印付きで取り出されるので、検索結果がページを引用できます。</li>
          <li><b>arXiv PDFの自動取得</b> — 取り込み後、arXiv IDはあるのに本文がない論文は、データベースの隣の <span class="mono">pdf/</span> にPDFを保存して索引します。処理済みの論文はスキップ。</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> は動いているローカルZotero APIを読みます。</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span>(Zoteroの書き出しファイルも使えます)。</li>
          <li><b>再取り込み</b> — <span class="mono">research reingest --missing-pages</span> はページ印より前の本文を作り直し、ローカルPDFがなければarXivから取得します。</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">読書の進め方</div>
        <p><b>論文</b>テーブルで進捗を記録します。状態メニューで読書状態(未読〜中断)を、星で1〜5の評価を。CLIでも操作できます:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # 保存した本文を出力</pre>
      </div>
      <div class="panel">
        <div class="ph">トピック・ギャップ・レポート</div>
        <pre>research topics add "トピック名"
research topics add "サブ" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # LLMによる知識ギャップ分析
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps と report には設定ファイルに <span class="mono">[llm]</span> セクションが必要です。ない場合はプレースホルダーを返します。</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">ダッシュボードの使い方</div>
        <ul>
          <li><b>概要</b> — ライブラリの規模、読みの深度、パイプラインの漏斗、トピックのカバレッジ。</li>
          <li><b>論文</b> — ライブラリのテーブル。行をクリックすると詳細モーダルが開き、内蔵リーダーでページ印付きの本文や保存済みPDFを読めます。</li>
          <li><b>パイプライン</b> — 発見から精読まで、各論文がいまどこにいるか。</li>
          <li><b>履歴</b> — これまでの出来事を新しい順に並べたフィード。</li>
          <li><b>結果</b> — 知識ギャップと生成済みレポート。</li>
          <li><b>設定</b> — <span class="mono">[llm]</span>、ワークスペースのパス、ダッシュボードのバインド設定を編集。127.0.0.1 の外に開くにはトークンが必要です。</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">検索索引</div>
        <p>検索はタイトル・要約・メモ・タグ・キーワード・抽出本文を対象にしたFTS5全文検索です。索引は書き込みのたびに自動で維持されます。</p>
        <pre>research index --rebuild     # 全体を再構築
research enrich             # 検索キーワードを追加</pre>
        <p class="dim">キーワードは論文に<i>書かれていない</i>語 — 同義語、略語の展開形、別の言い回し — を補うものです。要約と違う語で検索してもヒットするのはこのためです。<span class="mono">research enrich</span> は <span class="mono">[llm]</span> があればLLMを使い、なければキーワードが要る論文の一覧を出します(エージェントが埋めます)。</p>
      </div>
      <div class="panel">
        <div class="ph">Zoteroへ書き戻し</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">既定はドライラン。タグはDOIで対応づけます。CLI専用です。</p>
      </div>
      <div class="panel">
        <div class="ph">エージェントモード(MCP)</div>
        <pre>research mcp</pre>
        <p>stdio MCPサーバーを立ち上げて、Claude CodeやCodexのようなホストから丸ごと操作できるようにします。ホストにこのバイナリとコマンドを向けてください。</p>
      </div>
      <div class="panel">
        <div class="ph">設定ファイル</div>
        <p><span class="mono">~/.research/config.toml</span> — セクションは2つ: <span class="mono">[llm]</span>(プロバイダー、モデル、キーの<i>環境変数名</i>)と <span class="mono">[dashboard]</span>(ホスト、ポート、トークン)。秘密の値がファイルに書かれることはありません。</p>
      </div>
      <div class="panel">
        <div class="ph">ソース</div>
        <p><a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> で公開中 — issueとスターを歓迎します。</p>
      </div>
    </div>`;
MANUAL["zh-Hans"] = `
    <div>
      <div class="panel">
        <div class="ph">快速上手</div>
        <p>一次性配置好,然后就是导入 → 索引 → 检索的循环。</p>
        <pre>research init                        # 工作区 + 配置引导
research ingest "query"              # 导入论文(默认:全部来源)
research index                       # 刷新全文索引
research query "query" --evidence    # 检索元数据 + 正文</pre>
        <p class="dim">每一步都可以放心重跑:导入会跳过重复项,索引只做增量同步。</p>
      </div>
      <div class="panel">
        <div class="ph">把论文导入进来</div>
        <ul>
          <li><b>在线来源</b> — <span class="mono">--source</span> 可选 <span class="mono">arxiv</span>、<span class="mono">s2</span>、<span class="mono">openalex</span>、<span class="mono">europepmc</span>、<span class="mono">preprints</span> 或 <span class="mono">all</span>。某个来源失败只会警告,其余照常继续。</li>
          <li><b>本地 PDF</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>。正文会连同页码标记一起提取,搜索结果可以精确引用到页。</li>
          <li><b>自动下载 arXiv PDF</b> — 导入之后,有 arXiv 编号但没有正文的论文会自动把 PDF 下载到数据库旁边的 <span class="mono">pdf/</span> 并建立索引。已处理过的会跳过。</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> 读取正在运行的本地 Zotero API。</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span>(Zotero 导出的文件也可以)。</li>
          <li><b>重新提取</b> — <span class="mono">research reingest --missing-pages</span> 会为缺少页码标记的旧正文重建,本地没有 PDF 时从 arXiv 下载。</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">阅读流程</div>
        <p>在<b>论文</b>表格里记录进度:状态下拉框设置阅读状态(未读到放弃),星星打 1–5 分。也可以用命令行:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # 输出已存正文</pre>
      </div>
      <div class="panel">
        <div class="ph">主题、缺口、报告</div>
        <pre>research topics add "主题名称"
research topics add "子主题" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # LLM 知识缺口分析
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps 和 report 需要配置文件里有 <span class="mono">[llm]</span> 段;没有的话只返回占位结果。</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">仪表盘导览</div>
        <ul>
          <li><b>概览</b> — 库的规模、阅读深度、流水线漏斗、主题覆盖率。</li>
          <li><b>论文</b> — 文献库表格。点一行打开详情弹窗;内置阅读器可以看带页码标记的正文或已存的 PDF。</li>
          <li><b>流水线</b> — 每篇论文从发现到精读走到了哪一步。</li>
          <li><b>历史</b> — 所有动态按时间倒序排成的信息流。</li>
          <li><b>结果</b> — 知识缺口和已生成的报告。</li>
          <li><b>设置</b> — 编辑 <span class="mono">[llm]</span>、工作区路径和仪表盘监听配置。绑定到 127.0.0.1 之外需要令牌。</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">搜索索引</div>
        <p>搜索基于 FTS5 全文索引,覆盖标题、摘要、笔记、标签、关键词和提取出的正文。每次写入都会自动维护索引。</p>
        <pre>research index --rebuild     # 强制全量重建
research enrich             # 补充搜索关键词</pre>
        <p class="dim">关键词补的是论文里<i>没有出现</i>的词 — 同义词、缩写全称、另一种说法 — 这样即使用词和摘要不同也能搜到。<span class="mono">research enrich</span> 在配置了 <span class="mono">[llm]</span> 时用 LLM;否则会列出需要补关键词的论文,交给代理去填。</p>
      </div>
      <div class="panel">
        <div class="ph">写回 Zotero</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">默认只做演练;标签按 DOI 匹配。仅限命令行。</p>
      </div>
      <div class="panel">
        <div class="ph">代理模式(MCP)</div>
        <pre>research mcp</pre>
        <p>启动一个 stdio MCP 服务器,让 Claude Code、Codex 这类宿主直接驱动整个工具。把宿主指到这个二进制和命令即可。</p>
      </div>
      <div class="panel">
        <div class="ph">配置文件</div>
        <p><span class="mono">~/.research/config.toml</span> — 两个段:<span class="mono">[llm]</span>(提供商、模型、密钥的<i>环境变量名</i>)和 <span class="mono">[dashboard]</span>(主机、端口、令牌)。密钥本体绝不会写进文件。</p>
      </div>
      <div class="panel">
        <div class="ph">源码</div>
        <p>开源在 <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — 欢迎提 issue 和点星。</p>
      </div>
    </div>`;
MANUAL["zh-Hant"] = `
    <div>
      <div class="panel">
        <div class="ph">快速上手</div>
        <p>一次性設定好,之後就是匯入 → 索引 → 搜尋的循環。</p>
        <pre>research init                        # 工作區 + 設定引導
research ingest "query"              # 匯入論文(預設:全部來源)
research index                       # 更新全文索引
research query "query" --evidence    # 搜尋詮釋資料 + 內文</pre>
        <p class="dim">每個步驟都能放心重跑:匯入會跳過重複項,索引只做增量同步。</p>
      </div>
      <div class="panel">
        <div class="ph">把論文收進來</div>
        <ul>
          <li><b>線上來源</b> — <span class="mono">--source</span> 可選 <span class="mono">arxiv</span>、<span class="mono">s2</span>、<span class="mono">openalex</span>、<span class="mono">europepmc</span>、<span class="mono">preprints</span> 或 <span class="mono">all</span>。某個來源失敗只會警告,其餘照常繼續。</li>
          <li><b>本機 PDF</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>。內文會連同頁碼標記一起擷取,搜尋結果可以精確引用到頁。</li>
          <li><b>自動下載 arXiv PDF</b> — 匯入之後,有 arXiv 編號但沒有內文的論文會自動把 PDF 下載到資料庫旁的 <span class="mono">pdf/</span> 並建立索引。處理過的會跳過。</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> 讀取執行中的本機 Zotero API。</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span>(Zotero 匯出的檔案也可以)。</li>
          <li><b>重新擷取</b> — <span class="mono">research reingest --missing-pages</span> 會為缺少頁碼標記的舊內文重建,本機沒有 PDF 時從 arXiv 下載。</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">閱讀流程</div>
        <p>在<b>論文</b>表格裡記錄進度:狀態下拉選單設定閱讀狀態(未讀到放棄),星星打 1–5 分。也可以用命令列:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # 輸出已存內文</pre>
      </div>
      <div class="panel">
        <div class="ph">主題、缺口、報告</div>
        <pre>research topics add "主題名稱"
research topics add "子主題" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # LLM 知識缺口分析
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps 和 report 需要設定檔裡有 <span class="mono">[llm]</span> 段;沒有的話只會回傳占位結果。</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">儀表板導覽</div>
        <ul>
          <li><b>總覽</b> — 書庫規模、閱讀深度、流程漏斗、主題覆蓋率。</li>
          <li><b>論文</b> — 書庫表格。點一行開啟詳情視窗;內建閱讀器可以看帶頁碼標記的內文或已存的 PDF。</li>
          <li><b>流程</b> — 每篇論文從發現到精讀走到了哪一步。</li>
          <li><b>歷史</b> — 所有動態按時間倒序排成的資訊流。</li>
          <li><b>結果</b> — 知識缺口和已產生的報告。</li>
          <li><b>設定</b> — 編輯 <span class="mono">[llm]</span>、工作區路徑和儀表板監聽設定。綁定到 127.0.0.1 之外需要權杖。</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">搜尋索引</div>
        <p>搜尋基於 FTS5 全文索引,涵蓋標題、摘要、筆記、標籤、關鍵字和擷取出的內文。每次寫入都會自動維護索引。</p>
        <pre>research index --rebuild     # 強制全量重建
research enrich             # 補充搜尋關鍵字</pre>
        <p class="dim">關鍵字補的是論文裡<i>沒有出現</i>的詞 — 同義詞、縮寫全稱、另一種說法 — 這樣即使用詞和摘要不同也搜得到。<span class="mono">research enrich</span> 在設定了 <span class="mono">[llm]</span> 時用 LLM;否則會列出需要補關鍵字的論文,交給代理去填。</p>
      </div>
      <div class="panel">
        <div class="ph">寫回 Zotero</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">預設只做演練;標籤按 DOI 對應。僅限命令列。</p>
      </div>
      <div class="panel">
        <div class="ph">代理模式(MCP)</div>
        <pre>research mcp</pre>
        <p>啟動一個 stdio MCP 伺服器,讓 Claude Code、Codex 這類宿主直接驅動整個工具。把宿主指向這個執行檔和命令即可。</p>
      </div>
      <div class="panel">
        <div class="ph">設定檔</div>
        <p><span class="mono">~/.research/config.toml</span> — 兩個段:<span class="mono">[llm]</span>(供應商、模型、金鑰的<i>環境變數名</i>)和 <span class="mono">[dashboard]</span>(主機、連接埠、權杖)。金鑰本體絕不會寫進檔案。</p>
      </div>
      <div class="panel">
        <div class="ph">原始碼</div>
        <p>開源在 <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — 歡迎提 issue 和點星。</p>
      </div>
    </div>`;
MANUAL.es = `
    <div>
      <div class="panel">
        <div class="ph">Para empezar</div>
        <p>Configúralo una vez y a vivir: el ciclo es importar → indexar → buscar.</p>
        <pre>research init                        # espacio de trabajo + guía de configuración
research ingest "query"              # trae artículos (por defecto: todas las fuentes)
research index                       # refresca el índice de texto completo
research query "query" --evidence    # busca en metadatos + cuerpo</pre>
        <p class="dim">Todos los pasos se pueden repetir sin miedo: la importación salta duplicados y el índice se sincroniza por incremental.</p>
      </div>
      <div class="panel">
        <div class="ph">Traer artículos</div>
        <ul>
          <li><b>Fuentes web</b> — <span class="mono">--source</span> acepta <span class="mono">arxiv</span>, <span class="mono">s2</span>, <span class="mono">openalex</span>, <span class="mono">europepmc</span>, <span class="mono">preprints</span> o <span class="mono">all</span>. Si una fuente falla avisa y el resto sigue.</li>
          <li><b>PDFs locales</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>. El cuerpo se extrae con marcas de página, así las búsquedas pueden citar una página concreta.</li>
          <li><b>PDFs de arXiv, automáticos</b> — tras importar, los artículos con ID de arXiv pero sin cuerpo descargan su PDF a <span class="mono">pdf/</span>, junto a la base, y se indexan. Los ya cubiertos se saltan.</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> lee una API local de Zotero que esté corriendo.</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span> (sirve el export de una biblioteca Zotero).</li>
          <li><b>Reextraer</b> — <span class="mono">research reingest --missing-pages</span> reconstruye cuerpos anteriores a las marcas de página, descargando de arXiv si no hay PDF local.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Cómo leer</div>
        <p>Registra tu avance en la tabla de <b>artículos</b>: el selector fija el estado de lectura (de sin leer a abandonado) y las estrellas ponen nota del 1 al 5. También por script:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # imprime el cuerpo guardado</pre>
      </div>
      <div class="panel">
        <div class="ph">Temas, lagunas, informes</div>
        <pre>research topics add "Nombre del tema"
research topics add "Sub" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # análisis de lagunas con LLM
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps y report necesitan una sección <span class="mono">[llm]</span> en la configuración; sin ella devuelven marcadores de posición.</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">Este panel</div>
        <ul>
          <li><b>Resumen</b> — tamaño de la biblioteca, profundidad de lectura, embudo del flujo y cobertura por tema.</li>
          <li><b>Artículos</b> — la tabla de la biblioteca; pulsa una fila para el detalle y usa el lector integrado para cuerpos con páginas o el PDF guardado.</li>
          <li><b>Flujo</b> — dónde está cada artículo, del descubrimiento a la lectura profunda.</li>
          <li><b>Historial</b> — todo lo que ha pasado, en orden inverso al tiempo.</li>
          <li><b>Resultados</b> — lagunas de conocimiento e informes generados.</li>
          <li><b>Ajustes</b> — edita <span class="mono">[llm]</span>, la ruta del espacio y el bind del panel. Publicar fuera de 127.0.0.1 exige token.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Índice de búsqueda</div>
        <p>La búsqueda es FTS5 de texto completo sobre título, resumen, notas, etiquetas, palabras clave y cuerpo extraído. El índice se mantiene solo con cada escritura.</p>
        <pre>research index --rebuild     # reconstrucción completa forzada
research enrich             # añade palabras clave</pre>
        <p class="dim">Las palabras clave cubren los términos que el artículo <i>no</i> contiene — sinónimos, siglas desarrolladas, otras formulaciones — así una búsqueda redactada distinto al resumen también lo encuentra. <span class="mono">research enrich</span> usa <span class="mono">[llm]</span> si está configurado; si no, lista los artículos pendientes para que un agente los rellene.</p>
      </div>
      <div class="panel">
        <div class="ph">Devolución a Zotero</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">Por defecto solo simula; las etiquetas se emparejan por DOI. Solo CLI, a propósito.</p>
      </div>
      <div class="panel">
        <div class="ph">Modo agente (MCP)</div>
        <pre>research mcp</pre>
        <p>Levanta un servidor MCP por stdio para que hosts como Claude Code o Codex manejen la herramienta entera. Apunta el host a este binario y comando.</p>
      </div>
      <div class="panel">
        <div class="ph">Archivo de configuración</div>
        <p><span class="mono">~/.research/config.toml</span> — dos secciones: <span class="mono">[llm]</span> (proveedor, modelo, el <i>nombre de la variable</i> de la clave) y <span class="mono">[dashboard]</span> (host, puerto, token). Los secretos jamás se escriben en el archivo.</p>
      </div>
      <div class="panel">
        <div class="ph">Código fuente</div>
        <p>Open source en <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — issues y estrellas bienvenidas.</p>
      </div>
    </div>`;
MANUAL.fr = `
    <div>
      <div class="panel">
        <div class="ph">Démarrage rapide</div>
        <p>Une configuration, puis une boucle : importer → indexer → chercher.</p>
        <pre>research init                        # espace de travail + guide de config
research ingest "query"              # importe des articles (défaut : toutes les sources)
research index                       # rafraîchit l'index plein texte
research query "query" --evidence    # cherche dans métadonnées + corps</pre>
        <p class="dim">Chaque étape peut être relancée sans risque : l'import saute les doublons, l'index se synchronise par incrément.</p>
      </div>
      <div class="panel">
        <div class="ph">Faire entrer les articles</div>
        <ul>
          <li><b>Sources web</b> — <span class="mono">--source</span> accepte <span class="mono">arxiv</span>, <span class="mono">s2</span>, <span class="mono">openalex</span>, <span class="mono">europepmc</span>, <span class="mono">preprints</span> ou <span class="mono">all</span>. Une source en échec prévient, les autres continuent.</li>
          <li><b>PDF locaux</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>. Le corps est extrait avec des repères de page : les résultats peuvent citer une page précise.</li>
          <li><b>PDF arXiv, automatiquement</b> — après l'import, les articles avec un ID arXiv mais sans corps voient leur PDF téléchargé dans <span class="mono">pdf/</span>, à côté de la base, puis indexé. Les déjà traités sont ignorés.</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> lit une API Zotero locale en cours d'exécution.</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span> (l'export d'une bibliothèque Zotero convient).</li>
          <li><b>Réextraire</b> — <span class="mono">research reingest --missing-pages</span> reconstruit les corps antérieurs aux repères de page, en téléchargeant depuis arXiv s'il n'y a pas de PDF local.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Flux de lecture</div>
        <p>Suivez votre avancement dans la table des <b>articles</b> : le sélecteur fixe le statut de lecture (de non lu à abandonné), les étoiles posent une note de 1 à 5. Scriptable aussi :</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # affiche le corps enregistré</pre>
      </div>
      <div class="panel">
        <div class="ph">Sujets, écarts, rapports</div>
        <pre>research topics add "Nom du sujet"
research topics add "Sous-sujet" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # analyse des écarts par LLM
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps et report exigent une section <span class="mono">[llm]</span> dans la configuration ; sans elle, ils renvoient des espaces réservés.</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">Ce tableau de bord</div>
        <ul>
          <li><b>Aperçu</b> — taille de la bibliothèque, profondeur de lecture, entonnoir du pipeline, couverture par sujet.</li>
          <li><b>Articles</b> — la table de la bibliothèque ; cliquez une ligne pour le détail, et le lecteur intégré pour les corps paginés ou le PDF enregistré.</li>
          <li><b>Pipeline</b> — où se trouve chaque article, de la découverte à la lecture approfondie.</li>
          <li><b>Historique</b> — tout ce qui s'est passé, du plus récent au plus ancien.</li>
          <li><b>Résultats</b> — écarts de connaissance et rapports générés.</li>
          <li><b>Réglages</b> — éditez <span class="mono">[llm]</span>, le chemin de l'espace et le bind du tableau de bord. Publier au-delà de 127.0.0.1 exige un jeton.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Index de recherche</div>
        <p>La recherche est du FTS5 plein texte sur titre, résumé, notes, tags, mots-clés et corps extrait. L'index se maintient automatiquement à chaque écriture.</p>
        <pre>research index --rebuild     # reconstruction complète forcée
research enrich             # ajoute des mots-clés</pre>
        <p class="dim">Les mots-clés couvrent les termes que l'article ne contient <i>pas</i> — synonymes, sigles développés, autres tournures — si bien qu'une requête formulée autrement que le résumé le trouve quand même. <span class="mono">research enrich</span> utilise <span class="mono">[llm]</span> s'il est configuré ; sinon il liste les articles à compléter pour qu'un agent le fasse.</p>
      </div>
      <div class="panel">
        <div class="ph">Retour vers Zotero</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">Simulation par défaut ; les tags sont appariés par DOI. CLI volontairement seul.</p>
      </div>
      <div class="panel">
        <div class="ph">Mode agent (MCP)</div>
        <pre>research mcp</pre>
        <p>Lance un serveur MCP par stdio pour que des hôtes comme Claude Code ou Codex pilotent l'outil entier. Pointez l'hôte vers ce binaire et cette commande.</p>
      </div>
      <div class="panel">
        <div class="ph">Fichier de configuration</div>
        <p><span class="mono">~/.research/config.toml</span> — deux sections : <span class="mono">[llm]</span> (fournisseur, modèle, le <i>nom de la variable</i> de la clé) et <span class="mono">[dashboard]</span> (hôte, port, jeton). Les secrets ne sont jamais écrits dans le fichier.</p>
      </div>
      <div class="panel">
        <div class="ph">Source</div>
        <p>Open source sur <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — issues et étoiles bienvenues.</p>
      </div>
    </div>`;
MANUAL.de = `
    <div>
      <div class="panel">
        <div class="ph">Schnellstart</div>
        <p>Einmal einrichten, dann ein Loop: importieren → indizieren → suchen.</p>
        <pre>research init                        # Arbeitsbereich + Konfigurationsguide
research ingest "query"              # Artikel holen (Standard: alle Quellen)
research index                       # Volltextindex auffrischen
research query "query" --evidence    # Metadaten + Volltext durchsuchen</pre>
        <p class="dim">Jeder Schritt darf wiederholt werden: Der Import überspringt Duplikate, der Index synchronisiert inkrementell.</p>
      </div>
      <div class="panel">
        <div class="ph">Artikel hereinholen</div>
        <ul>
          <li><b>Web-Quellen</b> — <span class="mono">--source</span> nimmt <span class="mono">arxiv</span>, <span class="mono">s2</span>, <span class="mono">openalex</span>, <span class="mono">europepmc</span>, <span class="mono">preprints</span> oder <span class="mono">all</span>. Eine Ausfallquelle warnt nur, der Rest läuft weiter.</li>
          <li><b>Lokale PDFs</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>. Der Text wird mit Seitenmarken extrahiert, Suchtreffer können eine Seite zitieren.</li>
          <li><b>arXiv-PDFs, automatisch</b> — nach dem Import bekommen Artikel mit arXiv-ID, aber ohne Text, ihr PDF nach <span class="mono">pdf/</span> neben der Datenbank geladen und indiziert. Abgedeckte werden übersprungen.</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> liest eine laufende lokale Zotero-API.</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span> (ein Zotero-Export funktioniert).</li>
          <li><b>Neu extrahieren</b> — <span class="mono">research reingest --missing-pages</span> baut Texte ohne Seitenmarken neu auf und lädt von arXiv, wenn kein lokales PDF existiert.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Lese-Workflow</div>
        <p>Fortschritt in der <b>Artikel</b>-Tabelle festhalten: Das Auswahlmenü setzt den Lesestatus (von ungelesen bis abgebrochen), die Sterne vergeben 1–5 Punkte. Auch per Skript:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # gespeicherten Text ausgeben</pre>
      </div>
      <div class="panel">
        <div class="ph">Themen, Lücken, Berichte</div>
        <pre>research topics add "Themenname"
research topics add "Unterthema" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # Lückenanalyse per LLM
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps und report brauchen eine <span class="mono">[llm]</span>-Sektion in der Konfiguration; ohne geben sie Platzhalter zurück.</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">Dieses Dashboard</div>
        <ul>
          <li><b>Übersicht</b> — Bibliotheksgröße, Lesetiefe, Ablauftrichter, Themenabdeckung.</li>
          <li><b>Artikel</b> — die Bibliothekstabelle; eine Zeile öffnet die Details, der eingebaute Leser zeigt paginierte Texte oder das gespeicherte PDF.</li>
          <li><b>Ablauf</b> — wo jeder Artikel steht, vom Fund bis zur vertieften Lektüre.</li>
          <li><b>Verlauf</b> — alles, was passiert ist, in umgekehrter Chronologie.</li>
          <li><b>Ergebnisse</b> — Wissenslücken und erzeugte Berichte.</li>
          <li><b>Einstellungen</b> — <span class="mono">[llm]</span>, Arbeitsbereich-Pfad und Dashboard-Bind bearbeiten. Jenseits von 127.0.0.1 braucht es einen Schlüssel.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Suchindex</div>
        <p>Die Suche ist FTS5-Volltext über Titel, Abstract, Notizen, Tags, Keywords und extrahierten Text. Der Index wird bei jedem Schreibzugriff automatisch gepflegt.</p>
        <pre>research index --rebuild     # vollständiger Neuaufbau
research enrich             # Such-Keywords ergänzen</pre>
        <p class="dim">Keywords decken die Wörter ab, die ein Artikel <i>nicht</i> enthält — Synonyme, ausgeschriebene Abkürzungen, andere Formulierungen — so findet eine anders formulierte Suche ihn trotzdem. <span class="mono">research enrich</span> nutzt <span class="mono">[llm]</span>, wenn eingerichtet; sonst listet es die Artikel auf, und ein Agent füllt die Keywords ein.</p>
      </div>
      <div class="panel">
        <div class="ph">Zurückschreiben nach Zotero</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">Standard ist der Probelauf; Tags werden per DOI zugeordnet. Bewusst nur CLI.</p>
      </div>
      <div class="panel">
        <div class="ph">Agentenmodus (MCP)</div>
        <pre>research mcp</pre>
        <p>Startet einen stdio-MCP-Server, damit Hosts wie Claude Code oder Codex das ganze Werkzeug steuern können. Zeig dem Host dieses Binary und diesen Befehl.</p>
      </div>
      <div class="panel">
        <div class="ph">Konfigurationsdatei</div>
        <p><span class="mono">~/.research/config.toml</span> — zwei Sektionen: <span class="mono">[llm]</span> (Anbieter, Modell, der <i>Umgebungsvariablenname</i> des Schlüssels) und <span class="mono">[dashboard]</span> (Host, Port, Schlüssel). Geheimnisse landen nie in der Datei.</p>
      </div>
      <div class="panel">
        <div class="ph">Quellcode</div>
        <p>Open source auf <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — Issues und Sterne willkommen.</p>
      </div>
    </div>`;
MANUAL.pt = `
    <div>
      <div class="panel">
        <div class="ph">Começo rápido</div>
        <p>Configure uma vez e entre no ritmo: importar → indexar → buscar.</p>
        <pre>research init                        # espaço de trabalho + guia de configuração
research ingest "query"              # traz artigos (padrão: todas as fontes)
research index                       # refresca o índice de texto completo
research query "query" --evidence    # busca em metadados + corpo</pre>
        <p class="dim">Todo passo pode ser repetido sem susto: a importação pula duplicados e o índice sincroniza por incremento.</p>
      </div>
      <div class="panel">
        <div class="ph">Trazer artigos</div>
        <ul>
          <li><b>Fontes web</b> — <span class="mono">--source</span> aceita <span class="mono">arxiv</span>, <span class="mono">s2</span>, <span class="mono">openalex</span>, <span class="mono">europepmc</span>, <span class="mono">preprints</span> ou <span class="mono">all</span>. Se uma fonte falha, avisa e o resto segue.</li>
          <li><b>PDFs locais</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>. O corpo é extraído com marcas de página, então a busca pode citar uma página exata.</li>
          <li><b>PDFs do arXiv, automático</b> — depois de importar, artigos com ID do arXiv mas sem corpo baixam o PDF para <span class="mono">pdf/</span>, ao lado do banco, e entram no índice. Os já cobertos são pulados.</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> lê uma API local do Zotero em execução.</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span> (o export de uma biblioteca Zotero serve).</li>
          <li><b>Reextrair</b> — <span class="mono">research reingest --missing-pages</span> reconstrói corpos anteriores às marcas de página, baixando do arXiv quando não há PDF local.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Fluxo de leitura</div>
        <p>Registre o avanço na tabela de <b>artigos</b>: o seletor fixa o estado de leitura (de não lido a abandonado) e as estrelas dão nota de 1 a 5. Também por script:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # imprime o corpo guardado</pre>
      </div>
      <div class="panel">
        <div class="ph">Tópicos, lacunas, relatórios</div>
        <pre>research topics add "Nome do tópico"
research topics add "Sub" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # análise de lacunas com LLM
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps e report pedem uma seção <span class="mono">[llm]</span> na configuração; sem ela devolvem marcadores de posição.</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">Este painel</div>
        <ul>
          <li><b>Visão geral</b> — tamanho do acervo, profundidade de leitura, funil do fluxo, cobertura por tópico.</li>
          <li><b>Artigos</b> — a tabela do acervo; clique numa linha para os detalhes e use o leitor embutido para corpos paginados ou o PDF guardado.</li>
          <li><b>Fluxo</b> — onde cada artigo está, da descoberta à leitura a fundo.</li>
          <li><b>Histórico</b> — tudo que aconteceu, do mais recente para trás.</li>
          <li><b>Resultados</b> — lacunas de conhecimento e relatórios gerados.</li>
          <li><b>Ajustes</b> — edite <span class="mono">[llm]</span>, o caminho do espaço e o bind do painel. Abrir além de 127.0.0.1 exige token.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Índice de busca</div>
        <p>A busca é FTS5 de texto completo sobre título, resumo, notas, tags, palavras-chave e corpo extraído. O índice se mantém sozinho a cada escrita.</p>
        <pre>research index --rebuild     # reconstrução completa forçada
research enrich             # adiciona palavras-chave</pre>
        <p class="dim">As palavras-chave cobrem os termos que o artigo <i>não</i> contém — sinônimos, siglas por extenso, outras formulações — assim uma busca redigida diferente do resumo também o encontra. <span class="mono">research enrich</span> usa <span class="mono">[llm]</span> quando configurado; sem ele, lista os artigos pendentes para um agente preencher.</p>
      </div>
      <div class="panel">
        <div class="ph">De volta ao Zotero</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">Por padrão só ensaia; as tags são casadas por DOI. Só CLI, de propósito.</p>
      </div>
      <div class="panel">
        <div class="ph">Modo agente (MCP)</div>
        <pre>research mcp</pre>
        <p>Sobe um servidor MCP por stdio para que hosts como Claude Code ou Codex manobrem a ferramenta inteira. Aponte o host para este binário e comando.</p>
      </div>
      <div class="panel">
        <div class="ph">Arquivo de configuração</div>
        <p><span class="mono">~/.research/config.toml</span> — duas seções: <span class="mono">[llm]</span> (provedor, modelo, o <i>nome da variável</i> da chave) e <span class="mono">[dashboard]</span> (host, porta, token). Segredos nunca vão para o arquivo.</p>
      </div>
      <div class="panel">
        <div class="ph">Código-fonte</div>
        <p>Open source em <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — issues e estrelas bem-vindas.</p>
      </div>
    </div>`;
MANUAL.ru = `
    <div>
      <div class="panel">
        <div class="ph">Быстрый старт</div>
        <p>Настроить один раз — и жить в цикле: импорт → индекс → поиск.</p>
        <pre>research init                        # рабочая область + обзор настроек
research ingest "query"              # подтянуть статьи (по умолчанию: все источники)
research index                       # обновить полнотекстовый индекс
research query "query" --evidence    # поиск по метаданным + тексту</pre>
        <p class="dim">Любой шаг можно смело повторять: импорт пропускает дубликаты, индекс досинхронизируется по инкременту.</p>
      </div>
      <div class="panel">
        <div class="ph">Как попадают статьи</div>
        <ul>
          <li><b>Веб-источники</b> — <span class="mono">--source</span> принимает <span class="mono">arxiv</span>, <span class="mono">s2</span>, <span class="mono">openalex</span>, <span class="mono">europepmc</span>, <span class="mono">preprints</span> или <span class="mono">all</span>. Упавший источник только предупредит — остальные продолжат.</li>
          <li><b>Локальные PDF</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>. Текст извлекается с привязкой к страницам, поэтому поиск может цитировать конкретную страницу.</li>
          <li><b>PDF с arXiv, автоматически</b> — после импорта статьям с arXiv-ID, но без текста, PDF скачивается в <span class="mono">pdf/</span> рядом с базой и индексируется. Уже покрытые пропускаются.</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> читает работающий локальный API Zotero.</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span> (подойдёт экспорт библиотеки Zotero).</li>
          <li><b>Переизвлечь</b> — <span class="mono">research reingest --missing-pages</span> заново собирает тексты без страничных меток, скачивая с arXiv, если локального PDF нет.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Как читать</div>
        <p>Отмечайте прогресс в таблице <b>статей</b>: селектор задаёт статус чтения (от «не прочитана» до «брошена»), звёзды — оценку от 1 до 5. Можно и из скрипта:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # вывести сохранённый текст</pre>
      </div>
      <div class="panel">
        <div class="ph">Темы, пробелы, отчёты</div>
        <pre>research topics add "Название темы"
research topics add "Подтема" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # анализ пробелов через LLM
research report --topic a,b --title "..."</pre>
        <p class="dim">Для gaps и report в конфиге нужна секция <span class="mono">[llm]</span>; без неё вернутся заглушки.</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">Эта панель</div>
        <ul>
          <li><b>Обзор</b> — размер библиотеки, глубина чтения, воронка конвейера, покрытие тем.</li>
          <li><b>Статьи</b> — таблица библиотеки; клик по строке открывает карточку, встроенный читалка показывает текст с привязкой к страницам или сохранённый PDF.</li>
          <li><b>Конвейер</b> — где находится каждая статья: от находки до глубокого изучения.</li>
          <li><b>История</b> — лента всего происшедшего, от свежего к старому.</li>
          <li><b>Результаты</b> — пробелы в знаниях и готовые отчёты.</li>
          <li><b>Настройки</b> — правка <span class="mono">[llm]</span>, пути рабочей области и привязки панели. Открытие наружу, кроме 127.0.0.1, требует токен.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Поисковый индекс</div>
        <p>Поиск — полнотекстовый FTS5 по названию, аннотации, заметкам, тегам, ключевым словам и извлечённому тексту. Индекс поддерживается сам при каждой записи.</p>
        <pre>research index --rebuild     # принудительная полная пересборка
research enrich             # добавить поисковые ключевые слова</pre>
        <p class="dim">Ключевые слова покрывают то, чего в статье <i>нет</i> — синонимы, развёрнутые аббревиатуры, другие формулировки — поэтому запрос, звучащий иначе, чем аннотация, её всё равно найдёт. <span class="mono">research enrich</span> при настроенном <span class="mono">[llm]</span> использует модель; без него печатает список статей, а ключевые слова добавляет агент.</p>
      </div>
      <div class="panel">
        <div class="ph">Обратная запись в Zotero</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">По умолчанию — пробный прогон; теги сопоставляются по DOI. Сознательно только CLI.</p>
      </div>
      <div class="panel">
        <div class="ph">Режим агента (MCP)</div>
        <pre>research mcp</pre>
        <p>Поднимает MCP-сервер поверх stdio, чтобы хосты вроде Claude Code или Codex управляли инструментом целиком. Направьте хост на этот бинарник и команду.</p>
      </div>
      <div class="panel">
        <div class="ph">Файл конфигурации</div>
        <p><span class="mono">~/.research/config.toml</span> — две секции: <span class="mono">[llm]</span> (провайдер, модель, <i>имя переменной</i> с ключом) и <span class="mono">[dashboard]</span> (хост, порт, токен). Секреты в файл никогда не пишутся.</p>
      </div>
      <div class="panel">
        <div class="ph">Исходники</div>
        <p>Open source на <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — welcome: issues и звёзды.</p>
      </div>
    </div>`;
MANUAL.it = `
    <div>
      <div class="panel">
        <div class="ph">Per cominciare</div>
        <p>Si configura una volta, poi si gira in tondo: importa → indicizza → cerca.</p>
        <pre>research init                        # area di lavoro + guida alla configurazione
research ingest "query"              # porta articoli (predefinito: tutte le fonti)
research index                       # rinfresca l'indice a testo pieno
research query "query" --evidence    # cerca tra metadati + corpo</pre>
        <p class="dim">Ogni passo si può rifare senza timore: l'import salta i duplicati e l'indice si sincronizza a incrementi.</p>
      </div>
      <div class="panel">
        <div class="ph">Far entrare gli articoli</div>
        <ul>
          <li><b>Fonti web</b> — <span class="mono">--source</span> accetta <span class="mono">arxiv</span>, <span class="mono">s2</span>, <span class="mono">openalex</span>, <span class="mono">europepmc</span>, <span class="mono">preprints</span> o <span class="mono">all</span>. Se una fonte fallsce avvisa e le altre continuano.</li>
          <li><b>PDF locali</b> — <span class="mono">research ingest --source pdf --path file-or-dir</span>. Il corpo viene estratto con i riferimenti di pagina, così i risultati possono citare una pagina precisa.</li>
          <li><b>PDF da arXiv, in automatico</b> — dopo l'import, gli articoli con ID arXiv ma senza corpo si scaricano il PDF in <span class="mono">pdf/</span>, accanto al database, e finiscono nell'indice. Quelli già coperti vengono saltati.</li>
          <li><b>Zotero</b> — <span class="mono">--source zotero</span> legge un'API Zotero locale in esecuzione.</li>
          <li><b>BibTeX / CSL-JSON</b> — <span class="mono">research import file.bib</span> (va bene l'export di una libreria Zotero).</li>
          <li><b>Riestrarre</b> — <span class="mono">research reingest --missing-pages</span> ricostruisce i corpi privi di riferimenti di pagina, scaricando da arXiv se manca un PDF locale.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Come si legge</div>
        <p>Registra i progressi nella tabella degli <b>articoli</b>: il menu a tendina fissa lo stato di lettura (da "da leggere" ad "abbandonato"), le stelle assegnano un voto da 1 a 5. Si può fare anche da script:</p>
        <pre>research read &lt;id&gt; --status completed --rating 4
research read &lt;id&gt; --body             # stampa il corpo salvato</pre>
      </div>
      <div class="panel">
        <div class="ph">Argomenti, lacune, rapporti</div>
        <pre>research topics add "Nome argomento"
research topics add "Sotto" --parent &lt;topic-id&gt;
research ingest "query" --topic &lt;topic-id&gt;
research gaps --topic &lt;topic-id&gt;      # analisi delle lacune con LLM
research report --topic a,b --title "..."</pre>
        <p class="dim">gaps e report richiedono una sezione <span class="mono">[llm]</span> nella configurazione; senza restituiscono segnaposto.</p>
      </div>
    </div>
    <div>
      <div class="panel">
        <div class="ph">Questo pannello</div>
        <ul>
          <li><b>Panoramica</b> — dimensione della libreria, profondità di lettura, imbuto del flusso, copertura per argomento.</li>
          <li><b>Articoli</b> — la tabella della libreria; clicca una riga per i dettagli e usa il lettore integrato per i corpi con pagine o il PDF salvato.</li>
          <li><b>Flusso</b> — dove si trova ogni articolo, dalla scoperta alla lettura approfondita.</li>
          <li><b>Cronologia</b> — tutto ciò che è successo, dal più recente all'indietro.</li>
          <li><b>Risultati</b> — lacune di conoscenza e rapporti generati.</li>
          <li><b>Impostazioni</b> — modifica <span class="mono">[llm]</span>, il percorso dell'area e il bind del pannello. Esporsi oltre 127.0.0.1 richiede un token.</li>
        </ul>
      </div>
      <div class="panel">
        <div class="ph">Indice di ricerca</div>
        <p>La ricerca è FTS5 a testo pieno su titolo, abstract, note, tag, parole chiave e corpo estratto. L'indice si mantiene da solo a ogni scrittura.</p>
        <pre>research index --rebuild     # ricostruzione completa forzata
research enrich             # aggiunge parole chiave</pre>
        <p class="dim">Le parole chiave coprono i termini che l'articolo <i>non</i> contiene — sinonimi, acronimi per esteso, altre formulazioni — così una ricerca formulata diversamente dall'abstract lo trova lo stesso. <span class="mono">research enrich</span> usa <span class="mono">[llm]</span> se configurato; altrimenti elenca gli articoli a cui mancano parole chiave, e le aggiunge un agente.</p>
      </div>
      <div class="panel">
        <div class="ph">Riscrittura su Zotero</div>
        <pre>research export --to zotero --apply</pre>
        <p class="dim">Per impostazione predefinita fa solo una prova a vuoto; i tag si abbinano via DOI. Solo CLI, di proposito.</p>
      </div>
      <div class="panel">
        <div class="ph">Modalità agente (MCP)</div>
        <pre>research mcp</pre>
        <p>Avvia un server MCP via stdio perché host come Claude Code o Codex governino l'intero strumento. Punta l'host a questo binario e comando.</p>
      </div>
      <div class="panel">
        <div class="ph">File di configurazione</div>
        <p><span class="mono">~/.research/config.toml</span> — due sezioni: <span class="mono">[llm]</span> (provider, modello, il <i>nome della variabile</i> della chiave) e <span class="mono">[dashboard]</span> (host, porta, token). I segreti non finiscono mai nel file.</p>
      </div>
      <div class="panel">
        <div class="ph">Sorgenti</div>
        <p>Open source su <a href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener">github.com/epicsagas/research-agent</a> — issue e stelle benvenute.</p>
      </div>
    </div>`;

const pages = {

  overview: async () => {
    const chrome = await loadChrome();
    if (!chrome) throw new Error(t("srv_unreachable"));
    const { ov, cf } = chrome;
    drawNav({ papers: ov.papers, gaps: ov.gaps });
    const [pipe, res] = await Promise.all([api("/api/pipeline"), api("/api/results")]);

    if (ov.papers === 0 && ov.topics === 0) { $("main").innerHTML = onboarding(cf); return; }


    const readFrac = ov.papers ? ov.read / ov.papers : 0;
    const ps = pipe.paper_status;
    const deepRead = (ps.read || 0) + (ps.deep_read || 0);
    const avgCov = res.topics.length
      ? res.topics.reduce((a, t) => a + (t.state ? t.state.coverage_score : 0), 0) / res.topics.filter(t=>t.state).length || 0
      : 0;
    const highGaps = res.gaps.filter(g => (g.priority || 0) >= 0.7).length;
    const latestRep = res.reports.slice().sort((a,b) => (b.generated_at||"").localeCompare(a.generated_at||""))[0];

    const html = `
      <div class="page-head"><h1>${esc(t("nav_overview"))}</h1>
        <span class="page-sub">${esc(fmt("ov_sub", { p: nfmt(ov.papers), t: nfmt(ov.topics) }))}</span></div>
      <div class="tiles">
        <div class="tile"><div class="n">${nfmt(ov.papers)}</div><div class="l">${esc(t("t_papers"))}</div>
          <div class="x"><div class="depth-strip">${depthStrip(ps)}</div>
          <div class="cap">${esc(fmt("cap_depth", { n: deepRead }))}</div></div></div>
        <div class="tile"><div class="n">${nfmt(ov.read)}</div><div class="l">${esc(t("t_completed"))}</div>
          <div class="x"><div class="microbar"><i style="width:${pct(readFrac)}%;background:var(--c-green)"></i></div>
          <div class="cap">${esc(fmt("cap_completed", { p: pct(readFrac), n: nfmt(ov.queued) }))}</div></div></div>
        <div class="tile"><div class="n">${nfmt(ov.gaps)}</div><div class="l">${esc(t("t_gaps"))}</div>
          <div class="x"><div class="microbar"><i style="width:${ov.gaps?pct(highGaps/ov.gaps):0}%;background:var(--c-amber)"></i></div>
          <div class="cap">${esc(fmt("cap_gaps", { n: highGaps }))}</div></div></div>
        <div class="tile"><div class="n">${nfmt(ov.reports)}</div><div class="l">${esc(t("t_reports"))}</div>
          <div class="x"><div class="cap" style="margin-top:0">${latestRep ? esc(latestRep.title) + ", " + esc(day(latestRep.generated_at)) : esc(t("none_yet"))}</div></div></div>
        <div class="tile"><div class="n">${res.topics.length ? pct(avgCov) + "%" : "—"}</div><div class="l">${esc(t("t_avgcov"))}</div>
          <div class="x"><div class="microbar"><i style="width:${pct(avgCov)}%;background:var(--d4)"></i></div>
          <div class="cap">${esc(fmt("cap_topics", { n: res.topics.length }))}</div></div></div>
      </div>

      <div class="cols" style="margin-top:30px">
        <div>
          <div class="section-h">${esc(t("sec_depth"))}</div>
          <div id="f-depth"></div>
          <div class="section-h">${esc(t("sec_reading"))}</div>
          <div id="f-reading"></div>
        </div>
        <div>
          <div class="section-h">${esc(t("sec_coverage"))}</div>
          <div id="ov-topics">${topicRows(res.topics, true)}</div>
        </div>
      </div>`;
    $("main").innerHTML = html;
    funnel("f-depth", DEPTHS, ps, DEPTH_COLOR, DL[LANG]);
    funnel("f-reading", READS.filter(s => s !== "abandoned"), pipe.reading_status,
      [cssVar("--ramp-0"), cssVar("--ramp-1"), cssVar("--ramp-2"), cssVar("--ok")], RL[LANG]);
    if (pipe.reading_status.abandoned) {
      $("f-reading").innerHTML += '<div class="conv">' + esc(fmt("conv_aband", { n: nfmt(pipe.reading_status.abandoned) })) + "</div>";
    }
  },

  papers: async (arg) => {
    drawNav({});
    if (!state.papersLoaded) {
      $("main").innerHTML = '<div class="page-head"><h1>Papers</h1></div>' +
        '<div class="toolbar"></div><div class="skel w80"></div><div class="skel w60"></div><div class="skel w80"></div>';
    }
    const [data] = await Promise.all([
      api("/api/papers" + (state.paperTopic === "all" ? "" : "?topic=" + encodeURIComponent(state.paperTopic))),
      // Topic names for the filter dropdown, fetched once per session. If it
      // fails, drop any active scope — the dropdown could not show it.
      state.topics.length ? Promise.resolve()
        : api("/api/results").then(r => { state.topics = r.topics; }).catch(() => { state.paperTopic = "all"; }),
    ]);
    state.papers = data.papers;
    state.papersLoaded = true;
    if (arg) { openDrawer(arg); }
    drawPapersPage();
  },

  pipeline: async () => {
    drawNav({});
    $("main").innerHTML = '<div class="page-head"><h1>Pipeline</h1></div><div class="skel w60"></div>';
    const pipe = await api("/api/pipeline");
    const aband = pipe.reading_status.abandoned || 0;
    $("main").innerHTML = `
      <div class="page-head"><h1>${esc(t("nav_pipeline"))}</h1>
        <span class="page-sub">${esc(t("pipe_sub"))}</span></div>
      <div class="cols">
        <div>
          <div class="section-h">${esc(t("sec_depth"))}</div>
          <div id="f-depth"></div>
          <div class="section-h">${esc(t("sec_reading"))}</div>
          <div id="f-reading"></div>
          <div class="conv">${esc(fmt("conv_aband", { n: nfmt(aband) }))}</div>
        </div>
        <div>
          <div class="section-h">${esc(t("sec_stage"))}</div>
          ${stageTable(t("stage_depth"), DEPTHS, pipe.paper_status, DL[LANG])}
          ${stageTable(t("stage_reading"), READS, pipe.reading_status, RL[LANG])}
        </div>
      </div>`;
    funnel("f-depth", DEPTHS, pipe.paper_status, DEPTH_COLOR, DL[LANG]);
    funnel("f-reading", READS.filter(s => s !== "abandoned"), pipe.reading_status,
      [cssVar("--ramp-0"), cssVar("--ramp-1"), cssVar("--ramp-2"), cssVar("--ok")], RL[LANG]);
  },

  history: async () => {
    drawNav({});
    $("main").innerHTML = '<div class="page-head"><h1>History</h1></div><div class="skel w60"></div>';
    const h = await api("/api/history");
    const streams = [
      ["papers", h.papers_added],
      ["gaps", h.gaps_found],
      ["reports", h.reports_generated],
    ];
    const evs = [];
    streams.forEach(([key, items]) => {
      items.forEach(it => evs.push({ date: it.date, key, it }));
    });
    evs.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
    const shown = evs.filter(e => state.historyFilter === "all" || e.key === state.historyFilter);
    const byDay = new Map();
    shown.forEach(e => {
      const d = day(e.date) || "unknown";
      if (!byDay.has(d)) byDay.set(d, []);
      byDay.get(d).push(e);
    });
    const countFor = key => evs.filter(e => e.key === key).length;
    $("main").innerHTML = `
      <div class="page-head"><h1>${esc(t("nav_history"))}</h1>
        <span class="page-sub">${esc(fmt("hist_sub", { n: nfmt(evs.length) }))}</span></div>
      <div class="toolbar"><div class="chips">
        <button class="chip ${state.historyFilter==="all"?"on":""}" data-hf="all">${esc(t("all"))} <span class="cn">${evs.length}</span></button>
        ${["papers","gaps","reports"].map(k =>
          '<button class="chip '+(state.historyFilter===k?"on":"")+'" data-hf="'+k+'">'+
          esc(SL[LANG][k])+' <span class="cn">'+countFor(k)+'</span></button>').join("")}
      </div></div>
      ${shown.length ? [...byDay.entries()].map(([d, list]) =>
        '<div class="day"><span class="d">' + esc(d) + '</span><span class="n">' + list.length + "</span></div>" +
        list.map(e => eventRow(e)).join("")
      ).join("") : '<div class="empty-block"><h3>' + esc(t("no_activity_h")) + "</h3><p>" + esc(t("no_activity_p")) + "</p></div>"}`;
    $("main").querySelectorAll("[data-hf]").forEach(b =>
      b.addEventListener("click", () => { state.historyFilter = b.dataset.hf; render(); }));
    $("main").querySelectorAll("[data-paper]").forEach(a =>
      a.addEventListener("click", ev => { ev.preventDefault(); openDrawer(a.dataset.paper); }));
    $("main").querySelectorAll("[data-report]").forEach(a =>
      a.addEventListener("click", ev => { ev.preventDefault(); openReportById(a.dataset.report); }));
  },

  manual: async () => {
    drawNav({});
    loadChrome();
    $("main").innerHTML = `
      <div class="page-head"><h1>${esc(t("nav_manual"))}</h1>
        <span class="page-sub">${esc(t("manual_sub"))}</span></div>
      <div class="cols manual" style="margin-top:24px">${MANUAL[LANG] || MANUAL.en}</div>`;
  },

  results: async () => {
    drawNav({});
    $("main").innerHTML = '<div class="page-head"><h1>Results</h1></div><div class="skel w60"></div>';
    const r = await api("/api/results");
    state.topics = r.topics;
    resultsCache = {
      topics: r.topics,
      gaps: r.gaps.slice().sort((a,b) => (b.priority||0)-(a.priority||0)),
      reps: r.reports.slice().sort((a,b) => (b.generated_at||"").localeCompare(a.generated_at||"")),
    };
    drawResults();
  },


  config: async () => {
    drawNav({});
    loadChrome();
    const cf = await api("/api/config");
    const llm = cf.llm;
    const dash = cf.dashboard;
    const keyStatus = llm
      ? '<span class="dot ' + (llm.api_key_set ? "ok" : "bad") + '" style="display:inline-block;margin-right:7px"></span>' +
        esc(llm.api_key_set ? t("key_ok") : t("key_missing"))
      : '<span class="dot dim2" style="display:inline-block;margin-right:7px"></span>' + esc(t("key_unset"));
    $("main").innerHTML = `
      <div class="page-head"><h1>${esc(t("nav_config"))}</h1>
        <span class="page-sub">${t("cfg_sub")}</span></div>
      ${llm && !llm.api_key_set ? '<div class="alert">' + ICON.warn + "<div>" + fmt("cfg_alert", { env: "<b>" + esc(llm.api_key_env) + "</b>" }) + "</div></div>" : ""}
      <div class="panel">
        <div class="ph"><span style="margin-right:6px">${esc(t("p_llm"))}</span>${keyStatus}</div>
        <p class="dim" style="margin:2px 0 14px;font-size:13px">${esc(t("cfg_llm_desc"))}</p>
        <div class="form">
          <label>${esc(t("cfg_provider"))}<input id="cfg-provider" placeholder="anthropic" value="${llm ? esc(llm.provider) : ""}"></label>
          <label>${esc(t("cfg_model"))}<input id="cfg-model" list="cfg-model-list" placeholder="claude-sonnet-4-6" value="${llm ? esc(llm.model) : ""}">
            <datalist id="cfg-model-list"></datalist></label>
          <label>${esc(t("cfg_env"))}<input id="cfg-env" class="mono" placeholder="ANTHROPIC_API_KEY" value="${llm ? esc(llm.api_key_env) : ""}"></label>
        </div>
        <div class="form" style="grid-template-columns:1fr auto;align-items:end;margin-top:12px">
          <label>${esc(t("cfg_base"))} <span class="faint">${esc(t("cfg_base_hint"))}</span>
            <input id="cfg-base" class="mono" placeholder="https://api.openai.com/v1" value="${llm && llm.base_url ? esc(llm.base_url) : ""}"></label>
          <button class="btn ghost" id="cfg-load-models">${esc(t("cfg_load_models"))}</button>
        </div>
        <div style="display:flex;gap:8px;margin-top:14px;align-items:center">
          <button class="btn" id="cfg-save">${esc(t("cfg_save"))}</button>
          ${llm ? '<button class="btn ghost" id="cfg-remove">' + esc(t("cfg_remove")) + "</button>" : ""}
          <span class="faint" style="font-size:12px" id="cfg-hint"></span>
        </div>
      </div>

      <div class="panel">
        <div class="ph">${esc(t("p_workspace"))}</div>
        <p class="dim" style="margin:2px 0 14px;font-size:13px">${esc(t("ws_desc"))}</p>
        <div class="form" style="grid-template-columns:1fr">
          <label>${esc(t("ws_db"))}<input id="ws-path" class="mono" placeholder="~/.research/research.db" value="${esc(cf.configured_database_path)}"></label>
        </div>
      </div>

      <div class="panel">
        <div class="ph">${esc(t("p_server"))}</div>
        <p class="dim" style="margin:2px 0 14px;font-size:13px">${fmt("srv_desc", { host: esc(location.host) })}</p>
        <div class="form">
          <label>${esc(t("srv_bind"))}<select id="srv-host">
            <option value="127.0.0.1" ${dash.host === "127.0.0.1" ? "selected" : ""}>${esc(t("srv_local"))}</option>
            <option value="0.0.0.0" ${dash.host === "0.0.0.0" ? "selected" : ""}>${esc(t("srv_net"))}</option>
          </select></label>
          <label>${esc(t("srv_port"))}<input id="srv-port" type="number" min="1" max="65535" value="${dash.port}"></label>
          <label>${esc(t("srv_token"))}${dash.token_set ? "" : ' <span class="faint">' + esc(t("srv_token_gen")) + "</span>"}
            ${dash.token_set
              ? '<div style="display:flex;gap:6px"><input id="srv-token" class="mono" readonly value="' + esc(dash.token) + '"><button class="btn ghost" id="srv-copy-token" title="' + esc(t("srv_token")) + '">' + ICON.copy + '</button></div>'
              : '<input id="srv-token" class="mono" placeholder="' + esc(t("srv_token_none")) + '" readonly>'}
          </label>
        </div>
        <div style="display:flex;gap:8px;margin-top:14px;align-items:center">
          <button class="btn" id="srv-save">${esc(t("srv_save"))}</button>
          <button class="btn" id="srv-restart">${t("srv_restart")}</button>
          <span class="faint" style="font-size:12px" id="srv-hint"></span>
        </div>
      </div>`;
    $("main").querySelectorAll("[data-copy]").forEach(b =>
      b.addEventListener("click", () => copyText(b.dataset.copy, t("ws_db"))));

    const readLlm = () => {
      const provider = $("cfg-provider").value.trim();
      const model = $("cfg-model").value.trim();
      const env = $("cfg-env").value.trim();
      const base = $("cfg-base").value.trim();
      if (!provider || !model || !env) { toast(t("toast_required"), true); return null; }
      return { provider, model, api_key_env: env, base_url: base || null };
    };
    const readDashboard = () => ({
      host: $("srv-host").value,
      port: +$("srv-port").value,
      token: $("srv-token").value.trim() || null,
    });
    const saveLlm = async (llmValue, okMsg) => {
      try {
        await api("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ llm: llmValue }) });
        toast(okMsg);
        loadChrome();
        return true;
      } catch (e) { toast(e.message, true); return false; }
    };
    const saveServer = async () => {
      const body = { workspace: { database_path: $("ws-path").value.trim() }, dashboard: readDashboard() };
      if (!body.workspace.database_path) { toast(t("toast_db_required"), true); return false; }
      try {
        await api("/api/config", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
        return true;
      } catch (e) { toast(e.message, true); return false; }
    };
    $("cfg-save").addEventListener("click", () => {
      const llmValue = readLlm();
      if (llmValue) saveLlm(llmValue, t("toast_llm_saved"));
    });
    const rmBtn = $("cfg-remove");
    if (rmBtn) rmBtn.addEventListener("click", () => saveLlm(null, t("toast_llm_removed")));

    $("cfg-load-models").addEventListener("click", async () => {
      const hint = $("cfg-hint");
      hint.textContent = t("loading_models");
      try {
        const d = await api("/api/models");
        const list = $("cfg-model-list");
        list.innerHTML = d.models.map(m => "<option value=\"" + esc(m) + "\"></option>").join("");
        hint.textContent = fmt("models_loaded", { n: d.models.length });
      } catch (e) { hint.textContent = ""; toast(e.message, true); }
    });

    const copyBtn = $("srv-copy-token");
    if (copyBtn) copyBtn.addEventListener("click", () => copyText($("srv-token").value, t("srv_token")));

    $("srv-restart").addEventListener("click", async () => {
      if (!(await saveServer())) return;
      $("srv-hint").textContent = t("restarting");
      try { await api("/api/restart", { method: "POST" }); } catch {}
      // Poll until the fresh instance answers, then reload the app.
      for (let i = 0; i < 40; i++) {
        await new Promise(r => setTimeout(r, 500));
        try { await fetch("/api/overview"); location.reload(); return; } catch {}
      }
      $("srv-hint").textContent = "";
      toast(t("restart_fail"), true);
    });
  },
};

/* ───────────────────────── page pieces ───────────────────────── */
function depthStrip(ps) {
  const total = DEPTHS.reduce((a, s) => a + (ps[s] || 0), 0) || 1;
  return DEPTHS.map((s, i) => {
    const w = 100 * (ps[s] || 0) / total;
    return "<i style=\"flex:0 0 " + Math.max(w, ps[s] ? 2 : 0) + "%;background:" + DEPTH_COLOR[i] + '"></i>';
  }).join("");
}

function stageTable(title, stages, counts, labels) {
  const total = stages.reduce((a, s) => a + (counts[s] || 0), 0);
  return '<table style="margin-bottom:20px"><thead><tr><th colspan="2">' + title + " — " + total + "</th></tr></thead><tbody>" +
    stages.map(s => "<tr><td class='dim' style='padding:6px 10px'>" + esc(labels[s]) +
      "</td><td style='text-align:right;padding:6px 10px'>" + nfmt(counts[s] || 0) + "</td></tr>").join("") +
    "</tbody></table>";
}

function topicRows(topics, compact) {
  if (!topics.length) return "";
  return topics.map(tp => {
    const st = tp.state;
    const cov = st ? st.coverage_score : 0;
    return '<div class="topic-row"><span class="indent" style="--depth:' + (tp.depth || 0) + '"></span>' +
      '<div class="ringwrap">' + ring(null, 18, cov) + '<span class="pct">' + pct(cov) + "%</span></div>" +
      '<div><div class="nm">' + esc(tp.name) + "</div>" +
      (compact ? "" : '<div class="pt-sub">' + esc(st ? fmt("topic_read", { n: nfmt(st.papers_read), m: nfmt(st.papers_queued) }) : t("topic_none")) + "</div>") + "</div>" +
      '<div class="meta">' + (st ? esc(fmt("topic_gaps", { n: nfmt(st.gaps_identified) })) : "") + "</div></div>";
  }).join("");
}

function eventRow(e) {
  const c = STREAM_COLOR[e.key];
  let main, trail = "";
  if (e.key === "papers") {
    // Open the detail modal over the current page rather than routing to
    // #/papers/<id>: jumping to another page to read one abstract loses the
    // reader's place in the feed.
    main = '<a href="#" data-paper="' + esc(e.it.id) + '">' + mdInline(e.it.title) + "</a>";
    trail = e.it.source || "";
  } else if (e.key === "gaps") {
    main = mdInline(e.it.description);
    trail = e.it.topic_id ? esc(fmt("ev_topic", { id: e.it.topic_id.slice(0, 8) })) : "";
  } else {
    // Same reasoning as papers: open the report over the feed rather than
    // navigating to Results and making the reader find it again.
    main = '<a href="#" data-report="' + esc(e.it.id) + '">' + mdInline(e.it.title) + "</a>";
  }
  return '<div class="ev"><span class="tick">' + clock(e.date) + '</span>' +
    '<span class="mark" style="background:' + c + '"></span><span>' + main + "</span>" +
    (trail ? '<span class="trail">' + trail + "</span>" : "") + "</div>";
}

function nothing(msg) {
  return '<div class="faint" style="padding:18px 2px;font-size:13px">' + esc(msg) + "</div>";
}

function onboarding(cf) {
  return `
    <div class="page-head"><h1>${esc(t("onb_h"))}</h1><span class="page-sub">${esc(t("onb_sub"))}</span></div>
    <div class="empty-block">
      <h3>${esc(t("onb_h2"))}</h3>
      <p>${esc(t("onb_p"))}</p>
      <div class="steps">
        <div class="step"><span class="no">01</span><div><div>${esc(t("onb_s1"))}</div>
          <div class="cmd"><b>research topics add "your topic"</b></div></div></div>
        <div class="step"><span class="no">02</span><div><div>${esc(t("onb_s2"))}</div>
          <div class="cmd"><b>research ingest "search words" --topic &lt;topic-id&gt;</b></div></div></div>
        <div class="step"><span class="no">03</span><div><div>${esc(t("onb_s3"))}</div>
          <div class="cmd"><b>research gaps --topic &lt;topic-id&gt;</b> &nbsp;·&nbsp; <b>research report --topic &lt;topic-id&gt;</b></div></div></div>
      </div>
      <p class="faint" style="margin:18px 0 0;font-size:12px">${esc(t("onb_foot"))}${cf && cf.llm ? "." : esc(t("onb_foot_llm")) + "."}</p>
    </div>`;
}

/* ───────────────────────── papers page ───────────────────────── */
function drawPapersPage() {
  $("main").innerHTML = `
    <div class="page-head"><h1>${esc(t("nav_papers"))}</h1><span class="page-sub" id="p-count"></span></div>
    <div class="toolbar">
      <div class="search">${ICON.search}<input id="q" placeholder="${esc(t("search_ph"))}" value="${esc(state.query)}"><kbd>/</kbd></div>
      <div class="chips" id="f-chips">
        <button class="chip" data-f="all">${esc(t("all"))}</button>
        ${READS.map(s => '<button class="chip" data-f="' + s + '">' + esc(RL[LANG][s]) + "</button>").join("")}
      </div>
      <select id="ptopic" title="${esc(t("scope_topic"))}">
        <option value="all">${esc(t("all_topics"))}</option>
        ${state.topics.map(tp => '<option value="' + esc(tp.id) + '"' + (state.paperTopic === tp.id ? " selected" : "") + ">" + esc(tp.name) + "</option>").join("")}
      </select>
      <select id="sort" style="margin-left:auto">
        <option value="recent">${esc(t("sort_recent"))}</option>
        <option value="relevance">${esc(t("sort_relevance"))}</option>
        <option value="rating">${esc(t("sort_rating"))}</option>
        <option value="title">${esc(t("sort_title"))}</option>
      </select>
    </div>
    <div id="p-table"></div>`;
  const q = $("q");
  q.addEventListener("input", () => { state.query = q.value; drawPaperTable(); });
  q.addEventListener("keydown", e => { if (e.key === "Escape") { q.value = ""; state.query = ""; drawPaperTable(); } });
  const ptopic = $("ptopic");
  ptopic.value = state.paperTopic;
  ptopic.addEventListener("change", async () => {
    state.paperTopic = ptopic.value;
    $("p-table").innerHTML = '<div class="skel w80"></div><div class="skel w60"></div>';
    const data = await api("/api/papers" + (state.paperTopic === "all" ? "" : "?topic=" + encodeURIComponent(state.paperTopic)));
    state.papers = data.papers;
    drawPaperTable();
  });
  $("f-chips").querySelectorAll(".chip").forEach(b => {
    if (b.dataset.f === state.paperFilter) b.classList.add("on");
    b.addEventListener("click", () => {
      state.paperFilter = b.dataset.f;
      $("f-chips").querySelectorAll(".chip").forEach(x => x.classList.toggle("on", x.dataset.f === state.paperFilter));
      drawPaperTable();
    });
  });
  $("sort").value = state.paperSort;
  $("sort").addEventListener("change", () => { state.paperSort = $("sort").value; drawPaperTable(); });
  drawPaperTable();
}

function drawPaperTable() {
  const q = state.query.trim().toLowerCase();
  let list = state.papers.filter(p =>
    (state.paperFilter === "all" || p.reading_status === state.paperFilter) &&
    (!q || (p.title + " " + p.authors.join(" ") + " " + p.tags.join(" ") + " " + p.abstract_text).toLowerCase().includes(q)));
  const by = {
    recent: (a,b) => (b.created_at||"").localeCompare(a.created_at||""),
    relevance: (a,b) => (b.relevance_score||0) - (a.relevance_score||0),
    rating: (a,b) => (b.rating||0) - (a.rating||0),
    title: (a,b) => a.title.localeCompare(b.title),
  }[state.paperSort];
  list = list.slice().sort(by);
  $("p-count").textContent = fmt("shown_count", { n: nfmt(list.length), m: nfmt(state.papers.length) });

  $("p-table").innerHTML = list.length ? `
    <table><thead><tr>
      <th style="width:66px">${esc(t("col_depth"))}</th><th>${esc(t("col_paper"))}</th><th style="width:150px">${esc(t("col_reading"))}</th>
      <th style="width:112px">${esc(t("col_rating"))}</th><th style="width:80px">${esc(t("col_relevance"))}</th></tr></thead>
      <tbody>${list.map(p => paperRow(p)).join("")}</tbody></table>`
    : '<div class="empty-block"><h3>' + esc(t("no_match_h")) + "</h3><p>" + esc(t("no_match_p")) + "</p></div>";

  $("p-table").querySelectorAll("tr.rowlink").forEach(tr =>
    tr.addEventListener("click", e => {
      if (e.target.closest("select") || e.target.closest(".stars") || e.target.closest(".rowlink-ext")) return;
      location.hash = "#/papers/" + tr.dataset.id;
    }));
  bindRowControls($("p-table"));
}

function paperRow(p) {
  const u = safeUrl(p.url);
  const snippet = p.abstract_text
    ? '<div class="pt-abs">' + mdInline(p.abstract_text.slice(0, 160)) + (p.abstract_text.length > 160 ? "…" : "") + "</div>"
    : "";
  const link = u
    ? ' <a class="rowlink-ext" href="' + u + '" target="_blank" rel="noreferrer" title="' + esc(t("open_paper")) + '" aria-label="' + esc(t("open_paper")) + '">' + ICON.ext + "</a>"
    : "";
  return '<tr class="rowlink" data-id="' + esc(p.id) + '">' +
    "<td>" + dmark(p.status) + "</td>" +
    '<td><div class="pt">' + mdInline(p.title) + link + "</div>" +
      '<div class="pt-sub">' + esc(p.authors.slice(0, 2).join(", ")) + (p.authors.length > 2 ? " " + esc(t("et_al")) : "") +
      (p.year ? ", " + p.year : "") + "</div>" + snippet + "</td>" +
    "<td>" + statusSelect(p) + "</td>" +
    "<td>" + stars(p) + "</td>" +
    '<td><div class="rbar"><i style="width:' + pct(p.relevance_score) + '%"></i></div></td></tr>';
}

/* rating clicks + status changes, shared by table and drawer */
function bindRowControls(root) {
  root.querySelectorAll(".stars").forEach(el => {
    el.querySelectorAll("button").forEach(b => b.addEventListener("click", async () => {
      const id = el.dataset.id, v = +b.dataset.star;
      const paper = state.papers.find(p => p.id === id);
      const next = (paper && paper.rating === v) ? null : v; // click same star clears
      const ok = await patchPaper(id, { rating: next }, state.papers);
      if (ok) {
        el.querySelectorAll("button").forEach(x => x.classList.toggle("on", +x.dataset.star <= (next || 0)));
        toast(next ? fmt("rated", { n: next }) : t("rating_cleared"));
      }
    }));
  });
  root.querySelectorAll(".stepper").forEach(el => el.addEventListener("change", async () => {
    const ok = await patchPaper(el.dataset.id, { reading_status: el.value }, state.papers);
    if (ok) toast(fmt("moved", { x: RL[LANG][el.value] }));
  }));
}

/* ───────────────────────── drawer ───────────────────────── */
async function openDrawer(id) {
  // Callable from any page (History included), so the paper list may not be
  // loaded yet — fetch it once on demand.
  if (!state.papersLoaded) {
    try {
      const data = await api("/api/papers");
      state.papers = data.papers;
      state.papersLoaded = true;
    } catch { return; }
  }
  const p = state.papers.find(x => x.id === id);
  if (!p) return;
  const links = [];
  const u = safeUrl(p.url);
  if (u) links.push('<a href="' + u + '" target="_blank" rel="noreferrer">' + esc(t("publisher")) + " " + ICON.ext + "</a>");
  if (p.doi) links.push('<span class="mono">DOI ' + esc(p.doi) + "</span>");
  if (p.arxiv_id) links.push('<span class="mono">arXiv ' + esc(p.arxiv_id) + "</span>");
  if (p.pdf_path) links.push('<span class="mono">' + esc(p.pdf_path.split("/").pop()) + "</span>");
  $("drawer").innerHTML =
    '<button class="close" id="drawer-x" aria-label="' + esc(t("close")) + '">' + ICON.x + "</button>" +
    '<div style="margin-bottom:10px">' + dmark(p.status) + "</div>" +
    "<h2>" + mdInline(p.title) + "</h2>" +
    '<div class="dim" style="font-size:13px">' + esc(p.authors.join(", ")) +
      (p.year ? " · " + p.year : "") + (p.venue ? " · " + esc(p.venue) : "") + "</div>" +
    (links.length ? '<div class="sec" style="margin-top:12px;padding-top:10px;display:flex;gap:14px;flex-wrap:wrap;font-size:12.5px">' + links.join("") + "</div>" : "") +
    (p.abstract_text ? '<div class="abstract' + mdCls() + '">' + mdBlock(p.abstract_text) + "</div>" : "") +
    (p.pdf_path ? '<div class="sec"><button class="btn" id="drawer-read">' + esc(t("read_paper")) + "</button></div>" : "") +
    '<div class="sec"><div class="h">' + esc(t("reading_progress")) + "</div>" +
      '<div style="display:flex;gap:18px;align-items:center">' + statusSelect(p) + stars(p) + "</div></div>" +
    (p.tags.length ? '<div class="sec"><div class="h">' + esc(t("tags")) + "</div>" + p.tags.map(tg => '<span class="tag">' + esc(tg) + "</span>").join("") + "</div>" : "") +
    (p.notes ? '<div class="sec"><div class="h">' + esc(t("notes")) + '</div><div class="abstract' + mdCls() + '" style="margin:0">' + mdBlock(p.notes) + "</div></div>" : "") +
    '<div class="sec"><table class="kv">' +
      "<tr><td>" + esc(t("added")) + "</td><td>" + esc(day(p.created_at)) + "</td></tr>" +
      "<tr><td>" + esc(t("updated")) + "</td><td>" + esc(day(p.updated_at)) + "</td></tr>" +
      '<tr><td>ID</td><td class="mono">' + esc(p.id) + "</td></tr></table></div>";
  enhance($("drawer"));
  $("drawer").classList.add("open");
  $("overlay").classList.add("open");
  // The node is reused between papers, so it retains the last scroll offset
  // and would open mid-abstract with the title and close button out of view.
  // preventScroll matters: a plain focus() scrolls the button into view and
  // undoes the reset.
  $("drawer").scrollTop = 0;
  $("drawer-x").focus({ preventScroll: true });
  bindRowControls($("drawer"));
  $("drawer-read")?.addEventListener("click", () => openPaperReader(p.id));
  $("drawer-x").addEventListener("click", closeDrawer);
}

function closeDrawer() {
  $("drawer").classList.remove("open");
  $("overlay").classList.remove("open");
  if (route().page === "papers" && route().arg) {
    history.replaceState(null, "", "#/papers");
  }
}
$("overlay").addEventListener("click", () => {
  if ($("reader").classList.contains("open")) closeReader();
  else closeDrawer();
});

/* ───────────────────────── report reader ───────────────────────── */
/* Stored bodies carry `<!-- page N -->` ingest anchors; escape everything,
   then re-open safe spans where the markers were, so page breaks render as
   separators instead of literal comment text. */
function bodyHtml(body) {
  return esc(body).replace(/&lt;!-- page (\d+) --&gt;/g,
    '<span class="page-anchor">p. $1</span>');
}
/* Markdown path: split on the page markers first (sanitizing would drop the
   comments), render each chunk, join with the ruled anchors. */
function paperBodyHtml(body) {
  if (!mdActive()) return bodyHtml(body);
  return body.split(/<!-- page (\d+) -->/).map((chunk, i) =>
    i % 2 ? '<span class="page-anchor">p. ' + esc(chunk) + "</span>" : mdBlock(chunk)
  ).join("");
}

async function openPaperReader(id) {
  const p = state.papers.find(x => x.id === id);
  if (!p) return;
  const head =
    '<button class="close" id="reader-x" aria-label="' + esc(t("close")) + '">' + ICON.x + "</button>" +
    '<div class="inner">' +
    "<h2>" + esc(p.title) + "</h2>" +
    '<div class="meta">' + esc(p.authors.slice(0, 3).join(", ")) + (p.year ? " · " + p.year : "") + "</div>";
  // Same card, same dimmed backdrop as the paper detail modal — replace it
  // rather than stack on top of it.
  $("drawer").classList.remove("open");
  $("reader").classList.add("open");
  $("overlay").classList.add("open");
  $("reader").scrollTop = 0;
  if (p.pdf_path) {
    // Native browser PDF viewer — same-origin, so the iframe is allowed.
    $("reader").innerHTML = head +
      '<iframe class="pdf" src="/api/papers/' + encodeURIComponent(id) + '/pdf"></iframe></div>';
  } else {
    $("reader").innerHTML = head +
      '<div class="body' + mdCls() + '" id="paper-body"><span class="dim">' + esc(t("reader_loading")) + "</span></div></div>";
    try {
      const data = await api("/api/papers/" + encodeURIComponent(id) + "/body");
      $("paper-body").innerHTML = paperBodyHtml(data.body);
    } catch (e) {
      $("paper-body").innerHTML = '<span class="dim">' + esc(e.message) + "</span>";
    }
  }
  enhance($("reader"));
  $("reader-x").addEventListener("click", closeReader);
}
/// Open a report by id. History events carry only id/title/date, so the full
/// record (with its sections) has to come from /api/results.
async function openReportById(id) {
  let rep = resultsCache?.reps?.find(r => r.id === id);
  if (!rep) {
    try {
      const r = await api("/api/results");
      rep = (r.reports || []).find(x => x.id === id);
    } catch { return; }
  }
  if (rep) openReader(rep);
}

function openReader(rep) {
  $("reader").innerHTML =
    '<button class="close" id="reader-x" aria-label="' + esc(t("close")) + '">' + ICON.x + "</button>" +
    '<div class="inner">' +
    "<h2>" + esc(rep.title) + "</h2>" +
    '<div class="meta">' + esc(day(rep.generated_at)) + " · " + esc(fmt("report_sections", { n: rep.sections.length })) +
      (rep.output_path ? " · " + esc(rep.output_path) : "") + "</div>" +
    (rep.sections.length
      ? rep.sections.map(s => "<h3>" + esc(s.heading) + '</h3><div class="body' + mdCls() + '">' + mdBlock(s.content) + "</div>").join("")
      : '<div class="body dim">' + esc(t("report_no_sections")) + "</div>") +
    "</div>";
  $("drawer").classList.remove("open");
  $("reader").classList.add("open");
  $("overlay").classList.add("open");
  $("reader").scrollTop = 0;
  enhance($("reader"));
  $("reader-x").addEventListener("click", closeReader);
}

function closeReader() {
  $("reader").classList.remove("open");
  $("overlay").classList.remove("open");
}

/* ───────────────────────── keyboard ───────────────────────── */
document.addEventListener("keydown", e => {
  if (e.key === "Escape") {
    if ($("reader").classList.contains("open")) { closeReader(); return; }
    if ($("drawer").classList.contains("open")) { closeDrawer(); return; }
  }
  if (e.key === "/" && !e.target.closest("input, select, textarea")) {
    if (route().page === "papers") { e.preventDefault(); $("q")?.focus(); }
  }
});

/* ───────────────────────── boot ───────────────────────── */
async function render() {
  const { page, arg } = route();
  closeDrawerSilent();
  closeReader();
  try {
    await pages[page](arg);
  } catch (e) {
    $("main").innerHTML = '<div class="alert">' + ICON.warn +
      "<div><b>" + esc(t("err_api")) + "</b> " + esc(e.message) + "</div></div>";
  }
}
function closeDrawerSilent() {
  $("drawer").classList.remove("open");
  $("overlay").classList.remove("open");
}
/* ── chrome wiring: actions cluster + toggles + mobile nav ── */
/* One set of action buttons (language, theme, GitHub). Desktop parks them in
   the sidebar just above the status block; on mobile they return to the
   topbar. Moving the same nodes keeps their listeners intact. */
$("slot-topbar").innerHTML = `
  <div class="actions" id="actions-cluster">
    <div class="lang" id="lang-wrap">
      <button class="iconbtn wide" id="lang-btn" aria-haspopup="listbox" aria-expanded="false" title="Language">
        <span id="lang-cur"></span>
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" style="width:9px;height:9px"><path d="m3.5 6 4.5 4.5L12.5 6"/></svg>
      </button>
      <div class="lang-menu" id="lang-menu" role="listbox" aria-label="Language"></div>
    </div>
    <button class="iconbtn" id="theme-toggle" aria-label="Theme"></button>
    <a class="iconbtn" href="https://github.com/epicsagas/research-agent" target="_blank" rel="noopener" aria-label="GitHub">
      <svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
    </a>
  </div>`;
const placeActions = mq => {
  (mq.matches ? $("slot-side") : $("slot-topbar")).appendChild($("actions-cluster"));
};
const desktopMQ = window.matchMedia("(min-width: 901px)");
placeActions(desktopMQ);
desktopMQ.addEventListener?.("change", placeActions);

applyTheme();
applyLang();
buildLangMenu();

$("theme-toggle").addEventListener("click", () => {
  THEME = THEME === "light" ? "dark" : "light";
  localStorage.setItem("ra-theme", THEME);
  applyTheme();
  render();  // charts read their colours from CSS vars at draw time
});

/* ── language dropdown ── */
function buildLangMenu() {
  $("lang-menu").innerHTML = LANGS.map(([code, name]) =>
    '<button role="option" data-lang="' + code + '" aria-selected="' + (code === LANG) + '">' +
    esc(name) + '<span class="lg-code">' + code + "</span></button>"
  ).join("");
  $("lang-menu").querySelectorAll("button").forEach(b =>
    b.addEventListener("click", () => {
      setLang(b.dataset.lang);
      toggleLangMenu(false);
    }));
}
function toggleLangMenu(open) {
  const menu = $("lang-menu"), btn = $("lang-btn");
  const show = open ?? !menu.classList.contains("open");
  menu.classList.toggle("open", show);
  btn.setAttribute("aria-expanded", String(show));
}
function setLang(code) {
  if (!LANGS.some(([c]) => c === code) || code === LANG) return;
  LANG = code;
  localStorage.setItem("ra-lang", LANG);
  applyLang();
  buildLangMenu();
  render();  // all copy is rendered per-language at draw time
}
$("lang-btn").addEventListener("click", e => { e.stopPropagation(); toggleLangMenu(); });
document.addEventListener("click", e => {
  if (!e.target.closest("#lang-wrap")) toggleLangMenu(false);
});

/* Follow the system only while the user has not chosen for themselves. */
window.matchMedia?.("(prefers-color-scheme: light)").addEventListener?.("change", e => {
  if (localStorage.getItem("ra-theme")) return;
  THEME = e.matches ? "light" : "dark";
  applyTheme();
  render();
});

const setNav = open => {
  document.body.classList.toggle("nav-open", open);
  $("burger").setAttribute("aria-expanded", String(open));
};
$("burger").addEventListener("click", () => setNav(!document.body.classList.contains("nav-open")));
$("scrim").addEventListener("click", () => setNav(false));
// A tap on a nav link should close the drawer, not leave it covering the page.
$("nav").addEventListener("click", e => { if (e.target.closest("a")) setNav(false); });

render();
loadChrome();
/* Deferred CDN libs land after first paint — redraw once they're in so
   markdown/math/mermaid apply, unless a modal is already open. */
window.addEventListener("load", () => {
  if (!$("drawer").classList.contains("open") && !$("reader").classList.contains("open")) render();
});
</script>
</body>
</html>