forked from mxx1111/Homelab
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.css
More file actions
674 lines (632 loc) · 34.7 KB
/
Copy pathapp.css
File metadata and controls
674 lines (632 loc) · 34.7 KB
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
/* 配色取自 Claude 官网:象牙底 + 陶土橙,状态色统一往暖里调,
避免纯绿纯红在米色背景上跳出来 */
:root{
--bg:#F0EEE6; --card:#FFFFFF; --card-alt:#F7F5EF;
--border:#E3E0D8; --border-strong:#D5D1C6;
--text:#141413; --dim:#73716C; --faint:#A3A099;
--accent:#D97757; --accent-hover:#C56647; --accent-soft:rgba(217,119,87,.10);
--ok:#7A9A65; --warn:#C1852A; --crit:#BC4C3C;
--ok-soft:rgba(122,154,101,.13); --warn-soft:rgba(193,133,42,.13);
--crit-soft:rgba(188,76,60,.12);
/* 多线图的系列色。第一条沿用主色,其余冷暖交替排开——
同色系挨着放,两条线在米色底上会看混 */
--s1:#5F8AA0; --s2:#7A9A65; --s3:#C1852A; --s4:#9B7BA8;
--radius:12px; --radius-sm:8px;
--serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,ui-serif,serif;
--sans:-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang SC",
"Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",sans-serif;
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
}
@media (prefers-color-scheme:dark){
:root{
--bg:#1F1E1D; --card:#2A2927; --card-alt:#232220;
--border:#3A3936; --border-strong:#4A4844;
--text:#F0EEE6; --dim:#A8A59E; --faint:#7A7770;
--accent:#E08A6B; --accent-hover:#EC9A7C; --accent-soft:rgba(224,138,107,.13);
--ok:#8FAE79; --warn:#D19B44; --crit:#D46A57;
--s1:#7FAAC2; --s2:#8FAE79; --s3:#D19B44; --s4:#B79BC4;
}
}
*{box-sizing:border-box; margin:0; padding:0}
body{
background:var(--bg); color:var(--text); min-height:100vh;
font:15px/1.6 var(--sans); -webkit-font-smoothing:antialiased;
padding:0 20px 64px;
}
/* 宽屏不封死宽度,靠列数自增来吃掉横向空间 */
.wrap{max-width:2200px; margin:0 auto}
/* ---------- 顶栏 ---------- */
header{
display:flex; align-items:center; gap:20px; flex-wrap:wrap;
/* 负 margin 抵消 body 的左右 padding,让背景铺满视口宽度——
否则 sticky 之后两侧会露出底下滚动的卡片 */
margin:0 -20px 22px; padding:26px 20px 18px;
border-bottom:1px solid var(--border);
position:sticky; top:0; z-index:50;
background:var(--bg);
transition:padding .18s, box-shadow .18s;
}
/* 滚下去之后收紧一点并加投影,腾出屏幕高度同时把层次分开 */
header.scrolled{
padding:12px 20px 11px;
box-shadow:0 4px 16px rgba(0,0,0,.06);
}
@media (prefers-color-scheme:dark){
header.scrolled{box-shadow:0 4px 16px rgba(0,0,0,.35)}
}
header.scrolled .brand h1{font-size:20px}
.brand{display:flex; align-items:baseline; gap:11px}
.brand h1{font:500 25px/1 var(--serif); letter-spacing:-.2px}
.brand .sub{font-size:12.5px; color:var(--faint)}
nav{display:flex; gap:3px; background:var(--card-alt); padding:3px;
border-radius:10px; border:1px solid var(--border); flex-wrap:wrap}
nav button{
font:500 13.5px/1 var(--sans); color:var(--dim); background:none; border:none;
padding:8px 15px; border-radius:7px; cursor:pointer; transition:.16s;
display:flex; align-items:center; gap:6px;
}
nav button:hover{color:var(--text)}
nav button.on{background:var(--card); color:var(--text);
box-shadow:0 1px 2px rgba(0,0,0,.05)}
@media (prefers-color-scheme:dark){ nav button.on{box-shadow:none} }
nav .badge{font-size:10px; font-weight:600; padding:1px 5px; border-radius:9px;
background:var(--crit); color:#fff; line-height:1.5}
nav .badge.warn{background:var(--warn)}
/* 节点切换器。放在状态指示前面,靠 margin-left:auto 顶到右侧 */
.nodepick{
margin-left:auto; font-size:12.5px; padding:7px 30px 7px 11px;
background-color:var(--card-alt); border-color:var(--border-strong);
max-width:190px;
}
/* 切到节点视图时给顶栏一条色带。多机场景下最容易犯的错是看着 B 机器的
数据以为是 A 的,一条持续可见的提示比任何文案都管用 */
body.nodeview header{box-shadow:inset 0 3px 0 var(--accent)}
body.nodeview .nodepick{border-color:var(--accent); color:var(--accent); font-weight:500}
/* 切换器隐藏时(单机部署)由状态栏接管靠右 */
.nodepick.hide ~ .status{margin-left:auto}
.status{display:flex; align-items:center; gap:8px;
font-size:12.5px; color:var(--faint); font-variant-numeric:tabular-nums}
.pulse{width:6px; height:6px; border-radius:50%; background:var(--ok);
animation:breathe 2.4s ease-in-out infinite}
@keyframes breathe{0%,100%{opacity:1}50%{opacity:.25}}
/* ---------- 卡片 ---------- */
.grid{display:grid; gap:16px; grid-template-columns:repeat(auto-fill,minmax(340px,1fr))}
/* 总览第一屏独立放安全态势;下面的设备与资源卡仍走瀑布流。卡片内容天生
长短不一,等高网格会把短卡片拉高、内部留出大片空白。columns 让
每张卡贴合自身内容,列与列之间自动找平。
只有总览用它——其他页有需要跨整行的卡片,columns 里做不到 */
.overview-cards{columns:366px auto; column-gap:16px}
.overview-cards .card{
break-inside:avoid; -webkit-column-break-inside:avoid;
margin-bottom:16px; display:block; min-height:0;
}
/* 瀑布流下卡片高度自由,列表就不必再截断 */
.overview-cards .list{max-height:none; min-height:0}
.overview-cards .card.span2{grid-column:auto}
.overview-security{
position:relative; overflow:hidden; margin-bottom:16px; padding:22px 24px;
background:linear-gradient(125deg,var(--card) 0%,var(--card) 58%,var(--accent-soft) 100%);
border:1px solid var(--border); border-radius:var(--radius);
}
.overview-security::after{
content:""; position:absolute; right:-90px; top:-130px; width:280px; height:280px;
border:1px solid rgba(217,119,87,.13); border-radius:50%; pointer-events:none;
}
.overview-security-head{position:relative; z-index:1; display:flex; align-items:center;
justify-content:space-between; gap:20px; margin-bottom:20px}
.overview-security-head .eyebrow{display:flex; align-items:center; gap:8px; color:var(--dim);
font-size:11px; font-weight:600; letter-spacing:1.2px}
.overview-security-head h2{font:500 25px/1.22 var(--serif); margin:5px 0 4px}
.overview-security-head h2.warn{color:var(--warn)}
.overview-security-head h2.crit{color:var(--crit)}
.overview-security-head p{font-size:12px; color:var(--faint)}
.overview-security-head .btn{position:relative; z-index:2}
.overview-security-body{position:relative; z-index:1; display:grid;
grid-template-columns:minmax(420px,.9fr) minmax(480px,1.25fr);
gap:24px; align-items:stretch}
.security-kpis{display:grid; grid-template-columns:repeat(4,minmax(88px,1fr)); gap:8px}
.security-kpi{padding:13px 14px; border:1px solid var(--border); border-radius:var(--radius-sm);
background:var(--card-alt)}
.security-kpi b{display:block; font:500 25px/1 var(--serif); font-variant-numeric:tabular-nums}
.security-kpi b i{font:400 12px/1 var(--sans); color:var(--faint); font-style:normal}
.security-kpi b.warn{color:var(--warn)} .security-kpi b.crit{color:var(--crit)}
.security-kpi span{display:block; margin-top:7px; color:var(--dim); font-size:11.5px}
.overview-list-title{margin:16px 0 4px; color:var(--faint); font-size:11px}
.overview-attack-row{display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:12px;
align-items:center; min-height:32px; border-bottom:1px solid var(--border);
font-size:12px}
.overview-attack-row:last-child{border-bottom:0}
.overview-attack-row>span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.overview-attack-row>span i{display:inline-grid; place-items:center; width:18px; height:18px;
margin-right:7px; border-radius:50%; background:var(--card-alt);
color:var(--faint); font-size:10px; font-style:normal}
.overview-attack-row em{color:var(--faint); font-style:normal}
.overview-attack-row b{font-weight:500; font-variant-numeric:tabular-nums}
.overview-calm{display:grid; place-items:center; min-height:116px; margin-top:12px;
border:1px dashed var(--border); border-radius:var(--radius-sm);
color:var(--faint); font-size:12px}
.overview-map-wrap{min-width:0}
.overview-map{height:224px; overflow:hidden; border:1px solid var(--border);
border-radius:var(--radius-sm); background:#DDE7EC}
.overview-map-caption{margin-top:6px; color:var(--faint); font-size:10.5px; text-align:right}
.overview-map .leaflet-tooltip.overview-map-tip{font:11px/1.4 var(--sans); white-space:nowrap;
padding:5px 7px; border:1px solid #CBC7BC; border-radius:6px;
box-shadow:0 5px 14px rgba(34,32,27,.16)}
@media(max-width:1120px){
.overview-security-body{grid-template-columns:1fr}
.overview-map{height:240px}
}
@media(max-width:640px){
.overview-security{padding:17px 16px}
.overview-security-head{align-items:flex-start; margin-bottom:15px}
.overview-security-head h2{font-size:21px}
.overview-security-head .btn{font-size:11px; padding:8px 10px}
.security-kpis{grid-template-columns:repeat(2,1fr)}
.security-kpi{padding:11px 12px}
.security-kpi b{font-size:22px}
.overview-attack-row{grid-template-columns:minmax(0,1fr) auto; gap:8px}
.overview-attack-row em{display:none}
.overview-map{height:190px}
.overview-map-caption{text-align:left}
}
.card{
background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
padding:18px 20px; display:flex; flex-direction:column; min-height:152px;
transition:border-color .18s;
}
.card:hover{border-color:var(--border-strong)}
.card.span2{grid-column:span 2}
.card.full{grid-column:1/-1}
.card.flat{min-height:0}
/* 内容天生短的卡片别被同行的长列表拉高——贴合自身高度,
底下留背景色比留一大块白色卡片好看 */
.card.top{align-self:start}
/* dense 让后面的小卡片回填前面留下的空隙,宽屏下不会右半边全空 */
.grid.dense{grid-auto-flow:row dense}
/* auto-fit 而不是 auto-fill:轨道数超过卡片数时把空轨道折叠掉,
四张卡就等分整行,不会在右边留出空列 */
.grid.cols4{grid-template-columns:repeat(auto-fit,minmax(290px,1fr))}
.grid.cols2{grid-template-columns:repeat(auto-fit,minmax(440px,1fr))}
/* 连接/端口页只有两张摘要卡。通用 auto-fill 网格在 2K 屏会生成五列,
两张卡只占前三列,造成右侧近一半空白。这里明确成“窄摘要 + 自适应主卡”,
下方明细仍跨整行,宽屏和表格的视觉边界就一致了。 */
.summary-grid{grid-template-columns:minmax(280px,330px) minmax(0,1fr)}
.summary-grid>*{min-width:0}
.summary-grid>.card.span2{grid-column:auto}
.summary-grid>.card.full{grid-column:1/-1}
@media(max-width:760px){
.summary-grid{grid-template-columns:1fr}
.summary-grid>.card.full{grid-column:auto}
}
/* ---------- 区块分隔 ---------- */
.section{display:flex; align-items:center; gap:14px; margin:26px 2px 12px}
.section:first-of-type{margin-top:4px}
.section h3{font:600 12.5px/1 var(--sans); color:var(--dim); letter-spacing:.06em}
.section .rule{flex:1; height:1px; background:var(--border)}
.section .meta{font-size:11.5px; color:var(--faint); font-variant-numeric:tabular-nums}
@media (max-width:760px){
.card.span2{grid-column:span 1}
body{padding:0 14px 48px}
header{gap:14px; margin:0 -14px 16px; padding:16px 14px 12px}
header.scrolled{padding:10px 14px 9px}
/* 窄屏切换器和状态同占一行,别各占一行把顶栏撑高 */
.nodepick{margin-left:0; flex:1 1 130px; max-width:none}
.status{flex:2 1 auto; margin-left:0; width:auto}
.nodepick.hide ~ .status{width:100%; margin-left:0}
nav{width:100%}
/* 八个页签固定排成四列两行。flex 的默认 min-width:auto 会按文字宽度
把后几个按钮挤出导航框,新增“安全中心”后在 390px 手机上已经可复现。 */
nav button{flex:1 1 calc(25% - 3px); min-width:0; justify-content:center;
padding:8px 6px}
}
.card h2{
font:600 11px/1 var(--sans); color:var(--dim); letter-spacing:1.3px;
text-transform:uppercase; margin-bottom:16px;
display:flex; align-items:center; gap:8px;
}
.card h2 .right{margin-left:auto; text-transform:none; letter-spacing:0;
font-weight:400; color:var(--faint); font-size:11.5px}
.dot{width:7px; height:7px; border-radius:50%; flex:none; background:var(--faint)}
.dot.ok{background:var(--ok)} .dot.warn{background:var(--warn)}
.dot.crit{background:var(--crit)} .dot.info{background:var(--accent)}
.big{font:500 33px/1.05 var(--serif); font-variant-numeric:tabular-nums;
letter-spacing:-.6px}
.big.sm{font-size:26px}
.unit{font:400 13px/1 var(--sans); color:var(--dim); margin-left:5px}
.sub{color:var(--dim); font-size:12.5px; margin-top:6px}
.row{display:flex; justify-content:space-between; align-items:center; gap:10px;
padding:7px 0; border-bottom:1px solid var(--border); font-size:13.5px}
.row:last-child{border-bottom:none}
.row .k{color:var(--dim); display:flex; align-items:center; gap:7px; min-width:0}
.row .k > span:not(.dot):not(.tag){overflow:hidden; text-overflow:ellipsis;
white-space:nowrap}
.row .v{font-variant-numeric:tabular-nums; flex:none; color:var(--text)}
.row .v.dim{color:var(--faint)}
.bar{height:5px; background:var(--border); border-radius:3px; overflow:hidden;
margin:8px 0 3px}
.bar i{display:block; height:100%; border-radius:3px; background:var(--ok);
transition:width .5s cubic-bezier(.4,0,.2,1)}
.bar i.warn{background:var(--warn)} .bar i.crit{background:var(--crit)}
.stats{display:flex; gap:24px; margin-bottom:14px; flex-wrap:wrap}
.stat .n{font:500 22px/1.1 var(--serif); font-variant-numeric:tabular-nums}
.stat .l{font-size:10.5px; color:var(--faint); text-transform:uppercase;
letter-spacing:.7px; margin-top:3px}
/* flex:1 让列表吃掉卡片剩余高度:同一行等高,内容多的就能多显示几条 */
.list{flex:1 1 auto; min-height:96px; max-height:min(420px,52vh);
overflow-y:auto; margin:0 -4px; padding:0 4px}
.list::-webkit-scrollbar{width:4px}
.list::-webkit-scrollbar-thumb{background:var(--border-strong); border-radius:2px}
.list::-webkit-scrollbar-track{background:transparent}
.tag{font:500 10.5px/1.5 var(--sans); padding:2px 7px; border-radius:5px;
background:var(--card-alt); color:var(--dim); border:1px solid var(--border);
flex:none; white-space:nowrap}
.tag.ok{background:var(--ok-soft); color:var(--ok); border-color:transparent}
.tag.warn{background:var(--warn-soft); color:var(--warn); border-color:transparent}
.tag.crit{background:var(--crit-soft); color:var(--crit); border-color:transparent}
.tag.accent{background:var(--accent-soft); color:var(--accent); border-color:transparent}
/* 机器名标签。同一台机器在告警列表、封禁列表、攻击来源、节点卡片里都用
同一个色,扫一眼就能把行按机器归类——只靠文字的话,多机场景下要逐行读。
颜色由名字 hash 而来,加机器不用改代码;四个色调是刻意的上限,
再多就分不清了,而且这个面板本来也不是为几十台机器设计的 */
.tag.mch{font-weight:600; border-color:transparent}
.tag.m0{background:rgba(122,154,101,.16); color:#5F7D4B}
.tag.m1{background:rgba(90,141,176,.16); color:#3F7196}
.tag.m2{background:rgba(168,116,180,.16); color:#8A5A9B}
.tag.m3{background:rgba(193,133,42,.16); color:#9A6816}
@media (prefers-color-scheme:dark){
.tag.m0{background:rgba(143,174,121,.20); color:#A5C48D}
.tag.m1{background:rgba(110,164,201,.20); color:#8FBEDC}
.tag.m2{background:rgba(190,140,201,.20); color:#CFA6DA}
.tag.m3{background:rgba(209,155,68,.20); color:#E0B978}
}
.mono{font-family:var(--mono); font-size:12.5px}
.empty{color:var(--faint); font-size:13px; padding:22px 0; text-align:center}
.note{color:var(--faint); font-size:11.5px; margin-top:10px; line-height:1.5}
.stale{opacity:.5}
.center{margin:auto 0}
/* ---------- 图表 ---------- */
.chart{width:100%; display:block; overflow:visible}
.chart .area{fill:var(--accent); opacity:.11}
.chart .line{fill:none; stroke:var(--accent); stroke-width:1.6;
stroke-linejoin:round; stroke-linecap:round}
.chart .line.s1{stroke:var(--s1)}
.chart .line.s2{stroke:var(--s2)}
.chart .line.s3{stroke:var(--s3)}
.chart .line.s4{stroke:var(--s4)}
.chart .grid-line{stroke:var(--border); stroke-width:1}
.chart .axis{fill:var(--faint); font:10.5px var(--sans)}
.chart .cursor{stroke:var(--border-strong); stroke-width:1; stroke-dasharray:3 3}
.chart .peak{fill:var(--accent)}
.spark{height:38px; margin:6px 0 2px}
.chartbox{position:relative; margin-top:6px}
.legend{display:flex; flex-wrap:wrap; gap:2px 14px; margin:0 0 6px; font-size:11.5px}
.legend span{display:inline-flex; align-items:center; gap:6px; color:var(--dim)}
.legend i{width:10px; height:2.5px; border-radius:2px; background:var(--accent)}
.legend .s1 i{background:var(--s1)} .legend .s2 i{background:var(--s2)}
.legend .s3 i{background:var(--s3)} .legend .s4 i{background:var(--s4)}
.legend b{color:var(--text); font-weight:600; font-variant-numeric:tabular-nums}
.legend em{font-style:normal; color:var(--faint); font-size:11px;
font-variant-numeric:tabular-nums}
.chartbox .hint{position:absolute; right:0; top:-20px; font-size:11.5px;
color:var(--faint); font-variant-numeric:tabular-nums}
.chart-empty{height:120px; display:flex; align-items:center; justify-content:center;
color:var(--faint); font-size:12.5px; background:var(--card-alt);
border-radius:var(--radius-sm)}
/* ---------- 表单 ---------- */
input,select,textarea{
font:14px/1 var(--sans); color:var(--text); background:var(--card);
border:1px solid var(--border-strong); border-radius:var(--radius-sm);
padding:9px 11px; transition:.16s; min-width:0;
}
input:focus,select:focus{outline:none; border-color:var(--accent);
box-shadow:0 0 0 3px var(--accent-soft)}
input::placeholder{color:var(--faint)}
select{cursor:pointer; appearance:none; padding-right:30px;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2373716C' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat:no-repeat; background-position:right 11px center}
.btn{
font:500 13.5px/1 var(--sans); padding:10px 17px; border-radius:var(--radius-sm);
border:1px solid transparent; cursor:pointer; transition:.16s; white-space:nowrap;
background:var(--accent); color:#fff;
}
.btn:hover{background:var(--accent-hover)}
.btn:active{transform:translateY(1px)}
.btn:disabled{opacity:.45; cursor:not-allowed; transform:none}
.btn.ghost{background:transparent; color:var(--dim); border-color:var(--border-strong)}
.btn.ghost:hover{background:var(--card-alt); color:var(--text)}
.btn.sm{font-size:12px; padding:5px 11px; border-radius:6px}
.btn.danger{background:var(--crit); color:#fff}
.btn.danger:hover{filter:brightness(1.08)}
.btn.confirm{background:var(--warn); color:#fff}
.form{display:flex; gap:10px; flex-wrap:wrap; align-items:center}
.form input.ip{flex:1; min-width:170px; font-family:var(--mono); font-size:13px}
.form input.why{flex:1.3; min-width:150px}
/* ---------- 表格 ---------- */
.tbl{width:100%; border-collapse:collapse; font-size:13.5px}
.tbl th{
font:600 10.5px/1 var(--sans); color:var(--faint); text-transform:uppercase;
letter-spacing:.8px; text-align:left; padding:0 12px 10px 0;
border-bottom:1px solid var(--border); white-space:nowrap;
}
.tbl td{padding:9px 12px 9px 0; border-bottom:1px solid var(--border);
vertical-align:middle}
.tbl tr:last-child td{border-bottom:none}
.tbl tr:hover td{background:var(--card-alt)}
.tbl td.act{text-align:right; padding-right:0; width:1%; white-space:nowrap}
.tbl .ipcell{font-family:var(--mono); font-size:12.5px; color:var(--text)}
.tbl .why{color:var(--dim); max-width:240px; overflow:hidden;
text-overflow:ellipsis; white-space:nowrap}
.scroll{overflow:auto; max-height:430px; margin:0 -2px; padding:0 2px}
.scroll.tall{max-height:min(640px,66vh)}
/* 并排的两个长列表用固定高度而非 max-height:一个 8 条、一个 30 条时,
max-height 会让两张卡一高一矮,底边对不齐 */
.scroll.fixed{height:min(460px,58vh); max-height:none}
.scroll::-webkit-scrollbar{width:5px; height:5px}
.scroll::-webkit-scrollbar-thumb{background:var(--border-strong); border-radius:3px}
.filters{display:flex; gap:9px; align-items:center; flex-wrap:wrap; margin-bottom:14px}
.filters input{flex:1; min-width:150px}
.chip{
font:500 12px/1 var(--sans); padding:6px 12px; border-radius:20px; cursor:pointer;
border:1px solid var(--border-strong); background:transparent; color:var(--dim);
transition:.16s;
}
.chip:hover{border-color:var(--accent); color:var(--text)}
.chip.on{background:var(--text); color:var(--bg); border-color:var(--text)}
.callout{
background:var(--accent-soft); border:1px solid transparent;
border-left:2.5px solid var(--accent); border-radius:var(--radius-sm);
padding:11px 14px; font-size:12.5px; color:var(--dim); line-height:1.55;
margin-bottom:16px;
}
.callout.warn{background:var(--warn-soft); border-left-color:var(--warn)}
.callout.crit{background:var(--crit-soft); border-left-color:var(--crit)}
.callout b{color:var(--text); font-weight:600}
.callout code{font-family:var(--mono); font-size:11.5px; background:var(--card);
padding:1px 5px; border-radius:4px}
/* 安全中心的防护闭环。五步在一行时能直观看出“有决策但没落地”之类断点,
窄屏自动换行,不引入图表依赖。 */
.secpipe{display:grid; grid-template-columns:repeat(5,minmax(92px,1fr)); gap:8px}
.secstage{position:relative; display:flex; flex-direction:column; gap:2px;
padding:10px 9px 9px 35px; background:var(--card-alt);
border:1px solid var(--border); border-radius:var(--radius-sm); min-width:0}
.secstage>span{position:absolute; left:9px; top:11px; width:18px; height:18px;
border-radius:50%; background:var(--accent-soft); color:var(--accent);
font:600 10px/18px var(--sans); text-align:center}
.secstage b{font-size:11.5px; font-weight:500; color:var(--dim); white-space:nowrap}
.secstage em{font:600 13px/1.3 var(--mono); font-style:normal; overflow:hidden;
text-overflow:ellipsis; white-space:nowrap}
@media(max-width:900px){.secpipe{grid-template-columns:repeat(2,minmax(120px,1fr))}}
/* Leaflet 交互地图。本地 Natural Earth 图层始终存在;可选瓦片只提供细节,
不把攻击 IP 拼进瓦片 URL,也不调用外部地理编码接口。 */
.secmap-card{padding-bottom:14px}
.secmap-toolbar{display:flex; align-items:center; justify-content:space-between;
gap:12px; margin:-4px 0 12px; flex-wrap:wrap}
.secmap-toolbar .note{margin:0}
.map-switch{display:flex; gap:5px; padding:3px; border:1px solid var(--border);
border-radius:20px; background:var(--card-alt)}
.map-switch .chip{border-color:transparent; padding:6px 13px}
.secmap-layout{display:grid; grid-template-columns:minmax(0,2.2fr) minmax(250px,.8fr);
gap:22px; align-items:start}
.secmap{height:clamp(270px,28vw,420px); border:1px solid var(--border); border-radius:var(--radius-sm);
background:#DDE7EC; overflow:hidden; position:relative; z-index:0}
.secmap.leaflet-container{font-family:var(--sans)}
.secmap .leaflet-control-zoom a{color:#343330; background:rgba(255,255,255,.94)}
.secmap .leaflet-control-attribution{font-size:10px; background:rgba(255,255,255,.84)}
.secmap .leaflet-control-attribution a{color:#4D6D80}
.local-map-label{width:0 !important; height:0 !important; border:0; background:none}
.local-map-label span{position:absolute; left:0; top:0; transform:translate(-50%,-50%);
color:#77736B; font:9.5px/1.2 var(--sans); white-space:nowrap; text-shadow:
0 1px 0 rgba(255,255,255,.96),1px 0 0 rgba(255,255,255,.96),
-1px 0 0 rgba(255,255,255,.96),0 -1px 0 rgba(255,255,255,.96)}
.local-map-label.province span{color:#69655D; font-weight:550; font-size:10px}
.local-map-label.city span{color:#4F4C47; font-size:9.5px; padding:2px 4px;
background:rgba(255,255,255,.72); border-radius:3px;
text-shadow:none; box-shadow:0 1px 2px rgba(40,36,29,.06)}
.map-source-state{padding:4px 7px; border:1px solid rgba(160,155,144,.38);
border-radius:5px; background:rgba(255,255,255,.9); color:#706C64;
font:10px/1.2 var(--sans); box-shadow:0 1px 3px rgba(30,28,24,.08)}
.map-source-state.detail{color:#4D6D80}.map-source-state.fallback{color:#9A6816}
.map-source-state.error{color:#A33F33}
.secmap .leaflet-tooltip-pane{z-index:1000}
.secmap .leaflet-tooltip.attack-tooltip{font:12px/1.5 var(--sans); color:#242321;
box-sizing:border-box; width:max-content; min-width:210px; max-width:280px;
padding:10px 12px; white-space:normal; overflow-wrap:anywhere;
pointer-events:none; background:rgba(255,255,255,.98);
border:1px solid #CBC7BC; border-radius:9px;
box-shadow:0 8px 24px rgba(34,32,27,.20)}
.secmap .map-tip-head{font-weight:650; font-size:13px; color:#262521; margin-bottom:5px}
.secmap .map-tip-coord{font-size:11px; color:#918E86; margin:-3px 0 5px}
.secmap .map-tip-counts{display:flex; gap:7px; color:#57554F; margin-bottom:5px}
.secmap .map-tip-counts span+span::before{content:"·"; margin-right:7px; color:#A5A198}
.secmap .map-provider{color:#6F6D66; padding-top:5px; border-top:1px solid #ECE9E1}
.secmap .map-tip-status{display:flex; align-items:center; gap:6px; margin-top:6px; font-weight:550}
.secmap .map-tip-status i{width:7px; height:7px; flex:none; border-radius:50%; background:#7AA36A}
.secmap .map-tip-status.blocked{color:#A33F33}
.secmap .map-tip-status.blocked i{background:#C24E3F}
.secmap .map-tip-status.clear{color:#638055}
.secmap-empty{position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
width:max-content; max-width:calc(100% - 32px); padding:8px 12px;
color:var(--dim); font-size:12px; text-align:center; pointer-events:none; z-index:500;
background:rgba(255,255,255,.92); border:1px solid var(--border-strong);
border-radius:var(--radius-sm); box-shadow:0 5px 16px rgba(35,31,25,.08)}
.secmap-rank{display:flex; flex-direction:column; min-width:0}
.secmap-legend{display:flex; align-items:center; flex-wrap:wrap; gap:8px 15px;
color:var(--dim); font-size:11.5px; margin-bottom:9px}
.map-key{display:inline-flex; align-items:center; gap:6px}
.map-dot{width:10px; height:10px; border-radius:50%; background:var(--accent);
border:2px solid var(--card)}
.map-dot.blocked{border-color:var(--crit)}
.secmap-rank .row{padding:8px 0}
.secmap-rank .k{flex:1}
.secmap-rank .bar{width:min(130px,34%); margin:0 8px 0 auto; flex:none}
.secmap-rank .v{font-size:12px; min-width:64px; text-align:right}
@media(max-width:1000px){
.secmap-layout{grid-template-columns:1fr}
.secmap-rank{display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 20px}
.secmap-legend{grid-column:1/-1}
}
@media(max-width:640px){
.secmap{height:220px}
.secmap-rank{display:block}
.secmap-toolbar{align-items:flex-start}
}
/* ---------- 告警条 ---------- */
.alertbar{display:flex; flex-direction:column; gap:8px; margin-bottom:18px}
.alertline{
display:flex; align-items:center; gap:11px; padding:11px 15px;
background:var(--card); border:1px solid var(--border);
border-left:2.5px solid var(--warn); border-radius:var(--radius-sm);
font-size:13.5px;
}
.alertline.crit{border-left-color:var(--crit)}
.alertline .t{font-weight:500}
.alertline .d{color:var(--dim); font-size:12.5px; overflow:hidden;
text-overflow:ellipsis; white-space:nowrap}
.alertline .when{margin-left:auto; color:var(--faint); font-size:12px;
white-space:nowrap; font-variant-numeric:tabular-nums}
/* ---------- 事件时间线 ---------- */
.timeline{position:relative; padding-left:19px}
.timeline::before{content:""; position:absolute; left:4px; top:6px; bottom:6px;
width:1px; background:var(--border)}
.tl-item{position:relative; padding:8px 0 8px 2px; font-size:13.5px}
.tl-item::before{content:""; position:absolute; left:-19px; top:14px;
width:7px; height:7px; border-radius:50%;
background:var(--faint); border:2px solid var(--bg)}
.tl-item.crit::before{background:var(--crit)}
.tl-item.warn::before{background:var(--warn)}
.tl-item.info::before{background:var(--ok)}
.tl-item .t{display:flex; align-items:center; gap:8px}
.tl-item .d{color:var(--dim); font-size:12.5px; margin-top:2px}
.tl-item .when{color:var(--faint); font-size:11.5px; margin-top:2px;
font-variant-numeric:tabular-nums}
/* ---------- 日志查看 ---------- */
.modal{
position:fixed; inset:0; z-index:200; background:rgba(20,20,19,.45);
display:flex; align-items:center; justify-content:center; padding:24px;
animation:fade .18s;
}
@keyframes fade{from{opacity:0}to{opacity:1}}
.modal-box{
background:var(--card); border:1px solid var(--border-strong);
border-radius:var(--radius); width:min(1100px,100%); max-height:86vh;
display:flex; flex-direction:column; overflow:hidden;
box-shadow:0 18px 50px rgba(0,0,0,.20);
}
.modal-head{display:flex; align-items:center; gap:12px; padding:15px 19px;
border-bottom:1px solid var(--border)}
.modal-head h3{font:500 16px/1 var(--serif)}
.modal-head .sp{margin-left:auto}
/* ---------- 节点卡片 ----------
每台一张,跟其他卡片一起在总览的 grid 里流动。不做成一张全宽卡片装下
所有节点:grid 默认不回填空隙,全宽卡片前面那张只占一列的卡片,
右边会空出一整排 */
.nodealert{font-size:11.5px; color:var(--crit); background:var(--crit-soft);
border-radius:5px; padding:4px 7px; margin-bottom:9px}
.nodemetrics{display:flex; flex-direction:column; gap:9px}
.nm .nmk{display:flex; justify-content:space-between; align-items:baseline;
font-size:11.5px; color:var(--dim); gap:8px}
.nm .nmk > span{overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.nm .nmk b{font:500 12.5px/1 var(--sans); color:var(--text); flex:none;
font-variant-numeric:tabular-nums}
.nm .bar{margin:4px 0 0}
.nmx{font-size:10.5px; color:var(--faint); margin-top:3px; line-height:1.45}
.nodefoot{
display:flex; flex-wrap:wrap; gap:3px 11px; margin-top:11px; padding-top:9px;
border-top:1px solid var(--border); font-size:11px; color:var(--dim);
}
.nodefoot b{font-weight:600; color:var(--text); font-variant-numeric:tabular-nums}
.nodefoot .dim{color:var(--faint); margin-left:auto}
.nodedetail{margin-top:11px; padding-top:10px; border-top:1px solid var(--border)}
/* 展开入口。做成整行而不是按钮:可点区域大,手机上好按 */
.nodemore{
margin:10px -20px -18px; padding:9px 20px 10px; cursor:pointer;
font-size:11.5px; color:var(--faint); text-align:center;
border-top:1px solid var(--border); transition:.16s;
border-radius:0 0 var(--radius) var(--radius);
}
.nodemore:hover{color:var(--accent); background:var(--card-alt)}
.tagwrap{display:flex; flex-wrap:wrap; gap:4px}
/* 登录框复用 .modal 的遮罩,只把盒子改窄 */
.modal-box.login{
width:min(340px,100%); padding:26px 24px 24px; gap:11px;
align-items:stretch; max-height:none;
}
.modal-box.login h3{font:500 20px/1.2 var(--serif); margin-bottom:2px}
.modal-box.login input{width:100%}
.modal-box.login .btn{margin-top:3px; justify-content:center; text-align:center}
.modal-box.login .err{
font-size:12.5px; color:var(--crit); background:var(--crit-soft);
border-radius:var(--radius-sm); padding:8px 10px;
}
.logbox{
flex:1; overflow:auto; padding:14px 19px; margin:0;
font:12px/1.65 var(--mono); white-space:pre-wrap; word-break:break-all;
background:var(--card-alt); color:var(--text);
}
.logbox::-webkit-scrollbar{width:6px; height:6px}
.logbox::-webkit-scrollbar-thumb{background:var(--border-strong); border-radius:3px}
/* ---------- toast ---------- */
#toasts{position:fixed; right:20px; bottom:20px; z-index:300;
display:flex; flex-direction:column; gap:9px;
max-width:min(400px,calc(100vw - 40px))}
.toast{
background:var(--card); border:1px solid var(--border-strong);
border-left:2.5px solid var(--ok); border-radius:var(--radius-sm);
padding:12px 15px; font-size:13.5px; line-height:1.5;
box-shadow:0 6px 22px rgba(0,0,0,.10);
animation:slide .26s cubic-bezier(.2,.9,.3,1);
}
.toast.err{border-left-color:var(--crit)}
.toast b{display:block; font-weight:600; margin-bottom:2px; font-size:13px}
.toast span{color:var(--dim); font-size:12.5px}
@keyframes slide{from{opacity:0; transform:translateX(18px)}to{opacity:1; transform:none}}
@media (prefers-color-scheme:dark){
.toast{box-shadow:0 6px 22px rgba(0,0,0,.4)}
.modal-box{box-shadow:0 18px 50px rgba(0,0,0,.5)}
}
.hide{display:none !important}
/* ---------- 窄屏 ----------
表格列多,手机上横向滚动很难用。标了 .opt 的是次要列,窄屏直接不显示,
留下的都是一眼要看的和能点的。比把表格改成卡片式改动小得多,效果够用 */
@media (max-width:640px){
/* 防护节点那行右边挂着"检测/拦截"两个状态,和左边的名字挤在一起。
改成上下两行:名字在上,状态在下。总览的节点格子是 grid,
minmax 已经会自动单列,不用管 */
.nodeLine{flex-direction:column; align-items:stretch; gap:6px}
.nodeLine .v{
white-space:normal !important; text-align:left;
display:flex; flex-wrap:wrap; gap:4px 12px; font-size:12px;
}
.nodeLine .v > span{margin-left:0 !important}
.tbl .opt, .tbl th.opt{display:none}
.tbl th{padding-right:8px; font-size:10px}
.tbl td{padding-right:8px; font-size:12.5px}
.tbl .why{max-width:120px}
.btn.sm{padding:5px 9px; font-size:11.5px}
/* 八个页签两行完整展示。横向滚动会把后几个入口藏起来,安全中心加入后
“设置”等入口在手机首屏不可见,用户也没有明显线索知道还能横滑。 */
nav{overflow:visible; flex-wrap:wrap}
nav button{flex:1 1 calc(25% - 3px); min-width:0; padding:8px 5px}
.brand h1{font-size:21px}
/* 窄屏 sticky 头别占太高,状态行挪到导航下面单独一行已经够挤了 */
.status{font-size:11.5px}
.big{font-size:27px}
.stats{gap:16px}
.card{padding:15px 16px}
.form input, .form select, .form .btn{width:100%; flex:none}
.filters input{width:100%; flex:none}
.modal{padding:10px}
.logbox{font-size:11px; padding:11px 13px}
#toasts{right:10px; left:10px; bottom:10px; max-width:none}
}
/* ---------- 演示模式横幅 ---------- */
.demobar{
display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
background:var(--accent-soft); border:1px solid var(--accent);
border-radius:var(--radius); padding:12px 16px; margin-bottom:18px;
font-size:13px; line-height:1.6;
}
.demobar b{color:var(--accent); flex:none}
.demobar span{color:var(--text); flex:1 1 320px; min-width:0}
.demobar a{color:var(--accent); text-decoration:none; font-weight:500; flex:none}
.demobar a:hover{text-decoration:underline}