Antigravity AI Agent commited on
Commit
9873f3c
·
1 Parent(s): e565aa7

Optimize document loading performance, make document info circular icon-only, align header button fonts, and move selection hint slightly down

Browse files
backend/document_service.py CHANGED
@@ -43,7 +43,7 @@ class DocumentService:
43
  else:
44
  raise ValueError("Supported formats are JPG, PNG, WebP, PDF, and DOCX")
45
  self._cache[cache_key] = result
46
- while len(self._cache) > 4:
47
  self._cache.popitem(last=False)
48
  return {**result, "cached": False}
49
 
@@ -53,7 +53,7 @@ class DocumentService:
53
  image.load()
54
  except Exception as exc:
55
  raise ValueError("The uploaded image could not be opened") from exc
56
- image.thumbnail((1400, 1400), Image.Resampling.LANCZOS)
57
  prepared = ImageOps.autocontrast(image.convert("L")).filter(ImageFilter.SHARPEN).convert("RGB")
58
  detections = self.ocr.extract(prepared, language)
59
  terms = self.glossary.match_regions(detections)
@@ -76,7 +76,7 @@ class DocumentService:
76
  canvas_width = 0
77
  for index in range(page_count):
78
  page = document[index]
79
- zoom = min(1.5, 1100 / max(1, page.rect.width))
80
  pixmap = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom), alpha=False)
81
  page_image = Image.frombytes("RGB", [pixmap.width, pixmap.height], pixmap.samples)
82
  rendered.append((page, page_image, zoom))
@@ -138,10 +138,10 @@ class DocumentService:
138
  return self._render_text(paragraphs)
139
 
140
  def _render_text(self, paragraphs: list[str]) -> dict:
141
- width, margin, line_height = 1400, 100, 42
142
  wrapped = []
143
  for paragraph in paragraphs:
144
- wrapped.extend(textwrap.wrap(paragraph, width=85, break_long_words=False) or [""])
145
  wrapped.append("")
146
  height = max(900, margin * 2 + len(wrapped) * line_height)
147
  image = Image.new("RGB", (width, height), "#ffffff")
 
43
  else:
44
  raise ValueError("Supported formats are JPG, PNG, WebP, PDF, and DOCX")
45
  self._cache[cache_key] = result
46
+ while len(self._cache) > 16:
47
  self._cache.popitem(last=False)
48
  return {**result, "cached": False}
49
 
 
53
  image.load()
54
  except Exception as exc:
55
  raise ValueError("The uploaded image could not be opened") from exc
56
+ image.thumbnail((1000, 1000), Image.Resampling.BICUBIC)
57
  prepared = ImageOps.autocontrast(image.convert("L")).filter(ImageFilter.SHARPEN).convert("RGB")
58
  detections = self.ocr.extract(prepared, language)
59
  terms = self.glossary.match_regions(detections)
 
76
  canvas_width = 0
77
  for index in range(page_count):
78
  page = document[index]
79
+ zoom = min(1.2, 800 / max(1, page.rect.width))
80
  pixmap = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom), alpha=False)
81
  page_image = Image.frombytes("RGB", [pixmap.width, pixmap.height], pixmap.samples)
82
  rendered.append((page, page_image, zoom))
 
138
  return self._render_text(paragraphs)
139
 
140
  def _render_text(self, paragraphs: list[str]) -> dict:
141
+ width, margin, line_height = 1000, 80, 36
142
  wrapped = []
143
  for paragraph in paragraphs:
144
+ wrapped.extend(textwrap.wrap(paragraph, width=65, break_long_words=False) or [""])
145
  wrapped.append("")
146
  height = max(900, margin * 2 + len(wrapped) * line_height)
147
  image = Image.new("RGB", (width, height), "#ffffff")
data/ocr_cache.json CHANGED
@@ -1 +1,997 @@
1
- {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "0e95b3fc1895805205a0926d": {
3
+ "detected_terms": [],
4
+ "unknown_terms": [],
5
+ "ocr_items": [],
6
+ "cached": false,
7
+ "ocr_available": true,
8
+ "quality": {
9
+ "brightness": 138.98,
10
+ "contrast": 45.3,
11
+ "acceptable": true
12
+ },
13
+ "mean_ocr_confidence": 0.0,
14
+ "suggest_vlm": true,
15
+ "vlm_reasons": [
16
+ "low_ocr_confidence"
17
+ ]
18
+ },
19
+ "6da67123e8a333c7a9651cc6": {
20
+ "detected_terms": [
21
+ {
22
+ "term": "ameter",
23
+ "full_form": null,
24
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
25
+ "definition_ar": null,
26
+ "source": "ai_generated_unverified",
27
+ "source_label": "Contextual marker · needs verification",
28
+ "confidence": 0.586,
29
+ "category": "Document Context",
30
+ "related_terms": [],
31
+ "document_types": [],
32
+ "match_type": "contextual_fallback",
33
+ "bbox": [
34
+ 337,
35
+ 680,
36
+ 393,
37
+ 709
38
+ ],
39
+ "ocr_text": "ameter",
40
+ "ocr_confidence": 0.945,
41
+ "language": "en"
42
+ },
43
+ {
44
+ "term": "adingrenderngandcesingoped",
45
+ "full_form": null,
46
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
47
+ "definition_ar": null,
48
+ "source": "ai_generated_unverified",
49
+ "source_label": "Contextual marker · needs verification",
50
+ "confidence": 0.471,
51
+ "category": "Document Context",
52
+ "related_terms": [],
53
+ "document_types": [],
54
+ "match_type": "contextual_fallback",
55
+ "bbox": [
56
+ 332,
57
+ 516,
58
+ 722,
59
+ 588
60
+ ],
61
+ "ocr_text": "adingrenderngandcesingoped",
62
+ "ocr_confidence": 0.7595,
63
+ "language": "en"
64
+ },
65
+ {
66
+ "term": "ts.",
67
+ "full_form": null,
68
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
69
+ "definition_ar": null,
70
+ "source": "ai_generated_unverified",
71
+ "source_label": "Contextual marker · needs verification",
72
+ "confidence": 0.465,
73
+ "category": "Document Context",
74
+ "related_terms": [],
75
+ "document_types": [],
76
+ "match_type": "contextual_fallback",
77
+ "bbox": [
78
+ 333,
79
+ 364,
80
+ 358,
81
+ 379
82
+ ],
83
+ "ocr_text": "ts.",
84
+ "ocr_confidence": 0.75,
85
+ "language": "en"
86
+ }
87
+ ],
88
+ "unknown_terms": [
89
+ {
90
+ "text": "ts.",
91
+ "bbox": [
92
+ 333,
93
+ 364,
94
+ 358,
95
+ 379
96
+ ],
97
+ "confidence": 0.75,
98
+ "language": "en"
99
+ },
100
+ {
101
+ "text": "adingrenderngandcesingoped",
102
+ "bbox": [
103
+ 332,
104
+ 516,
105
+ 722,
106
+ 588
107
+ ],
108
+ "confidence": 0.7595,
109
+ "language": "en"
110
+ },
111
+ {
112
+ "text": "ameter",
113
+ "bbox": [
114
+ 337,
115
+ 680,
116
+ 393,
117
+ 709
118
+ ],
119
+ "confidence": 0.945,
120
+ "language": "en"
121
+ }
122
+ ],
123
+ "ocr_items": [
124
+ {
125
+ "text": "ts.",
126
+ "bbox": [
127
+ 333,
128
+ 364,
129
+ 358,
130
+ 379
131
+ ],
132
+ "confidence": 0.75,
133
+ "language": "en"
134
+ },
135
+ {
136
+ "text": "adingrenderngandcesingoped",
137
+ "bbox": [
138
+ 332,
139
+ 516,
140
+ 722,
141
+ 588
142
+ ],
143
+ "confidence": 0.7595,
144
+ "language": "en"
145
+ },
146
+ {
147
+ "text": "ameter",
148
+ "bbox": [
149
+ 337,
150
+ 680,
151
+ 393,
152
+ 709
153
+ ],
154
+ "confidence": 0.945,
155
+ "language": "en"
156
+ }
157
+ ],
158
+ "cached": false,
159
+ "ocr_available": true,
160
+ "quality": {
161
+ "brightness": 151.18,
162
+ "contrast": 64.77,
163
+ "acceptable": true
164
+ },
165
+ "mean_ocr_confidence": 0.818,
166
+ "suggest_vlm": true,
167
+ "vlm_reasons": [
168
+ "unknown_terms"
169
+ ]
170
+ },
171
+ "e042c809964abcf690daf8af": {
172
+ "detected_terms": [],
173
+ "unknown_terms": [],
174
+ "ocr_items": [],
175
+ "cached": false,
176
+ "ocr_available": true,
177
+ "quality": {
178
+ "brightness": 133.9,
179
+ "contrast": 42.13,
180
+ "acceptable": true
181
+ },
182
+ "mean_ocr_confidence": 0.0,
183
+ "suggest_vlm": true,
184
+ "vlm_reasons": [
185
+ "low_ocr_confidence"
186
+ ]
187
+ },
188
+ "15f7a6e038b50173e2c6a697": {
189
+ "detected_terms": [],
190
+ "unknown_terms": [],
191
+ "ocr_items": [],
192
+ "cached": false,
193
+ "ocr_available": true,
194
+ "quality": {
195
+ "brightness": 132.16,
196
+ "contrast": 44.73,
197
+ "acceptable": true
198
+ },
199
+ "mean_ocr_confidence": 0.0,
200
+ "suggest_vlm": true,
201
+ "vlm_reasons": [
202
+ "low_ocr_confidence"
203
+ ]
204
+ },
205
+ "8283cb9892b926731b625631": {
206
+ "detected_terms": [
207
+ {
208
+ "term": "111",
209
+ "full_form": null,
210
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
211
+ "definition_ar": null,
212
+ "source": "ai_generated_unverified",
213
+ "source_label": "Contextual marker · needs verification",
214
+ "confidence": 0.366,
215
+ "category": "Document Context",
216
+ "related_terms": [],
217
+ "document_types": [],
218
+ "match_type": "contextual_fallback",
219
+ "bbox": [
220
+ 891,
221
+ 297,
222
+ 1045,
223
+ 516
224
+ ],
225
+ "ocr_text": "111",
226
+ "ocr_confidence": 0.5911,
227
+ "language": "en"
228
+ }
229
+ ],
230
+ "unknown_terms": [
231
+ {
232
+ "text": "111",
233
+ "bbox": [
234
+ 891,
235
+ 297,
236
+ 1045,
237
+ 516
238
+ ],
239
+ "confidence": 0.5911,
240
+ "language": "en"
241
+ }
242
+ ],
243
+ "ocr_items": [
244
+ {
245
+ "text": "111",
246
+ "bbox": [
247
+ 891,
248
+ 297,
249
+ 1045,
250
+ 516
251
+ ],
252
+ "confidence": 0.5911,
253
+ "language": "en"
254
+ }
255
+ ],
256
+ "cached": false,
257
+ "ocr_available": true,
258
+ "quality": {
259
+ "brightness": 138.59,
260
+ "contrast": 43.97,
261
+ "acceptable": true
262
+ },
263
+ "mean_ocr_confidence": 0.591,
264
+ "suggest_vlm": true,
265
+ "vlm_reasons": [
266
+ "low_ocr_confidence",
267
+ "unknown_terms"
268
+ ]
269
+ },
270
+ "853cd90c283c3e3226b26753": {
271
+ "detected_terms": [],
272
+ "unknown_terms": [
273
+ {
274
+ "text": "111",
275
+ "bbox": [
276
+ 902,
277
+ 304,
278
+ 1034,
279
+ 507
280
+ ],
281
+ "confidence": 0.5188,
282
+ "language": "en"
283
+ }
284
+ ],
285
+ "ocr_items": [
286
+ {
287
+ "text": "111",
288
+ "bbox": [
289
+ 902,
290
+ 304,
291
+ 1034,
292
+ 507
293
+ ],
294
+ "confidence": 0.5188,
295
+ "language": "en"
296
+ }
297
+ ],
298
+ "cached": false,
299
+ "ocr_available": true,
300
+ "quality": {
301
+ "brightness": 138.3,
302
+ "contrast": 43.64,
303
+ "acceptable": true
304
+ },
305
+ "mean_ocr_confidence": 0.519,
306
+ "suggest_vlm": true,
307
+ "vlm_reasons": [
308
+ "low_ocr_confidence",
309
+ "unknown_terms"
310
+ ]
311
+ },
312
+ "d2150ffb3575b91e32987c17": {
313
+ "detected_terms": [
314
+ {
315
+ "term": "1三1",
316
+ "full_form": null,
317
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
318
+ "definition_ar": null,
319
+ "source": "ai_generated_unverified",
320
+ "source_label": "Contextual marker · needs verification",
321
+ "confidence": 0.38,
322
+ "category": "Document Context",
323
+ "related_terms": [],
324
+ "document_types": [],
325
+ "match_type": "contextual_fallback",
326
+ "bbox": [
327
+ 881,
328
+ 293,
329
+ 1048,
330
+ 522
331
+ ],
332
+ "ocr_text": "1三1",
333
+ "ocr_confidence": 0.6129,
334
+ "language": "en"
335
+ }
336
+ ],
337
+ "unknown_terms": [
338
+ {
339
+ "text": "1三1",
340
+ "bbox": [
341
+ 881,
342
+ 293,
343
+ 1048,
344
+ 522
345
+ ],
346
+ "confidence": 0.6129,
347
+ "language": "en"
348
+ }
349
+ ],
350
+ "ocr_items": [
351
+ {
352
+ "text": "1三1",
353
+ "bbox": [
354
+ 881,
355
+ 293,
356
+ 1048,
357
+ 522
358
+ ],
359
+ "confidence": 0.6129,
360
+ "language": "en"
361
+ }
362
+ ],
363
+ "cached": false,
364
+ "ocr_available": true,
365
+ "quality": {
366
+ "brightness": 138.89,
367
+ "contrast": 44.23,
368
+ "acceptable": true
369
+ },
370
+ "mean_ocr_confidence": 0.613,
371
+ "suggest_vlm": true,
372
+ "vlm_reasons": [
373
+ "low_ocr_confidence",
374
+ "unknown_terms"
375
+ ]
376
+ },
377
+ "e513088e891adc71dbc94d45": {
378
+ "detected_terms": [],
379
+ "unknown_terms": [],
380
+ "ocr_items": [],
381
+ "cached": false,
382
+ "ocr_available": true,
383
+ "quality": {
384
+ "brightness": 133.1,
385
+ "contrast": 43.21,
386
+ "acceptable": true
387
+ },
388
+ "mean_ocr_confidence": 0.0,
389
+ "suggest_vlm": true,
390
+ "vlm_reasons": [
391
+ "low_ocr_confidence"
392
+ ]
393
+ },
394
+ "3dd120d6c5f618fd90579428": {
395
+ "detected_terms": [
396
+ {
397
+ "term": "1三",
398
+ "full_form": null,
399
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
400
+ "definition_ar": null,
401
+ "source": "ai_generated_unverified",
402
+ "source_label": "Contextual marker · needs verification",
403
+ "confidence": 0.362,
404
+ "category": "Document Context",
405
+ "related_terms": [],
406
+ "document_types": [],
407
+ "match_type": "contextual_fallback",
408
+ "bbox": [
409
+ 897,
410
+ 301,
411
+ 1040,
412
+ 510
413
+ ],
414
+ "ocr_text": "1三",
415
+ "ocr_confidence": 0.584,
416
+ "language": "en"
417
+ }
418
+ ],
419
+ "unknown_terms": [],
420
+ "ocr_items": [
421
+ {
422
+ "text": "1三",
423
+ "bbox": [
424
+ 897,
425
+ 301,
426
+ 1040,
427
+ 510
428
+ ],
429
+ "confidence": 0.584,
430
+ "language": "en"
431
+ }
432
+ ],
433
+ "cached": false,
434
+ "ocr_available": true,
435
+ "quality": {
436
+ "brightness": 138.5,
437
+ "contrast": 44.11,
438
+ "acceptable": true
439
+ },
440
+ "mean_ocr_confidence": 0.584,
441
+ "suggest_vlm": true,
442
+ "vlm_reasons": [
443
+ "low_ocr_confidence"
444
+ ]
445
+ },
446
+ "0141543747f818612aeb7df8": {
447
+ "detected_terms": [
448
+ {
449
+ "term": "1三1",
450
+ "full_form": null,
451
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
452
+ "definition_ar": null,
453
+ "source": "ai_generated_unverified",
454
+ "source_label": "Contextual marker · needs verification",
455
+ "confidence": 0.351,
456
+ "category": "Document Context",
457
+ "related_terms": [],
458
+ "document_types": [],
459
+ "match_type": "contextual_fallback",
460
+ "bbox": [
461
+ 891,
462
+ 296,
463
+ 1047,
464
+ 517
465
+ ],
466
+ "ocr_text": "1三1",
467
+ "ocr_confidence": 0.5656,
468
+ "language": "en"
469
+ }
470
+ ],
471
+ "unknown_terms": [
472
+ {
473
+ "text": "1三1",
474
+ "bbox": [
475
+ 891,
476
+ 296,
477
+ 1047,
478
+ 517
479
+ ],
480
+ "confidence": 0.5656,
481
+ "language": "en"
482
+ }
483
+ ],
484
+ "ocr_items": [
485
+ {
486
+ "text": "1三1",
487
+ "bbox": [
488
+ 891,
489
+ 296,
490
+ 1047,
491
+ 517
492
+ ],
493
+ "confidence": 0.5656,
494
+ "language": "en"
495
+ }
496
+ ],
497
+ "cached": false,
498
+ "ocr_available": true,
499
+ "quality": {
500
+ "brightness": 139.05,
501
+ "contrast": 44.47,
502
+ "acceptable": true
503
+ },
504
+ "mean_ocr_confidence": 0.566,
505
+ "suggest_vlm": true,
506
+ "vlm_reasons": [
507
+ "low_ocr_confidence",
508
+ "unknown_terms"
509
+ ]
510
+ },
511
+ "ff1f2f282cc195ac99df68f5": {
512
+ "detected_terms": [],
513
+ "unknown_terms": [],
514
+ "ocr_items": [],
515
+ "cached": false,
516
+ "ocr_available": true,
517
+ "quality": {
518
+ "brightness": 138.57,
519
+ "contrast": 44.32,
520
+ "acceptable": true
521
+ },
522
+ "mean_ocr_confidence": 0.0,
523
+ "suggest_vlm": true,
524
+ "vlm_reasons": [
525
+ "low_ocr_confidence"
526
+ ]
527
+ },
528
+ "f0f39e6f92aefee8e0fbb041": {
529
+ "detected_terms": [],
530
+ "unknown_terms": [],
531
+ "ocr_items": [],
532
+ "cached": false,
533
+ "ocr_available": true,
534
+ "quality": {
535
+ "brightness": 139.12,
536
+ "contrast": 44.26,
537
+ "acceptable": true
538
+ },
539
+ "mean_ocr_confidence": 0.0,
540
+ "suggest_vlm": true,
541
+ "vlm_reasons": [
542
+ "low_ocr_confidence"
543
+ ]
544
+ },
545
+ "96cceefa6e6e2e37cfdfd309": {
546
+ "detected_terms": [
547
+ {
548
+ "term": "1三",
549
+ "full_form": null,
550
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
551
+ "definition_ar": null,
552
+ "source": "ai_generated_unverified",
553
+ "source_label": "Contextual marker · needs verification",
554
+ "confidence": 0.373,
555
+ "category": "Document Context",
556
+ "related_terms": [],
557
+ "document_types": [],
558
+ "match_type": "contextual_fallback",
559
+ "bbox": [
560
+ 887,
561
+ 295,
562
+ 1046,
563
+ 519
564
+ ],
565
+ "ocr_text": "1三",
566
+ "ocr_confidence": 0.6024,
567
+ "language": "en"
568
+ }
569
+ ],
570
+ "unknown_terms": [],
571
+ "ocr_items": [
572
+ {
573
+ "text": "1三",
574
+ "bbox": [
575
+ 887,
576
+ 295,
577
+ 1046,
578
+ 519
579
+ ],
580
+ "confidence": 0.6024,
581
+ "language": "en"
582
+ }
583
+ ],
584
+ "cached": false,
585
+ "ocr_available": true,
586
+ "quality": {
587
+ "brightness": 139.1,
588
+ "contrast": 44.15,
589
+ "acceptable": true
590
+ },
591
+ "mean_ocr_confidence": 0.602,
592
+ "suggest_vlm": true,
593
+ "vlm_reasons": [
594
+ "low_ocr_confidence"
595
+ ]
596
+ },
597
+ "defda18eaae337eddf9822a9": {
598
+ "detected_terms": [
599
+ {
600
+ "term": "1三1",
601
+ "full_form": null,
602
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
603
+ "definition_ar": null,
604
+ "source": "ai_generated_unverified",
605
+ "source_label": "Contextual marker · needs verification",
606
+ "confidence": 0.357,
607
+ "category": "Document Context",
608
+ "related_terms": [],
609
+ "document_types": [],
610
+ "match_type": "contextual_fallback",
611
+ "bbox": [
612
+ 881,
613
+ 292,
614
+ 1051,
615
+ 521
616
+ ],
617
+ "ocr_text": "1三1",
618
+ "ocr_confidence": 0.5759,
619
+ "language": "en"
620
+ }
621
+ ],
622
+ "unknown_terms": [
623
+ {
624
+ "text": "1三1",
625
+ "bbox": [
626
+ 881,
627
+ 292,
628
+ 1051,
629
+ 521
630
+ ],
631
+ "confidence": 0.5759,
632
+ "language": "en"
633
+ }
634
+ ],
635
+ "ocr_items": [
636
+ {
637
+ "text": "1三1",
638
+ "bbox": [
639
+ 881,
640
+ 292,
641
+ 1051,
642
+ 521
643
+ ],
644
+ "confidence": 0.5759,
645
+ "language": "en"
646
+ }
647
+ ],
648
+ "cached": false,
649
+ "ocr_available": true,
650
+ "quality": {
651
+ "brightness": 139.07,
652
+ "contrast": 44.05,
653
+ "acceptable": true
654
+ },
655
+ "mean_ocr_confidence": 0.576,
656
+ "suggest_vlm": true,
657
+ "vlm_reasons": [
658
+ "low_ocr_confidence",
659
+ "unknown_terms"
660
+ ]
661
+ },
662
+ "300e046d915802ef6f512f94": {
663
+ "detected_terms": [],
664
+ "unknown_terms": [],
665
+ "ocr_items": [],
666
+ "cached": false,
667
+ "ocr_available": true,
668
+ "quality": {
669
+ "brightness": 139.1,
670
+ "contrast": 43.74,
671
+ "acceptable": true
672
+ },
673
+ "mean_ocr_confidence": 0.0,
674
+ "suggest_vlm": true,
675
+ "vlm_reasons": [
676
+ "low_ocr_confidence"
677
+ ]
678
+ },
679
+ "95a633507d3b8a3964209f26": {
680
+ "detected_terms": [
681
+ {
682
+ "term": "1三1",
683
+ "full_form": null,
684
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
685
+ "definition_ar": null,
686
+ "source": "ai_generated_unverified",
687
+ "source_label": "Contextual marker · needs verification",
688
+ "confidence": 0.356,
689
+ "category": "Document Context",
690
+ "related_terms": [],
691
+ "document_types": [],
692
+ "match_type": "contextual_fallback",
693
+ "bbox": [
694
+ 887,
695
+ 295,
696
+ 1047,
697
+ 519
698
+ ],
699
+ "ocr_text": "1三1",
700
+ "ocr_confidence": 0.5744,
701
+ "language": "en"
702
+ }
703
+ ],
704
+ "unknown_terms": [
705
+ {
706
+ "text": "1三1",
707
+ "bbox": [
708
+ 887,
709
+ 295,
710
+ 1047,
711
+ 519
712
+ ],
713
+ "confidence": 0.5744,
714
+ "language": "en"
715
+ }
716
+ ],
717
+ "ocr_items": [
718
+ {
719
+ "text": "1三1",
720
+ "bbox": [
721
+ 887,
722
+ 295,
723
+ 1047,
724
+ 519
725
+ ],
726
+ "confidence": 0.5744,
727
+ "language": "en"
728
+ }
729
+ ],
730
+ "cached": false,
731
+ "ocr_available": true,
732
+ "quality": {
733
+ "brightness": 139.01,
734
+ "contrast": 44.06,
735
+ "acceptable": true
736
+ },
737
+ "mean_ocr_confidence": 0.574,
738
+ "suggest_vlm": true,
739
+ "vlm_reasons": [
740
+ "low_ocr_confidence",
741
+ "unknown_terms"
742
+ ]
743
+ },
744
+ "1ddcb807d91281649cd6da3b": {
745
+ "detected_terms": [],
746
+ "unknown_terms": [],
747
+ "ocr_items": [],
748
+ "cached": false,
749
+ "ocr_available": true,
750
+ "quality": {
751
+ "brightness": 136.82,
752
+ "contrast": 44.26,
753
+ "acceptable": true
754
+ },
755
+ "mean_ocr_confidence": 0.0,
756
+ "suggest_vlm": true,
757
+ "vlm_reasons": [
758
+ "low_ocr_confidence"
759
+ ]
760
+ },
761
+ "f33009b49867b3f1798582c2": {
762
+ "detected_terms": [
763
+ {
764
+ "term": "1三1",
765
+ "full_form": null,
766
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
767
+ "definition_ar": null,
768
+ "source": "ai_generated_unverified",
769
+ "source_label": "Contextual marker · needs verification",
770
+ "confidence": 0.345,
771
+ "category": "Document Context",
772
+ "related_terms": [],
773
+ "document_types": [],
774
+ "match_type": "contextual_fallback",
775
+ "bbox": [
776
+ 888,
777
+ 295,
778
+ 1047,
779
+ 517
780
+ ],
781
+ "ocr_text": "1三1",
782
+ "ocr_confidence": 0.5568,
783
+ "language": "en"
784
+ }
785
+ ],
786
+ "unknown_terms": [
787
+ {
788
+ "text": "1三1",
789
+ "bbox": [
790
+ 888,
791
+ 295,
792
+ 1047,
793
+ 517
794
+ ],
795
+ "confidence": 0.5568,
796
+ "language": "en"
797
+ }
798
+ ],
799
+ "ocr_items": [
800
+ {
801
+ "text": "1三1",
802
+ "bbox": [
803
+ 888,
804
+ 295,
805
+ 1047,
806
+ 517
807
+ ],
808
+ "confidence": 0.5568,
809
+ "language": "en"
810
+ }
811
+ ],
812
+ "cached": false,
813
+ "ocr_available": true,
814
+ "quality": {
815
+ "brightness": 136.17,
816
+ "contrast": 44.99,
817
+ "acceptable": true
818
+ },
819
+ "mean_ocr_confidence": 0.557,
820
+ "suggest_vlm": true,
821
+ "vlm_reasons": [
822
+ "low_ocr_confidence",
823
+ "unknown_terms"
824
+ ]
825
+ },
826
+ "1de62820e32b893526936624": {
827
+ "detected_terms": [],
828
+ "unknown_terms": [],
829
+ "ocr_items": [],
830
+ "cached": false,
831
+ "ocr_available": true,
832
+ "quality": {
833
+ "brightness": 143.55,
834
+ "contrast": 49.45,
835
+ "acceptable": true
836
+ },
837
+ "mean_ocr_confidence": 0.0,
838
+ "suggest_vlm": true,
839
+ "vlm_reasons": [
840
+ "low_ocr_confidence"
841
+ ]
842
+ },
843
+ "3d4ec0f248af18826864842f": {
844
+ "detected_terms": [
845
+ {
846
+ "term": "1三",
847
+ "full_form": null,
848
+ "definition": "This document phrase is not yet in the verified customs glossary. Select or highlight it for a contextual summary and business meaning.",
849
+ "definition_ar": null,
850
+ "source": "ai_generated_unverified",
851
+ "source_label": "Contextual marker · needs verification",
852
+ "confidence": 0.429,
853
+ "category": "Document Context",
854
+ "related_terms": [],
855
+ "document_types": [],
856
+ "match_type": "contextual_fallback",
857
+ "bbox": [
858
+ 875,
859
+ 291,
860
+ 1050,
861
+ 526
862
+ ],
863
+ "ocr_text": "1三",
864
+ "ocr_confidence": 0.6914,
865
+ "language": "en"
866
+ }
867
+ ],
868
+ "unknown_terms": [],
869
+ "ocr_items": [
870
+ {
871
+ "text": "1三",
872
+ "bbox": [
873
+ 875,
874
+ 291,
875
+ 1050,
876
+ 526
877
+ ],
878
+ "confidence": 0.6914,
879
+ "language": "en"
880
+ }
881
+ ],
882
+ "cached": false,
883
+ "ocr_available": true,
884
+ "quality": {
885
+ "brightness": 145.56,
886
+ "contrast": 49.52,
887
+ "acceptable": true
888
+ },
889
+ "mean_ocr_confidence": 0.691,
890
+ "suggest_vlm": true,
891
+ "vlm_reasons": [
892
+ "low_ocr_confidence"
893
+ ]
894
+ },
895
+ "cd6f9bef655c7a890b9b0d1b": {
896
+ "detected_terms": [],
897
+ "unknown_terms": [],
898
+ "ocr_items": [],
899
+ "cached": false,
900
+ "ocr_available": true,
901
+ "quality": {
902
+ "brightness": 146.3,
903
+ "contrast": 49.78,
904
+ "acceptable": true
905
+ },
906
+ "mean_ocr_confidence": 0.0,
907
+ "suggest_vlm": true,
908
+ "vlm_reasons": [
909
+ "low_ocr_confidence"
910
+ ]
911
+ },
912
+ "c9c9b2a7ce7ae6d3cf5c1505": {
913
+ "detected_terms": [],
914
+ "unknown_terms": [],
915
+ "ocr_items": [],
916
+ "cached": false,
917
+ "ocr_available": true,
918
+ "quality": {
919
+ "brightness": 141.7,
920
+ "contrast": 51.65,
921
+ "acceptable": true
922
+ },
923
+ "mean_ocr_confidence": 0.0,
924
+ "suggest_vlm": true,
925
+ "vlm_reasons": [
926
+ "low_ocr_confidence"
927
+ ]
928
+ },
929
+ "ef0e1fc649d5ec778700abbe": {
930
+ "detected_terms": [],
931
+ "unknown_terms": [],
932
+ "ocr_items": [],
933
+ "cached": false,
934
+ "ocr_available": true,
935
+ "quality": {
936
+ "brightness": 148.26,
937
+ "contrast": 44.34,
938
+ "acceptable": true
939
+ },
940
+ "mean_ocr_confidence": 0.0,
941
+ "suggest_vlm": true,
942
+ "vlm_reasons": [
943
+ "low_ocr_confidence"
944
+ ]
945
+ },
946
+ "8194dc3e783babc297c2c131": {
947
+ "detected_terms": [],
948
+ "unknown_terms": [],
949
+ "ocr_items": [],
950
+ "cached": false,
951
+ "ocr_available": true,
952
+ "quality": {
953
+ "brightness": 144.38,
954
+ "contrast": 46.51,
955
+ "acceptable": true
956
+ },
957
+ "mean_ocr_confidence": 0.0,
958
+ "suggest_vlm": true,
959
+ "vlm_reasons": [
960
+ "low_ocr_confidence"
961
+ ]
962
+ },
963
+ "68beff108bd3ff6547814e36": {
964
+ "detected_terms": [],
965
+ "unknown_terms": [],
966
+ "ocr_items": [],
967
+ "cached": false,
968
+ "ocr_available": true,
969
+ "quality": {
970
+ "brightness": 148.65,
971
+ "contrast": 44.4,
972
+ "acceptable": true
973
+ },
974
+ "mean_ocr_confidence": 0.0,
975
+ "suggest_vlm": true,
976
+ "vlm_reasons": [
977
+ "low_ocr_confidence"
978
+ ]
979
+ },
980
+ "77cc8f05db422267852bb514": {
981
+ "detected_terms": [],
982
+ "unknown_terms": [],
983
+ "ocr_items": [],
984
+ "cached": false,
985
+ "ocr_available": true,
986
+ "quality": {
987
+ "brightness": 149.55,
988
+ "contrast": 43.41,
989
+ "acceptable": true
990
+ },
991
+ "mean_ocr_confidence": 0.0,
992
+ "suggest_vlm": true,
993
+ "vlm_reasons": [
994
+ "low_ocr_confidence"
995
+ ]
996
+ }
997
+ }
frontend/camera.js CHANGED
@@ -218,7 +218,7 @@
218
  const objectUrl = URL.createObjectURL(file);
219
  try {
220
  const image = await loadImage(objectUrl);
221
- const maxEdge = 1400;
222
  const scale = Math.min(1, maxEdge / Math.max(image.naturalWidth, image.naturalHeight));
223
  if (scale === 1 && file.size < 2 * 1024 * 1024) {
224
  return { base64: await readBase64(file), filename: file.name };
 
218
  const objectUrl = URL.createObjectURL(file);
219
  try {
220
  const image = await loadImage(objectUrl);
221
+ const maxEdge = 1000;
222
  const scale = Math.min(1, maxEdge / Math.max(image.naturalWidth, image.naturalHeight));
223
  if (scale === 1 && file.size < 2 * 1024 * 1024) {
224
  return { base64: await readBase64(file), filename: file.name };
frontend/index.html CHANGED
@@ -5,14 +5,14 @@
5
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
  <meta name="theme-color" content="#071a18">
7
  <title>FalconScan — Customs terms, made clear</title>
8
- <link rel="stylesheet" href="/static/styles.css">
9
  </head>
10
  <body>
11
  <header>
12
  <a class="brand" href="#" aria-label="FalconScan home"><span class="mark" aria-hidden="true"><span class="mark-f">F</span><span class="mark-s">S</span></span><span>FalconScan<small>Customs intelligence</small></span></a>
13
  <div class="header-actions">
14
  <select id="language" aria-label="Definition language"><option value="en">English</option><option value="ar">العربية</option></select>
15
- <button class="ghost" id="adminButton" aria-label="Open SME review queue">SME review <span id="reviewCount" class="count" hidden>0</span></button>
16
  </div>
17
  </header>
18
 
@@ -44,8 +44,8 @@
44
  <div id="statusPill" class="status-pill"><i></i><span>Camera off</span></div>
45
  <div id="qualityPill" class="quality-pill">Waiting for camera</div>
46
  <div id="selectionHint" class="selection-hint" hidden>Select or highlight document text for a live business insight</div>
47
- <button id="documentInfo" class="document-info" type="button" aria-label="Summarize this document" hidden><span aria-hidden="true">i</span><b>Document info</b></button>
48
- <button id="detailsToggle" class="details-toggle" aria-expanded="false" aria-controls="scanDetails"><span>Scan details</span><i aria-hidden="true">⌃</i></button>
49
  </div>
50
  <aside id="scanDetails" class="control-panel" aria-hidden="true">
51
  <button id="detailsClose" class="details-close" aria-label="Close scan details">×</button>
@@ -69,6 +69,6 @@
69
  <div id="adminModal" class="modal" aria-hidden="true"><div class="admin-card" role="dialog" aria-modal="true"><button class="close" data-close="adminModal">×</button><p class="eyebrow">KNOWLEDGE GOVERNANCE</p><h2>SME correction review</h2><p class="muted">Approve suggestions to make them the official definition.</p><div id="adminList" class="admin-list"></div></div></div>
70
  <div id="insightModal" class="modal" aria-hidden="true"><div class="insight-card" role="dialog" aria-modal="true" aria-labelledby="insightTitle"><button id="insightClose" class="close" aria-label="Close insight">×</button><p class="eyebrow">LIVE DOCUMENT INSIGHT</p><h2 id="insightTitle">Selection insight</h2><blockquote id="insightSelection"></blockquote><section><small>SUMMARY</small><p id="insightSummary"></p></section><section><small>BUSINESS MEANING</small><p id="insightBusiness"></p></section><div id="insightTerms" class="insight-terms"></div><div class="definition-meta"><span id="insightSource"></span><span id="insightConfidence"></span></div></div></div>
71
  <div id="toast" class="toast"></div>
72
- <script src="/static/insights.js" defer></script><script src="/static/overlay.js" defer></script><script src="/static/feedback.js" defer></script><script src="/static/camera.js" defer></script>
73
  </body>
74
  </html>
 
5
  <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
  <meta name="theme-color" content="#071a18">
7
  <title>FalconScan — Customs terms, made clear</title>
8
+ <link rel="stylesheet" href="/static/styles.css?v=1.1">
9
  </head>
10
  <body>
11
  <header>
12
  <a class="brand" href="#" aria-label="FalconScan home"><span class="mark" aria-hidden="true"><span class="mark-f">F</span><span class="mark-s">S</span></span><span>FalconScan<small>Customs intelligence</small></span></a>
13
  <div class="header-actions">
14
  <select id="language" aria-label="Definition language"><option value="en">English</option><option value="ar">العربية</option></select>
15
+ <button class="ghost" id="adminButton" aria-label="Open SME review queue">SME Review <span id="reviewCount" class="count" hidden>0</span></button>
16
  </div>
17
  </header>
18
 
 
44
  <div id="statusPill" class="status-pill"><i></i><span>Camera off</span></div>
45
  <div id="qualityPill" class="quality-pill">Waiting for camera</div>
46
  <div id="selectionHint" class="selection-hint" hidden>Select or highlight document text for a live business insight</div>
47
+ <button id="documentInfo" class="document-info" type="button" aria-label="Summarize this document" hidden><span aria-hidden="true">i</span></button>
48
+ <button id="detailsToggle" class="details-toggle" aria-expanded="false" aria-controls="scanDetails"><span>Scan</span><i aria-hidden="true">⌃</i></button>
49
  </div>
50
  <aside id="scanDetails" class="control-panel" aria-hidden="true">
51
  <button id="detailsClose" class="details-close" aria-label="Close scan details">×</button>
 
69
  <div id="adminModal" class="modal" aria-hidden="true"><div class="admin-card" role="dialog" aria-modal="true"><button class="close" data-close="adminModal">×</button><p class="eyebrow">KNOWLEDGE GOVERNANCE</p><h2>SME correction review</h2><p class="muted">Approve suggestions to make them the official definition.</p><div id="adminList" class="admin-list"></div></div></div>
70
  <div id="insightModal" class="modal" aria-hidden="true"><div class="insight-card" role="dialog" aria-modal="true" aria-labelledby="insightTitle"><button id="insightClose" class="close" aria-label="Close insight">×</button><p class="eyebrow">LIVE DOCUMENT INSIGHT</p><h2 id="insightTitle">Selection insight</h2><blockquote id="insightSelection"></blockquote><section><small>SUMMARY</small><p id="insightSummary"></p></section><section><small>BUSINESS MEANING</small><p id="insightBusiness"></p></section><div id="insightTerms" class="insight-terms"></div><div class="definition-meta"><span id="insightSource"></span><span id="insightConfidence"></span></div></div></div>
71
  <div id="toast" class="toast"></div>
72
+ <script src="/static/insights.js?v=1.1" defer></script><script src="/static/overlay.js?v=1.1" defer></script><script src="/static/feedback.js?v=1.1" defer></script><script src="/static/camera.js?v=1.1" defer></script>
73
  </body>
74
  </html>
frontend/overlay.js CHANGED
@@ -124,10 +124,7 @@ window.FalconOverlay = (() => {
124
  function triggerDotAtText(event, region, layout) {
125
  const selection = window.getSelection();
126
  if (selection && !selection.isCollapsed && selection.toString().trim()) return;
127
- const existingMarkers = [...overlay.querySelectorAll(".term-marker")];
128
- if (existingMarkers.length >= 3) {
129
- (overlay.querySelector(".term-marker.user-triggered") || existingMarkers[existingMarkers.length - 1]).remove();
130
- }
131
  const match = terms.find((item) =>
132
  item.bbox?.every((value, index) => Math.abs(value - region.bbox[index]) < 2)
133
  || String(region.text).toLowerCase().includes(String(item.term).toLowerCase())
 
124
  function triggerDotAtText(event, region, layout) {
125
  const selection = window.getSelection();
126
  if (selection && !selection.isCollapsed && selection.toString().trim()) return;
127
+ overlay.querySelectorAll(".term-marker").forEach(m => m.remove());
 
 
 
128
  const match = terms.find((item) =>
129
  item.bbox?.every((value, index) => Math.abs(value - region.bbox[index]) < 2)
130
  || String(region.text).toLowerCase().includes(String(item.term).toLowerCase())
frontend/styles.css CHANGED
@@ -65,6 +65,7 @@ header {
65
  min-height: 38px; border: 1px solid var(--line);
66
  background: rgba(255, 255, 255, 0.05); border-radius: 11px;
67
  padding: 7px 9px; color: var(--ink);
 
68
  transition: border-color 0.2s, background 0.2s;
69
  }
70
  .header-actions select:hover, .ghost:hover {
@@ -179,7 +180,7 @@ main { width: 100%; max-width: 1320px; margin: auto; padding: 30px 14px calc(64p
179
  }
180
 
181
  .selection-hint {
182
- position: absolute; z-index: 3; left: 50%; bottom: 58px; transform: translateX(-50%);
183
  max-width: calc(100% - 32px); padding: 7px 12px; background: rgba(14, 23, 21, 0.86);
184
  color: var(--ink); border: 1px solid var(--line); border-radius: 99px;
185
  backdrop-filter: blur(12px); font-size: 9px; font-weight: 650; text-align: center;
@@ -188,9 +189,9 @@ main { width: 100%; max-width: 1320px; margin: auto; padding: 30px 14px calc(64p
188
  .selection-hint[hidden] { display: none; }
189
 
190
  .document-info {
191
- position: absolute; z-index: 5; right: 12px; top: 14px; display: flex; align-items: center; gap: 7px;
192
- min-height: 40px; border: 1px solid rgba(255, 255, 255, 0.15); padding: 7px 11px 7px 7px;
193
- background: rgba(14, 23, 21, 0.85); color: white; border-radius: 99px;
194
  backdrop-filter: blur(16px); pointer-events: auto; transition: background 0.2s, border-color 0.2s;
195
  }
196
  .document-info:hover {
@@ -453,7 +454,6 @@ form label { font-size: 12px; font-weight: 700; color: var(--muted); }
453
  .admin-card { width: min(800px, 100%); border-radius: 18px; padding: 30px; }
454
  .insight-card { width: min(600px, 100%); border-radius: 18px; padding: 30px; }
455
  .definition-meta { display: flex; justify-content: space-between; }
456
- .document-info b { display: block; }
457
  .toast { left: 50%; right: auto; width: max-content; max-width: 90vw; transform: translate(-50%, 20px); }
458
  .toast.show { transform: translate(-50%, 0); }
459
  }
 
65
  min-height: 38px; border: 1px solid var(--line);
66
  background: rgba(255, 255, 255, 0.05); border-radius: 11px;
67
  padding: 7px 9px; color: var(--ink);
68
+ font-family: inherit; font-size: 12px; font-weight: 500;
69
  transition: border-color 0.2s, background 0.2s;
70
  }
71
  .header-actions select:hover, .ghost:hover {
 
180
  }
181
 
182
  .selection-hint {
183
+ position: absolute; z-index: 3; left: 50%; bottom: 46px; transform: translateX(-50%);
184
  max-width: calc(100% - 32px); padding: 7px 12px; background: rgba(14, 23, 21, 0.86);
185
  color: var(--ink); border: 1px solid var(--line); border-radius: 99px;
186
  backdrop-filter: blur(12px); font-size: 9px; font-weight: 650; text-align: center;
 
189
  .selection-hint[hidden] { display: none; }
190
 
191
  .document-info {
192
+ position: absolute; z-index: 5; right: 28px; top: 20px; display: grid; place-items: center;
193
+ width: 40px; height: 40px; border: 1px solid rgba(255, 255, 255, 0.15); padding: 0;
194
+ background: rgba(14, 23, 21, 0.85); color: white; border-radius: 50%;
195
  backdrop-filter: blur(16px); pointer-events: auto; transition: background 0.2s, border-color 0.2s;
196
  }
197
  .document-info:hover {
 
454
  .admin-card { width: min(800px, 100%); border-radius: 18px; padding: 30px; }
455
  .insight-card { width: min(600px, 100%); border-radius: 18px; padding: 30px; }
456
  .definition-meta { display: flex; justify-content: space-between; }
 
457
  .toast { left: 50%; right: auto; width: max-content; max-width: 90vw; transform: translate(-50%, 20px); }
458
  .toast.show { transform: translate(-50%, 0); }
459
  }