@sphoro/bugcatch-web-sdk 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +104 -6
  2. package/dist/bugcatch-call.es.js +587 -0
  3. package/dist/bugcatch-call.es.js.map +1 -0
  4. package/dist/bugcatch-call.umd.js +2 -0
  5. package/dist/bugcatch-call.umd.js.map +1 -0
  6. package/dist/bugcatch-core.es.js +3292 -0
  7. package/dist/bugcatch-core.es.js.map +1 -0
  8. package/dist/bugcatch-panel.es.js +2531 -0
  9. package/dist/bugcatch-panel.es.js.map +1 -0
  10. package/dist/bugcatch-panel.umd.js +1311 -0
  11. package/dist/bugcatch-panel.umd.js.map +1 -0
  12. package/dist/bugcatch.es.js +2 -5625
  13. package/dist/bugcatch.es.js.map +1 -1
  14. package/dist/bugcatch.standalone.umd.js +1619 -0
  15. package/dist/bugcatch.standalone.umd.js.map +1 -0
  16. package/dist/bugcatch.umd.js +195 -1318
  17. package/dist/bugcatch.umd.js.map +1 -1
  18. package/package.json +6 -6
  19. package/src/accent.js +38 -0
  20. package/src/announcements.js +147 -0
  21. package/src/busy.js +18 -0
  22. package/src/call.js +7 -0
  23. package/src/capture.js +9 -0
  24. package/src/chunks.js +56 -0
  25. package/src/gesture.js +33 -0
  26. package/src/icons.js +18 -0
  27. package/src/identity.js +13 -8
  28. package/src/incoming-call.js +25 -45
  29. package/src/index.d.ts +93 -2
  30. package/src/index.js +164 -42
  31. package/src/lazy.js +23 -0
  32. package/src/notice.js +68 -0
  33. package/src/panel/call.js +242 -0
  34. package/src/panel/compose.js +416 -0
  35. package/src/panel/composer.js +235 -0
  36. package/src/panel/icons.js +36 -0
  37. package/src/panel/index.js +265 -0
  38. package/src/panel/known.js +130 -0
  39. package/src/panel/messages.js +176 -0
  40. package/src/panel/notices.js +34 -0
  41. package/src/panel/picks.js +65 -0
  42. package/src/panel/refs.js +321 -0
  43. package/src/panel/render.js +132 -0
  44. package/src/panel/signin.js +228 -0
  45. package/src/panel/styles.js +1016 -0
  46. package/src/panel/thread.js +494 -0
  47. package/src/session.js +8 -3
  48. package/src/umd/call.js +6 -0
  49. package/src/umd/lazy.js +98 -0
  50. package/src/umd/panel.js +6 -0
  51. package/src/updates.js +97 -34
  52. package/src/widget.js +385 -3376
@@ -0,0 +1,2531 @@
1
+ import { s as ke, m as $e, a as Se, c as ce, b as Le, d as Qe, R as be, h as P, n as Te, e as Ee, f as qe, g as ee, i as G, j as F, r as ze, v as Me, k as Ce, S as He, U as Ae, l as Ie, o as Re, p as De } from "./bugcatch-core.es.js";
2
+ const Ve = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5A1.5 1.5 0 0 1 4.5 7h2.2a1 1 0 0 0 .83-.45l.94-1.4A1 1 0 0 1 9.3 4.7h5.4a1 1 0 0 1 .83.45l.94 1.4a1 1 0 0 0 .83.45h2.2A1.5 1.5 0 0 1 21 8.5v9A1.5 1.5 0 0 1 19.5 19h-15A1.5 1.5 0 0 1 3 17.5z"/><circle cx="12" cy="13" r="3.4"/></svg>', je = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="12.5" height="12" rx="2.2"/><path d="M15.5 10.8l4.2-2.6a.6.6 0 0 1 .92.5v6.6a.6.6 0 0 1-.92.5l-4.2-2.6z"/></svg>', fe = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11.5l-7.6 7.6a4.6 4.6 0 0 1-6.5-6.5l8-8a3.2 3.2 0 0 1 4.5 4.5l-8 8a1.8 1.8 0 0 1-2.5-2.5l7.1-7.1"/></svg>', de = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2.5" width="6" height="11" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21M9 21h6"/></svg>', ge = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l16-8-6 16-2.6-6.2L4 12Z"/></svg>', Oe = '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="2"/></svg>', Ne = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5.5L8 12l6.5 6.5"/></svg>', ae = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.2 3.5h3l1.5 3.7-2 1.4a11.5 11.5 0 0 0 5.7 5.7l1.4-2 3.7 1.5v3a1.8 1.8 0 0 1-2 1.8A15.6 15.6 0 0 1 4.4 5.5a1.8 1.8 0 0 1 1.8-2z"/></svg>', Be = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12a8 8 0 1 1-2.6-5.9M20 4v4.5h-4.5"/></svg>', We = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 6l6 6-6 6"/></svg>', me = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.9l2.75 5.82 6.37.76-4.7 4.37 1.24 6.3L12 17.03l-5.66 3.12 1.24-6.3-4.7-4.37 6.37-.76z"/></svg>', Ye = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 13.5l2.6-7.2A1.5 1.5 0 0 1 7.5 5.3h9a1.5 1.5 0 0 1 1.4 1L20.5 13.5V18a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 18z"/><path d="M3.5 13.5h4.8l1.2 2.2h5l1.2-2.2h4.8"/></svg>', Pe = '<svg class="bc-brand" viewBox="0 0 254 60" fill="none" role="img" aria-label="sphoro BugCatch"><g fill="currentColor"><path transform="translate(0.00 40.00) scale(0.04000 -0.04000)" d="M276.00 -14.00Q179.00 -14.00 117.00 28.00Q55.00 70.00 42.00 148.00L158.00 178.00Q165.00 143.00 181.50 123.00Q198.00 103.00 222.50 94.50Q247.00 86.00 276.00 86.00Q320.00 86.00 341.00 101.50Q362.00 117.00 362.00 140.00Q362.00 163.00 342.00 175.50Q322.00 188.00 278.00 196.00L250.00 201.00Q198.00 211.00 155.00 228.50Q112.00 246.00 86.00 277.00Q60.00 308.00 60.00 357.00Q60.00 431.00 114.00 470.50Q168.00 510.00 256.00 510.00Q339.00 510.00 394.00 473.00Q449.00 436.00 466.00 376.00L349.00 340.00Q341.00 378.00 316.50 394.00Q292.00 410.00 256.00 410.00Q220.00 410.00 201.00 397.50Q182.00 385.00 182.00 363.00Q182.00 339.00 202.00 327.50Q222.00 316.00 256.00 310.00L284.00 305.00Q340.00 295.00 385.50 278.50Q431.00 262.00 457.50 231.50Q484.00 201.00 484.00 149.00Q484.00 71.00 427.50 28.50Q371.00 -14.00 276.00 -14.00Z"/><path transform="translate(19.56 40.00) scale(0.04000 -0.04000)" d="M70.00 -200.00V496.00H194.00V436.00H212.00Q229.00 465.00 265.00 487.50Q301.00 510.00 368.00 510.00Q428.00 510.00 479.00 480.50Q530.00 451.00 561.00 394.00Q592.00 337.00 592.00 256.00V240.00Q592.00 159.00 561.00 102.00Q530.00 45.00 479.00 15.50Q428.00 -14.00 368.00 -14.00Q323.00 -14.00 292.50 -3.50Q262.00 7.00 243.50 23.50Q225.00 40.00 214.00 57.00H196.00V-200.00ZM330.00 96.00Q389.00 96.00 427.50 133.50Q466.00 171.00 466.00 243.00V253.00Q466.00 325.00 427.00 362.50Q388.00 400.00 330.00 400.00Q272.00 400.00 233.00 362.50Q194.00 325.00 194.00 253.00V243.00Q194.00 171.00 233.00 133.50Q272.00 96.00 330.00 96.00Z"/><path transform="translate(43.68 40.00) scale(0.04000 -0.04000)" d="M70.00 0.00V700.00H196.00V435.00H214.00Q222.00 451.00 239.00 467.00Q256.00 483.00 284.50 493.50Q313.00 504.00 357.00 504.00Q415.00 504.00 458.50 477.50Q502.00 451.00 526.00 404.50Q550.00 358.00 550.00 296.00V0.00H424.00V286.00Q424.00 342.00 396.50 370.00Q369.00 398.00 318.00 398.00Q260.00 398.00 228.00 359.50Q196.00 321.00 196.00 252.00V0.00Z"/><path transform="translate(66.92 40.00) scale(0.04000 -0.04000)" d="M306.00 -14.00Q232.00 -14.00 173.00 16.00Q114.00 46.00 80.00 103.00Q46.00 160.00 46.00 240.00V256.00Q46.00 336.00 80.00 393.00Q114.00 450.00 173.00 480.00Q232.00 510.00 306.00 510.00Q380.00 510.00 439.00 480.00Q498.00 450.00 532.00 393.00Q566.00 336.00 566.00 256.00V240.00Q566.00 160.00 532.00 103.00Q498.00 46.00 439.00 16.00Q380.00 -14.00 306.00 -14.00ZM306.00 98.00Q364.00 98.00 402.00 135.50Q440.00 173.00 440.00 243.00V253.00Q440.00 323.00 402.50 360.50Q365.00 398.00 306.00 398.00Q248.00 398.00 210.00 360.50Q172.00 323.00 172.00 253.00V243.00Q172.00 173.00 210.00 135.50Q248.00 98.00 306.00 98.00Z"/><path transform="translate(90.00 40.00) scale(0.04000 -0.04000)" d="M70.00 0.00V496.00H194.00V440.00H212.00Q223.00 470.00 248.50 484.00Q274.00 498.00 308.00 498.00H368.00V386.00H306.00Q258.00 386.00 227.00 360.50Q196.00 335.00 196.00 282.00V0.00Z"/><path transform="translate(104.44 40.00) scale(0.04000 -0.04000)" d="M306.00 -14.00Q232.00 -14.00 173.00 16.00Q114.00 46.00 80.00 103.00Q46.00 160.00 46.00 240.00V256.00Q46.00 336.00 80.00 393.00Q114.00 450.00 173.00 480.00Q232.00 510.00 306.00 510.00Q380.00 510.00 439.00 480.00Q498.00 450.00 532.00 393.00Q566.00 336.00 566.00 256.00V240.00Q566.00 160.00 532.00 103.00Q498.00 46.00 439.00 16.00Q380.00 -14.00 306.00 -14.00ZM306.00 98.00Q364.00 98.00 402.00 135.50Q440.00 173.00 440.00 243.00V253.00Q440.00 323.00 402.50 360.50Q365.00 398.00 306.00 398.00Q248.00 398.00 210.00 360.50Q172.00 323.00 172.00 253.00V243.00Q172.00 173.00 210.00 135.50Q248.00 98.00 306.00 98.00Z"/></g><rect x="139.52" y="16" width="1.6" height="26" rx="0.8" fill="var(--bc-brand-mark, #5647ee)" opacity="0.4"/><g fill="var(--bc-brand-mark, #5647ee)"><path transform="translate(154.52 35.00) scale(0.01400 -0.01400)" d="M46.35 0.00V105.85H139.75V594.15H46.35V700.00H404.08Q467.38 700.00 514.79 678.41Q562.20 656.83 588.70 617.33Q615.20 577.83 615.20 523.35V513.70Q615.20 465.52 597.03 434.76Q578.85 404.00 553.91 387.24Q528.98 370.48 506.28 363.30V346.35Q528.98 340.35 555.24 323.50Q581.50 306.65 600.00 275.48Q618.50 244.30 618.50 194.48V184.48Q618.50 126.65 591.41 85.24Q564.33 43.83 516.83 21.91Q469.33 0.00 406.85 0.00ZM259.85 109.15H392.60Q441.20 109.15 469.80 132.60Q498.40 156.05 498.40 198.55V208.20Q498.40 250.70 470.21 274.15Q442.02 297.60 392.60 297.60H259.85ZM259.85 406.75H391.65Q437.55 406.75 466.32 430.11Q495.10 453.48 495.10 493.80V503.80Q495.10 544.95 466.74 567.90Q438.37 590.85 391.65 590.85H259.85Z"/><path transform="translate(167.00 35.00) scale(0.01400 -0.01400)" d="M335.30 -14.00Q251.22 -14.00 190.60 16.63Q129.97 47.25 97.56 104.96Q65.15 162.68 65.15 243.03V700.00H185.25V239.73Q185.25 170.48 224.03 131.99Q262.80 93.50 335.30 93.50Q407.80 93.50 446.57 131.99Q485.35 170.48 485.35 239.73V700.00H605.45V243.03Q605.45 162.68 573.13 104.96Q540.80 47.25 480.09 16.63Q419.38 -14.00 335.30 -14.00Z"/><path transform="translate(179.60 35.00) scale(0.01400 -0.01400)" d="M311.85 -14.00Q238.45 -14.00 179.69 18.50Q120.92 51.00 86.69 113.67Q52.45 176.35 52.45 266.87V433.13Q52.45 569.00 128.01 641.50Q203.57 714.00 332.35 714.00Q460.13 714.00 529.23 645.59Q598.33 577.17 598.33 460.87V456.70H479.87V464.52Q479.87 504.67 463.76 536.86Q447.65 569.05 414.99 587.77Q382.32 606.50 332.35 606.50Q257.63 606.50 215.09 560.70Q172.55 514.90 172.55 435.13V264.87Q172.55 185.93 215.09 138.89Q257.63 91.85 334.00 91.85Q410.02 91.85 445.12 132.83Q480.22 173.80 480.22 237.92V249.67H302.85V351.88H598.33V0.00H487.52V69.10H470.57Q462.70 50.87 446.06 31.44Q429.42 12.00 397.64 -1.00Q365.85 -14.00 311.85 -14.00Z"/><path transform="translate(192.05 35.00) scale(0.01400 -0.01400)" d="M332.35 -14.00Q203.57 -14.00 128.01 58.50Q52.45 131.00 52.45 266.87V433.13Q52.45 569.00 128.01 641.50Q203.57 714.00 332.35 714.00Q460.13 714.00 529.58 643.85Q599.03 573.70 599.03 451.57V445.75H480.57V455.22Q480.57 521.42 444.15 563.96Q407.72 606.50 332.35 606.50Q257.63 606.50 215.09 560.70Q172.55 514.90 172.55 435.13V264.87Q172.55 185.93 215.09 139.71Q257.63 93.50 332.35 93.50Q407.72 93.50 444.15 136.45Q480.57 179.40 480.57 244.78V262.25H599.03V248.43Q599.03 126.30 529.58 56.15Q460.13 -14.00 332.35 -14.00Z"/><path transform="translate(204.25 35.00) scale(0.01400 -0.01400)" d="M19.75 0.00 211.62 700.00H421.33L613.20 0.00H489.27L447.42 159.60H185.53L143.68 0.00ZM214.25 270.40H418.70L324.95 624.87H308.00Z"/><path transform="translate(216.31 35.00) scale(0.01400 -0.01400)" d="M233.95 0.00V590.85H24.70V700.00H563.30V590.85H354.05V0.00Z"/><path transform="translate(227.75 35.00) scale(0.01400 -0.01400)" d="M332.35 -14.00Q203.57 -14.00 128.01 58.50Q52.45 131.00 52.45 266.87V433.13Q52.45 569.00 128.01 641.50Q203.57 714.00 332.35 714.00Q460.13 714.00 529.58 643.85Q599.03 573.70 599.03 451.57V445.75H480.57V455.22Q480.57 521.42 444.15 563.96Q407.72 606.50 332.35 606.50Q257.63 606.50 215.09 560.70Q172.55 514.90 172.55 435.13V264.87Q172.55 185.93 215.09 139.71Q257.63 93.50 332.35 93.50Q407.72 93.50 444.15 136.45Q480.57 179.40 480.57 244.78V262.25H599.03V248.43Q599.03 126.30 529.58 56.15Q460.13 -14.00 332.35 -14.00Z"/><path transform="translate(239.95 35.00) scale(0.01400 -0.01400)" d="M69.50 0.00V700.00H189.60V405.58H466.40V700.00H586.50V0.00H466.40V296.42H189.60V0.00Z"/></g></svg>', Fe = `
3
+ @keyframes bc-fade-in { from { opacity: 0 } to { opacity: 1 } }
4
+ @keyframes bc-fade-out { to { opacity: 0 } }
5
+ @keyframes bc-panel-in { from { opacity: 0; transform: translateY(14px) scale(.965) } to { opacity: 1; transform: none } }
6
+ @keyframes bc-panel-out { to { opacity: 0; transform: translateY(10px) scale(.975) } }
7
+ @keyframes bc-sheet-in { from { transform: translateY(100%) } to { transform: none } }
8
+ @keyframes bc-sheet-out { to { transform: translateY(100%) } }
9
+ @keyframes bc-rise { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: none } }
10
+ @keyframes bc-pulse { 0%, 100% { opacity: 1; transform: scale(1) } 50% { opacity: .3; transform: scale(.78) } }
11
+ @keyframes bc-spin { to { transform: rotate(360deg) } }
12
+ @keyframes bc-pop { 0% { transform: scale(.5); opacity: 0 } 65% { transform: scale(1.1) } 100% { transform: scale(1); opacity: 1 } }
13
+ @keyframes bc-nudge { 25% { transform: translateX(-3px) } 75% { transform: translateX(3px) } }
14
+ @keyframes bc-typing { 0%, 60%, 100% { transform: translateY(0); opacity: .45 } 30% { transform: translateY(-3px); opacity: 1 } }
15
+ @keyframes bc-shimmer { from { background-position: 200% 0 } to { background-position: -200% 0 } }
16
+
17
+ /* ---------- scrim + panel ---------- */
18
+ /* Still modal — focus stays in the panel and a click outside closes it — but
19
+ the host's page only dims enough to say so. It is their product, and the
20
+ visitor is reporting something about what is on it. */
21
+ .scrim {
22
+ position: fixed; inset: 0; z-index: 2147483001;
23
+ background: rgba(14,14,16,.14);
24
+ animation: bc-fade-in 200ms ease both;
25
+ }
26
+ .scrim.closing { animation: bc-fade-out 170ms ease both; }
27
+
28
+ /* The panel belongs to the launcher, so it opens out of that corner rather
29
+ than in the middle of the screen. --bc-dock-h is measured at open time
30
+ because the dock's height depends on whether the launcher is showing.
31
+ A column: the header and footer stay put and only the middle scrolls. */
32
+ .panel {
33
+ position: fixed;
34
+ display: flex; flex-direction: column;
35
+ width: calc(100vw - 40px); max-width: 400px;
36
+ max-height: calc(100vh - var(--bc-dock-h, 84px) - 52px);
37
+ overflow: hidden;
38
+ background: var(--bc-bg); color: var(--bc-fg);
39
+ border-radius: var(--bc-r-xl); box-shadow: var(--bc-shadow);
40
+ animation: bc-panel-in 340ms var(--bc-ease) both;
41
+ }
42
+ /* A conversation wants a height of its own, so the thread fills the panel
43
+ rather than shrinking to its first two messages. */
44
+ .panel.is-chat { height: 640px; }
45
+ .scrim.at-right .panel { right: 20px; transform-origin: bottom right; }
46
+ .scrim.at-left .panel { left: 20px; transform-origin: bottom left; }
47
+ .scrim.at-bottom .panel { bottom: calc(20px + var(--bc-dock-h, 84px) + 12px); }
48
+ .scrim.at-top .panel { top: calc(20px + var(--bc-dock-h, 84px) + 12px); transform-origin: top right; }
49
+ .scrim.closing .panel { animation: bc-panel-out 170ms ease both; }
50
+
51
+ /* ---------- header ---------- */
52
+ .head {
53
+ flex: none; position: relative;
54
+ display: flex; align-items: center; gap: 11px;
55
+ padding: 14px 10px 13px var(--bc-pad);
56
+ background: var(--bc-bg);
57
+ border-bottom: 1px solid var(--bc-border);
58
+ }
59
+ .head .mark { width: 32px; }
60
+ /* The assistant asking its one follow-up wears the same orb, smaller. */
61
+ .avatar.orb { width: 26px; }
62
+ .head .titles { flex: 1; min-width: 0; }
63
+ .head h2 {
64
+ margin: 0 0 1px; font-size: var(--bc-fs-lg); font-weight: 600; letter-spacing: -.015em; line-height: 1.3;
65
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
66
+ }
67
+ .head p {
68
+ margin: 0; font-size: var(--bc-fs-sm); line-height: 1.4; color: var(--bc-muted);
69
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
70
+ }
71
+ .icon-btn {
72
+ flex: none; display: grid; place-items: center;
73
+ width: 32px; height: 32px; padding: 0; font-size: 16px;
74
+ border: none; border-radius: 999px; background: none; color: var(--bc-muted);
75
+ cursor: pointer; transition: background 160ms, color 160ms, transform 160ms var(--bc-ease);
76
+ }
77
+ .icon-btn:hover { background: color-mix(in srgb, var(--bc-fg) 7%, transparent); color: var(--bc-fg); }
78
+ .icon-btn:active { transform: scale(.92); }
79
+ .icon-btn.back { margin: 0 -6px 0 -8px; }
80
+ /* The theme button holds all three glyphs; the host's class picks one. */
81
+ .icon-btn.theme { font-size: 15px; margin-right: -4px; }
82
+ .icon-btn.theme > span { display: none; }
83
+ :host(.bc-light) .icon-btn.theme .m-light,
84
+ :host(.bc-dark) .icon-btn.theme .m-dark,
85
+ :host(.bc-auto) .icon-btn.theme .m-auto { display: block; }
86
+
87
+ .body {
88
+ flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
89
+ padding: 14px var(--bc-pad) 4px;
90
+ }
91
+
92
+ label { display: block; font-size: 13px; font-weight: 560; color: var(--bc-fg); margin-bottom: 6px; letter-spacing: -.005em; }
93
+ label .opt { font-weight: 450; color: var(--bc-faint); }
94
+ /* Fields are stone wells that turn white when you are in them, the way the
95
+ rest of the product's forms do. */
96
+ input[type=text], input[type=email], textarea {
97
+ width: 100%; padding: 10px 12px; font-size: var(--bc-fs-md); line-height: 1.45;
98
+ color: var(--bc-fg); background: var(--bc-stone);
99
+ border: 1px solid transparent; border-radius: var(--bc-r-md);
100
+ transition: border-color 160ms, box-shadow 160ms, background 160ms;
101
+ }
102
+ input[type=text]::placeholder, input[type=email]::placeholder, textarea::placeholder { color: var(--bc-faint); }
103
+ input[type=text]:hover, input[type=email]:hover, textarea:hover { border-color: var(--bc-border); }
104
+ input[type=text]:focus, input[type=email]:focus, textarea:focus {
105
+ outline: none; background: var(--bc-bg); border-color: var(--bc-accent); box-shadow: var(--bc-ring);
106
+ }
107
+ input.invalid, input.invalid:focus, textarea.invalid {
108
+ border-color: var(--bc-danger); animation: bc-nudge 240ms ease;
109
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--bc-danger) 16%, transparent);
110
+ }
111
+ .field-err {
112
+ display: flex; align-items: center; gap: 6px;
113
+ margin: 7px 0 0; font-size: var(--bc-fs-xs); font-weight: 550; color: var(--bc-danger);
114
+ animation: bc-rise 200ms var(--bc-ease) both;
115
+ }
116
+ .field-err svg { font-size: 13px; flex: none; }
117
+ textarea { min-height: 76px; resize: vertical; }
118
+ .field { margin-bottom: 14px; }
119
+ .field-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
120
+ .field-head .count { font-size: var(--bc-fs-xs); color: var(--bc-faint); font-variant-numeric: tabular-nums; }
121
+
122
+ /* The draft line: a quiet note, or a card offering to pick up where they left off. */
123
+ .compose > .note { font-size: var(--bc-fs-xs); color: var(--bc-faint); margin: -4px 0 10px; }
124
+ .compose > .note:empty { display: none; }
125
+ .compose > .note:has(button) {
126
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
127
+ margin: 0 0 14px; padding: 10px 12px; border-radius: var(--bc-r-md);
128
+ background: var(--bc-subtle); border: 1px solid var(--bc-border);
129
+ font-size: var(--bc-fs-sm); color: var(--bc-fg);
130
+ }
131
+ .compose > .note button {
132
+ padding: 5px 11px; font: inherit; font-size: var(--bc-fs-xs); font-weight: 600;
133
+ border-radius: 999px; cursor: pointer;
134
+ border: 1px solid var(--bc-accent); background: var(--bc-accent); color: var(--bc-on-accent);
135
+ }
136
+ .compose > .note button[data-discard] { background: var(--bc-bg); color: var(--bc-fg); border-color: var(--bc-border); }
137
+
138
+ /* ---------- what is already known ----------
139
+ A help surface in front of the form, not a step in it. Everything here is
140
+ dismissible by ignoring it: the reporter can read what is already known and
141
+ leave, tap "this is happening to me too" and leave, or type straight past all
142
+ of it and file the report they came to file. Nothing is ever in their way,
143
+ because a reporter who feels handled rather than helped stops reporting. */
144
+ /* One small eyebrow style for every group above the form. */
145
+ .known .known-h, .picks .picks-h {
146
+ margin: 0 0 7px; font-size: 11px; font-weight: 600; letter-spacing: .07em;
147
+ text-transform: uppercase; color: var(--bc-faint);
148
+ }
149
+ .known { margin-bottom: 14px; }
150
+ /* Hairline rows on the panel's own white, so they read as a list to scan
151
+ rather than more coloured cards to wade through. */
152
+ .known .issue {
153
+ display: flex; align-items: center; gap: 10px;
154
+ padding: 8px 8px 8px 12px; margin-bottom: 6px;
155
+ background: var(--bc-bg); border: 1px solid var(--bc-border); border-radius: var(--bc-r-md);
156
+ animation: bc-rise 240ms var(--bc-ease) both;
157
+ }
158
+ .known .issue.is-joined { background: color-mix(in srgb, var(--bc-ok) 6%, var(--bc-bg)); }
159
+ .known .issue .who { flex: 1; min-width: 0; }
160
+ .known .issue .t {
161
+ display: block; font-size: 13px; font-weight: 560; line-height: 1.4;
162
+ color: var(--bc-fg); overflow-wrap: anywhere;
163
+ }
164
+ .known .issue .meta {
165
+ display: block; margin-top: 2px; font-size: var(--bc-fs-xs); line-height: 1.45; color: var(--bc-muted);
166
+ }
167
+ .known .issue .join {
168
+ flex: none; height: 30px; padding: 0 12px; font: inherit; font-size: 12px; font-weight: 560;
169
+ color: var(--bc-fg); background: var(--bc-stone);
170
+ border: 0; border-radius: 999px; cursor: pointer;
171
+ transition: background 160ms, color 160ms;
172
+ }
173
+ .known .issue .join:hover { background: color-mix(in srgb, var(--bc-fg) 12%, var(--bc-stone)); }
174
+ .known .issue .join:disabled { color: var(--bc-ok); background: transparent; cursor: default; opacity: 1; }
175
+ .known .ask { display: flex; gap: 8px; margin: 2px 0 8px; }
176
+ .known .ask input { flex: 1; min-width: 0; }
177
+ .known .ask button { flex: none; }
178
+ .known .err { margin: 6px 0 0; font-size: 12px; line-height: 1.45; color: var(--bc-danger); }
179
+
180
+ /* ---------- questions the assistant answers by itself ---------- */
181
+ .picks { margin-bottom: 14px; }
182
+ .picks .picks-grid { display: flex; flex-wrap: wrap; gap: 6px; }
183
+ .picks .pick {
184
+ display: inline-flex; align-items: center; max-width: 100%;
185
+ min-height: 32px; padding: 6px 13px; text-align: left; cursor: pointer; font: inherit;
186
+ color: var(--bc-fg); background: var(--bc-stone);
187
+ border: 1px solid transparent; border-radius: 999px;
188
+ transition: background 160ms, border-color 160ms, color 160ms, transform 160ms var(--bc-ease);
189
+ animation: bc-rise 240ms var(--bc-ease) both;
190
+ }
191
+ .picks .pick:hover { background: color-mix(in srgb, var(--bc-fg) 10%, var(--bc-stone)); }
192
+ .picks .pick:active { transform: scale(.97); }
193
+ .picks .pick[aria-pressed="true"] { background: var(--bc-accent); color: var(--bc-on-accent); }
194
+ .picks .pick .t { font-size: 13px; font-weight: 520; line-height: 1.35; overflow-wrap: anywhere; }
195
+ .picks .picks-note {
196
+ display: flex; gap: 6px; align-items: flex-start; margin: 8px 0 0;
197
+ font-size: 12px; line-height: 1.45; color: var(--bc-muted);
198
+ }
199
+ .picks .picks-note svg { flex: none; margin-top: 2px; color: var(--bc-ok); }
200
+
201
+ /* ---------- who is reporting ----------
202
+ A strip above the form rather than a screen in front of it: where signing in
203
+ is optional this is a line the reporter may ignore, and it should read as
204
+ one. Where it is required the composer below is not rendered at all and this
205
+ strip is the whole panel. */
206
+ .whoami {
207
+ display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
208
+ margin-bottom: 14px; padding: 9px 12px;
209
+ font-size: var(--bc-fs-sm); line-height: 1.45; color: var(--bc-muted);
210
+ background: var(--bc-subtle); border: 1px solid var(--bc-border); border-radius: var(--bc-r-md);
211
+ }
212
+ .whoami.asking { display: block; padding: 14px; }
213
+ .whoami.asking .row input { background: var(--bc-bg); border-color: var(--bc-border); }
214
+ .whoami .who { flex: 1; min-width: 0; }
215
+ .whoami .who strong { color: var(--bc-fg); font-weight: 620; word-break: break-all; }
216
+ .link {
217
+ padding: 0; font: inherit; font-size: var(--bc-fs-sm); font-weight: 560; line-height: 1.45;
218
+ color: var(--bc-fg); background: none; border: none; cursor: pointer; border-radius: 4px;
219
+ text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--bc-fg) 30%, transparent);
220
+ text-underline-offset: 3px;
221
+ }
222
+ .link:hover { text-decoration-color: currentColor; }
223
+ .link:disabled { color: var(--bc-faint); cursor: default; text-decoration: none; }
224
+ .whoami .why { margin: 0 0 10px; font-size: var(--bc-fs-sm); line-height: 1.5; color: var(--bc-muted); }
225
+ .whoami .why strong { color: var(--bc-fg); font-weight: 620; word-break: break-all; }
226
+ .whoami .row { display: flex; gap: 8px; align-items: stretch; }
227
+ .whoami .row input { flex: 1; min-width: 0; }
228
+ .whoami .row button { flex: none; }
229
+ .whoami .after { display: flex; gap: 14px; align-items: center; margin-top: 9px; }
230
+ .whoami .err { color: var(--bc-danger); }
231
+ .whoami .code { letter-spacing: .28em; font-variant-numeric: tabular-nums; }
232
+
233
+ /* ---------- which record is this about ----------
234
+ Drawn only after Send has been pressed and only when what they wrote did not
235
+ already answer it, so most people never see this at all. Presented as the
236
+ assistant asking one friendly follow-up, not as a form field that appeared. */
237
+ .refs {
238
+ position: relative;
239
+ margin-bottom: 14px; padding: 14px;
240
+ font-size: var(--bc-fs-sm); line-height: 1.45; color: var(--bc-muted);
241
+ background: var(--bc-subtle);
242
+ border: 1px solid var(--bc-border); border-radius: var(--bc-r-lg);
243
+ animation: bc-item-in 280ms var(--bc-ease) both;
244
+ }
245
+ .refs .ask-head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 11px; }
246
+ .refs .ask-head strong { display: block; font-size: 13.5px; font-weight: 600; color: var(--bc-fg); margin-bottom: 1px; }
247
+ .refs .why { margin: 0; font-size: var(--bc-fs-sm); line-height: 1.5; color: var(--bc-muted); }
248
+ .refs .row { display: flex; gap: 8px; align-items: stretch; }
249
+ .refs .row input { flex: 1; min-width: 0; background: var(--bc-bg); border-color: var(--bc-border); }
250
+ .refs select {
251
+ flex: none; max-width: 45%; padding: 10px 10px; font-size: 13px;
252
+ color: var(--bc-fg); background: var(--bc-bg);
253
+ border: 1px solid var(--bc-border); border-radius: var(--bc-r-md);
254
+ }
255
+ .refs select:focus { outline: none; border-color: var(--bc-accent); box-shadow: var(--bc-ring); }
256
+ .refs .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 9px; }
257
+ .refs .chips button {
258
+ padding: 5px 11px; font: inherit; font-size: 12px; font-weight: 600; border-radius: 999px; cursor: pointer;
259
+ color: var(--bc-muted); background: var(--bc-bg); border: 1px solid var(--bc-border);
260
+ transition: background 160ms, color 160ms, border-color 160ms;
261
+ }
262
+ .refs .chips button[aria-pressed="true"] { color: var(--bc-on-accent); background: var(--bc-accent); border-color: var(--bc-accent); }
263
+ .refs .hint { margin: 8px 0 0; font-size: 12px; line-height: 1.45; color: var(--bc-muted); }
264
+ .refs .after { display: flex; gap: 12px; align-items: center; margin-top: 9px; }
265
+ .refs .err { color: var(--bc-danger); font-weight: 550; }
266
+ .refs .link { margin-top: 10px; font-size: 12px; color: var(--bc-muted); }
267
+ .refs .link:hover { color: var(--bc-fg); }
268
+ .refs .chips.cands button { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 550; }
269
+ /* "We read your ID" — under the description, while they type. */
270
+ .ref-found {
271
+ display: flex; align-items: center; gap: 6px;
272
+ margin: -6px 0 14px; font-size: 12px; line-height: 1.45; color: var(--bc-muted);
273
+ }
274
+ .ref-found svg { width: 14px; height: 14px; flex: none; color: var(--bc-ok); }
275
+ .ref-found b { font-weight: 600; color: var(--bc-fg); }
276
+
277
+ /* ---------- severity ----------
278
+ The segmented control from the product: a stone track, the choice lifted out
279
+ of it on white. */
280
+ .seg { display: grid; grid-template-columns: repeat(4, 1fr); gap: 2px; padding: 3px; background: var(--bc-stone); border-radius: 999px; }
281
+ .seg button {
282
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
283
+ height: 30px; padding: 0 4px; font: inherit; font-size: var(--bc-fs-sm); font-weight: 520; line-height: 1;
284
+ color: var(--bc-muted); background: none; border: none; border-radius: 999px;
285
+ cursor: pointer; transition: background 180ms var(--bc-ease), color 180ms, box-shadow 180ms;
286
+ }
287
+ .seg button::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--sev, var(--bc-faint)); opacity: .5; transition: opacity 180ms; }
288
+ .seg button[data-prio="LOW"] { --sev: var(--bc-ok); }
289
+ .seg button[data-prio="MEDIUM"] { --sev: var(--bc-info); }
290
+ .seg button[data-prio="HIGH"] { --sev: var(--bc-warn); }
291
+ .seg button[data-prio="CRITICAL"] { --sev: var(--bc-danger); }
292
+ .seg button:hover { color: var(--bc-fg); }
293
+ .seg button[aria-pressed=true] {
294
+ background: var(--bc-raised); color: var(--bc-fg); font-weight: 600;
295
+ box-shadow: 0 1px 2px rgba(20,18,14,.10), 0 0 0 .5px rgba(20,18,14,.08);
296
+ }
297
+ .seg button[aria-pressed=true]::before { opacity: 1; }
298
+ .seg button:focus-visible { outline-offset: -2px; }
299
+
300
+ /* ---------- attachments ----------
301
+ Three ring pills in a row: optional, so they take one line, not a band of
302
+ dashed drop zones. */
303
+ .tools { display: flex; flex-wrap: wrap; gap: 6px; }
304
+ .tool {
305
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
306
+ height: 32px; padding: 0 11px 0 9px; font: inherit; font-size: 12.5px; font-weight: 520; line-height: 1; white-space: nowrap;
307
+ color: var(--bc-fg); background: var(--bc-bg);
308
+ border: 1px solid var(--bc-border); border-radius: 999px; cursor: pointer;
309
+ transition: border-color 160ms, background 160ms, color 160ms, transform 160ms var(--bc-ease);
310
+ }
311
+ .tool svg { font-size: 15px; color: var(--bc-muted); transition: color 160ms; }
312
+ .tool:hover:not(:disabled) { background: var(--bc-subtle); border-color: color-mix(in srgb, var(--bc-fg) 20%, var(--bc-border)); }
313
+ .tool:hover:not(:disabled) svg { color: var(--bc-fg); }
314
+ .tool:active:not(:disabled) { transform: scale(.97); }
315
+ .tool:disabled { opacity: .45; cursor: not-allowed; }
316
+ .attach-hint { margin: 8px 0 0; font-size: var(--bc-fs-xs); color: var(--bc-faint); }
317
+
318
+ .thumbs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
319
+ .thumbs:empty { display: none; }
320
+ .thumb {
321
+ position: relative; width: 76px; height: 76px; flex: none;
322
+ border-radius: var(--bc-r-md); overflow: hidden;
323
+ border: 1px solid var(--bc-border); background: var(--bc-subtle);
324
+ box-shadow: var(--bc-shadow-sm);
325
+ animation: bc-item-in 300ms var(--bc-ease) both;
326
+ }
327
+ .thumb img, .thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }
328
+ .thumb .kind {
329
+ position: absolute; left: 4px; bottom: 4px;
330
+ display: inline-flex; align-items: center; gap: 3px;
331
+ padding: 2px 5px; border-radius: 5px;
332
+ background: rgba(10,12,16,.72); color: #fff; font-size: 9.5px; font-weight: 600; line-height: 1.3;
333
+ }
334
+ .thumb .drop {
335
+ position: absolute; top: 4px; right: 4px;
336
+ width: 20px; height: 20px; font-size: 11px; border-radius: 50%;
337
+ background: rgba(10,12,16,.7); color: #fff; border: none;
338
+ display: grid; place-items: center; cursor: pointer;
339
+ opacity: 0; transition: opacity 160ms, background 160ms;
340
+ }
341
+ .thumb:hover .drop, .thumb:focus-within .drop { opacity: 1; }
342
+ .thumb .drop:hover { background: var(--bc-danger); }
343
+ .thumb .veil {
344
+ position: absolute; inset: 0; display: grid; place-items: center; gap: 6px;
345
+ background: rgba(10,12,16,.62); color: #fff; font-size: 10.5px; font-weight: 600;
346
+ align-content: center;
347
+ }
348
+ .thumb .veil.err { background: color-mix(in srgb, var(--bc-danger) 82%, transparent); }
349
+ .spinner {
350
+ width: 17px; height: 17px; border-radius: 50%;
351
+ border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
352
+ animation: bc-spin 720ms linear infinite;
353
+ }
354
+ .bar { width: 46px; height: 3px; border-radius: 2px; background: rgba(255,255,255,.3); overflow: hidden; }
355
+ .bar i { display: block; height: 100%; width: 0; background: #fff; transition: width 180ms linear; }
356
+ .veil button {
357
+ background: none; border: none; color: #fff; font-size: 14px; padding: 2px;
358
+ cursor: pointer; display: grid; place-items: center;
359
+ }
360
+
361
+ /* ---------- footer ---------- */
362
+ .foot {
363
+ flex: none;
364
+ display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
365
+ padding: 12px 14px 12px var(--bc-pad); border-top: 1px solid var(--bc-border);
366
+ background: var(--bc-bg);
367
+ }
368
+ .foot .spacer { flex: 1; }
369
+ .foot .note { flex-basis: 100%; order: -1; font-size: var(--bc-fs-xs); color: var(--bc-muted); }
370
+ .foot .note:empty { display: none; }
371
+ /* Quiet, but still legible: this sits in someone else's product, so it identifies
372
+ where the report goes without competing with their interface. Quiet is bought
373
+ with colour (--bc-brand-ink sits a step below body text) rather than with a
374
+ blanket opacity — the BUGCATCH half is only about 3px of cap height at this
375
+ size, and dimming it on top of that left it as a smudge in both themes. */
376
+ .powered {
377
+ flex: none; display: flex; align-items: center; justify-content: center; gap: 6px;
378
+ padding: 0 var(--bc-pad) 10px; font-size: 10.5px; color: var(--bc-faint);
379
+ background: var(--bc-bg);
380
+ }
381
+ .brand-credit { display: flex; align-items: center; transition: color 0.15s; border-radius: 4px; }
382
+ .bc-brand { height: 15px; width: auto; display: block; color: var(--bc-brand-ink); }
383
+ .brand-credit:hover .bc-brand { color: var(--bc-fg); }
384
+ /* The product's two pills: ink for the one thing to do, a white ring for the
385
+ alternative. Flat — no glow, no lift. */
386
+ button.ghost, button.primary {
387
+ display: inline-flex; align-items: center; justify-content: center; gap: 7px;
388
+ height: 38px; padding: 0 15px; font: inherit; font-size: 13.5px; font-weight: 560; line-height: 1;
389
+ border-radius: 999px; cursor: pointer; white-space: nowrap;
390
+ transition: transform 160ms var(--bc-ease), filter 160ms, background 160ms, border-color 160ms, color 160ms, box-shadow 160ms;
391
+ }
392
+ button.ghost { background: var(--bc-bg); border: 0; color: var(--bc-fg); box-shadow: 0 0 0 1px var(--bc-border), 0 1px 2px rgba(20,18,14,.05); }
393
+ button.ghost:hover:not(:disabled) { background: var(--bc-subtle); }
394
+ button.ghost svg, button.primary svg { font-size: 15px; }
395
+ button.primary {
396
+ padding: 0 18px;
397
+ background: var(--bc-accent); border: 0; color: var(--bc-on-accent);
398
+ box-shadow: 0 1px 2px rgba(0,0,0,.14);
399
+ }
400
+ button.primary:hover:not(:disabled) { filter: brightness(1.12) contrast(.96); }
401
+ button.primary:active:not(:disabled) { transform: scale(.98); }
402
+ button:disabled { opacity: .45; cursor: not-allowed; }
403
+ button.primary:disabled { box-shadow: none; }
404
+ button.primary .spinner { border-color: color-mix(in srgb, var(--bc-on-accent) 40%, transparent); border-top-color: var(--bc-on-accent); width: 14px; height: 14px; }
405
+ button.quiet {
406
+ padding: 0 4px; height: 38px; font: inherit; font-size: 13px; font-weight: 520;
407
+ border: none; background: none; color: var(--bc-muted); cursor: pointer; border-radius: 8px;
408
+ text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px;
409
+ }
410
+ button.quiet:hover { color: var(--bc-fg); text-decoration-color: color-mix(in srgb, var(--bc-fg) 35%, transparent); }
411
+
412
+ /* ---------- your reports ---------- */
413
+ .history {
414
+ flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
415
+ padding: 8px 12px 14px;
416
+ }
417
+ /* On the conversation screen the inner thread does the scrolling, so this
418
+ wrapper must not — two nested scrollers means the composer drifts out of
419
+ reach exactly when someone reaches for it. */
420
+ .history.chatting {
421
+ overflow: hidden; display: flex; flex-direction: column; padding: 0;
422
+ }
423
+ .history .row {
424
+ display: flex; align-items: center; gap: 12px;
425
+ padding: 12px 10px; border-bottom: 1px solid var(--bc-border);
426
+ }
427
+ .history .row:last-child { border-bottom: 0; }
428
+ .history .who { flex: 1; min-width: 0; }
429
+ .history .ttl {
430
+ display: block; font-size: 13.5px; font-weight: 560; color: var(--bc-fg);
431
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
432
+ }
433
+ .history .sub { display: block; margin-top: 3px; font-size: var(--bc-fs-xs); color: var(--bc-faint); }
434
+ .history .row-ico {
435
+ flex: none; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px;
436
+ background: var(--bc-stone); color: var(--bc-muted); font-size: 16px;
437
+ }
438
+ .history .chev { flex: none; color: var(--bc-faint); font-size: 15px; transition: transform 160ms var(--bc-ease), color 160ms; }
439
+ .empty {
440
+ display: flex; flex-direction: column; align-items: center; gap: 10px;
441
+ padding: 40px 16px; text-align: center; font-size: var(--bc-fs-sm); color: var(--bc-muted);
442
+ }
443
+ .empty .empty-ico {
444
+ display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%;
445
+ background: var(--bc-stone); color: var(--bc-fg); font-size: 20px;
446
+ }
447
+ .skel {
448
+ display: block; height: 12px; border-radius: 6px; margin: 6px 0;
449
+ background: linear-gradient(90deg, var(--bc-subtle) 25%, color-mix(in srgb, var(--bc-fg) 8%, var(--bc-subtle)) 50%, var(--bc-subtle) 75%);
450
+ background-size: 200% 100%; animation: bc-shimmer 1.4s linear infinite;
451
+ }
452
+ /* A history row is a button now — it opens the report — so the UA styling has
453
+ to go, and the hit area has to read as one. */
454
+ .history button.row {
455
+ width: 100%; background: none; border: 0; margin-bottom: 2px;
456
+ font: inherit; color: inherit; text-align: left; cursor: pointer;
457
+ border-radius: var(--bc-r-md); transition: background 160ms;
458
+ animation: bc-rise 260ms var(--bc-ease) both;
459
+ }
460
+ .history button.row:hover { background: var(--bc-subtle); }
461
+ .history button.row:hover .chev { transform: translateX(2px); color: var(--bc-fg); }
462
+ .history button.row:focus-visible { outline-offset: -2px; }
463
+
464
+ /* ---------- one report, and what happened to it ---------- */
465
+ .report-head {
466
+ flex: none;
467
+ display: grid; grid-template-columns: 1fr auto; gap: 2px 10px; align-items: center;
468
+ padding: 10px var(--bc-pad); border-bottom: 1px solid var(--bc-border);
469
+ background: var(--bc-bg);
470
+ }
471
+ .report-head .ttl { font-size: 13.5px; font-weight: 600; color: var(--bc-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
472
+ .report-head .sub { font-size: var(--bc-fs-xs); color: var(--bc-faint); }
473
+ .report-head .pill { grid-row: 1 / span 2; grid-column: 2; align-self: center; }
474
+
475
+ /* The thread scrolls; the composer under it does not. Both live inside the
476
+ panel, which stops scrolling as a whole on this screen — a composer that
477
+ scrolls away is a composer people think isn't there. */
478
+ .thread {
479
+ flex: 1 1 auto; min-height: 140px;
480
+ display: flex; flex-direction: column; gap: 4px;
481
+ padding: 14px var(--bc-pad) 12px; overflow-y: auto; overscroll-behavior: contain;
482
+ }
483
+
484
+ /* The day a run of messages happened on, as a centred label on a rule. */
485
+ .day-sep {
486
+ display: flex; align-items: center; gap: 10px; margin: 10px 0 6px;
487
+ font-size: 11px; font-weight: 620; letter-spacing: .02em; color: var(--bc-faint);
488
+ }
489
+ .day-sep::before, .day-sep::after { content: ''; flex: 1; height: 1px; background: var(--bc-border); }
490
+
491
+ /* A status change is not a message and is not drawn as one: no side, no
492
+ bubble, no author. Centred and quiet, the way every chat marks the things
493
+ that happened rather than the things that were said. */
494
+ .sys {
495
+ align-self: center; max-width: 90%; text-align: center; margin: 6px 0;
496
+ padding: 4px 10px; border-radius: 999px; background: var(--bc-subtle);
497
+ font-size: var(--bc-fs-xs); line-height: 1.45; color: var(--bc-muted);
498
+ animation: bc-fade-in 240ms ease both;
499
+ }
500
+ .sys .dot {
501
+ display: inline-block; width: 6px; height: 6px; margin-right: 6px;
502
+ border-radius: 50%; background: var(--bc-muted); vertical-align: middle;
503
+ }
504
+ .sys .t { color: var(--bc-faint); }
505
+ .sys.k-status .dot { background: var(--bc-info); }
506
+ .sys.k-severity .dot { background: var(--bc-warn); }
507
+ .sys.k-assignment .dot { background: var(--bc-brand-mark); }
508
+ .sys.k-filed .dot { background: var(--bc-faint); }
509
+
510
+ /* ---------- the conversation ----------
511
+
512
+ A status change and a message are different things and are drawn as such: a
513
+ change is a line on the rail above, a message is a bubble with a side. The
514
+ reporter's own words sit right and accented, the other side's left beside an
515
+ avatar that says who it is — the arrangement every chat has taught people to
516
+ read without a legend. */
517
+ .msg-row, .typing-row {
518
+ display: grid; grid-template-columns: 28px minmax(0, 1fr); column-gap: 8px;
519
+ max-width: 88%; margin-top: 10px;
520
+ animation: bc-rise 260ms var(--bc-ease) both;
521
+ }
522
+ .msg-row.cont { margin-top: 3px; }
523
+ .msg-row > *, .typing-row > * { grid-column: 2; }
524
+ /* Fixed rows, so the avatar lines up with the bubble whatever else the
525
+ message carries (a name above it, answers or a time below it). */
526
+ .msg-row > .msg-who, .typing-row > .msg-who { grid-row: 1; }
527
+ .msg-row > .bubble, .typing-row > .bubble { grid-row: 2; }
528
+ .msg-row > .quick { grid-row: 3; }
529
+ .msg-row > .msg-when { grid-row: 4; }
530
+ .msg-row > .av, .typing-row > .av { grid-column: 1; grid-row: 2; align-self: end; }
531
+ /* In a run of messages the avatar sits by the last bubble and the time under
532
+ it, the way a chat marks where one turn ends. */
533
+ .msg-row:has(+ .msg-row.cont) > .av { visibility: hidden; }
534
+ .msg-row:has(+ .msg-row.cont) > .msg-when { display: none; }
535
+ .msg-row.theirs, .typing-row { align-self: flex-start; }
536
+ .msg-row.mine { align-self: flex-end; grid-template-columns: minmax(0, 1fr); justify-items: end; }
537
+ .msg-row.mine > * { grid-column: 1; }
538
+ .av {
539
+ display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%;
540
+ font-size: 11px; font-weight: 650; letter-spacing: .02em;
541
+ background: var(--bc-stone); color: var(--bc-fg);
542
+ }
543
+ .av.bot { width: 28px; }
544
+ .msg-who { font-size: var(--bc-fs-xs); font-weight: 560; color: var(--bc-muted); padding: 0 4px 4px; }
545
+ .msg-who.auto { color: var(--bc-fg); display: inline-flex; align-items: center; gap: 5px; }
546
+ .msg-who .tag {
547
+ padding: 1px 6px; border-radius: 999px; font-size: 9.5px; font-weight: 650; letter-spacing: .05em;
548
+ text-transform: uppercase; background: var(--bc-stone); color: var(--bc-muted);
549
+ }
550
+ .bubble {
551
+ justify-self: start; max-width: 100%;
552
+ padding: 9px 13px; border-radius: 18px; font-size: 13.5px; line-height: 1.5;
553
+ background: var(--bc-stone); color: var(--bc-fg);
554
+ border: 1px solid transparent;
555
+ border-bottom-left-radius: 6px;
556
+ }
557
+ /* pre-wrap belongs to the text, not the box — on the box it would also render
558
+ the markup's own indentation. */
559
+ .msg-text { display: block; white-space: pre-wrap; overflow-wrap: anywhere; }
560
+ .msg-row.mine .bubble {
561
+ justify-self: end;
562
+ background: var(--bc-accent); border-color: var(--bc-accent); color: var(--bc-on-accent);
563
+ border-bottom-left-radius: 18px; border-bottom-right-radius: 6px;
564
+ }
565
+ .msg-row.cont.theirs .bubble { border-top-left-radius: 6px; }
566
+ .msg-row.cont.mine .bubble { border-top-right-radius: 6px; }
567
+ /* A reply nobody wrote: drawn on white with a hairline, beside the orb and
568
+ labelled "Assistant" above it, so it reads the same to someone who cannot
569
+ see the difference at all. */
570
+ .msg-row.auto .bubble { background: var(--bc-bg); border-color: var(--bc-border); }
571
+ /* An attachment-only message has no text to wrap, so the bubble would be an
572
+ empty coloured box around it. */
573
+ .bubble.silent { padding: 0; background: none !important; border: 0; }
574
+ .msg-when { font-size: 10.5px; color: var(--bc-faint); padding: 4px 4px 0; }
575
+ .msg-row.pending { opacity: .6; }
576
+ .msg-row.pending .msg-when::after { content: ' · Sending…'; }
577
+ .msg-row.failed .bubble { border: 1px solid var(--bc-danger); }
578
+ .msg-row.failed .msg-when { color: var(--bc-danger); }
579
+ .msg-row.failed .msg-when::after { content: ' · Not sent'; }
580
+
581
+ /* The assistant, thinking. Three dots in a bubble where its next message will be. */
582
+ .typing { display: inline-flex; align-items: center; gap: 4px; padding: 12px 14px; justify-self: start; }
583
+ .typing i {
584
+ width: 6px; height: 6px; border-radius: 50%; background: var(--bc-muted); opacity: .45;
585
+ animation: bc-typing 1.2s ease-in-out infinite;
586
+ }
587
+ .typing i:nth-child(2) { animation-delay: .15s; }
588
+ .typing i:nth-child(3) { animation-delay: .3s; }
589
+ .typing-row .msg-when { padding-top: 4px; }
590
+
591
+ .att { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
592
+ .bubble.silent .att { margin-top: 0; }
593
+ .att img {
594
+ display: block; max-width: 100%; max-height: 190px; border-radius: var(--bc-r-md);
595
+ border: 1px solid var(--bc-border); object-fit: cover; cursor: zoom-in;
596
+ }
597
+ .att audio { width: 232px; max-width: 100%; height: 34px; }
598
+ .att video { max-width: 100%; max-height: 190px; border-radius: var(--bc-r-md); }
599
+ .att .file {
600
+ display: flex; align-items: center; gap: 8px; text-decoration: none;
601
+ padding: 8px 10px; border-radius: 10px; font-size: 12px;
602
+ background: var(--bc-elevated); border: 1px solid var(--bc-border); color: var(--bc-fg);
603
+ }
604
+ .att .file:hover { border-color: var(--bc-accent); }
605
+ .att .file .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
606
+ .att .file .sz { flex: none; color: var(--bc-faint); font-size: 11px; }
607
+
608
+ /* ---------- the assistant's live card ----------
609
+ A status header pinned between the report and the conversation: where the
610
+ investigation is, as words and as a stepper. It does not scroll with the
611
+ thread, because "is anything happening?" is the question on screen the
612
+ whole time. */
613
+ .assistant {
614
+ flex: none; position: relative; overflow: hidden;
615
+ margin: 10px 12px 0; padding: 12px 14px 12px;
616
+ border-radius: var(--bc-r-lg); border: 1px solid var(--bc-border);
617
+ background: var(--bc-subtle);
618
+ transition: background 400ms ease, border-color 400ms ease;
619
+ }
620
+ .assistant.resolved { background: color-mix(in srgb, var(--bc-ok) 7%, var(--bc-bg)); border-color: color-mix(in srgb, var(--bc-ok) 30%, var(--bc-border)); }
621
+ /* On a call: the call's own card is what they are watching. This one states
622
+ what is happening and stops moving — no breathing orb, no travelling bar. */
623
+ .assistant.calling { background: var(--bc-bg); }
624
+ .assistant.calling .as-progress i { transition: none; }
625
+ .as-body.entering { animation: bc-rise 360ms var(--bc-ease) both; }
626
+ .as-top { display: flex; gap: 11px; align-items: center; }
627
+ /* The orb, while the assistant is at work; a plain disc with a glyph once it
628
+ is on a call or has finished. */
629
+ .as-orb {
630
+ flex: none; width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center;
631
+ color: var(--bc-on-accent); font-size: 15px; background: var(--bc-accent);
632
+ }
633
+ .as-orb.orb { background: var(--bc-o2); }
634
+ .assistant.analyzing .as-orb.orb { animation: as-breathe 2.4s ease-in-out infinite; }
635
+ @keyframes as-breathe { 50% { transform: scale(.88); } }
636
+ .assistant.resolved .as-orb { background: var(--bc-ok); color: #fff; }
637
+ .assistant.reviewing .as-orb.orb { filter: saturate(.35); }
638
+ .as-text { min-width: 0; flex: 1; }
639
+ .as-title { font-weight: 600; font-size: 13.5px; letter-spacing: -.01em; color: var(--bc-fg); }
640
+ .as-detail { font-size: 12px; line-height: 1.45; color: var(--bc-muted); margin-top: 2px; }
641
+ .as-progress {
642
+ height: 3px; margin: 12px 0 0; border-radius: 999px; overflow: hidden;
643
+ background: color-mix(in srgb, var(--bc-fg) 9%, transparent);
644
+ }
645
+ .as-progress i {
646
+ display: block; height: 100%; border-radius: inherit; background: var(--bc-accent);
647
+ transition: width 700ms var(--bc-ease);
648
+ }
649
+ .assistant.resolved .as-progress i { background: var(--bc-ok); }
650
+ .assistant.analyzing .as-progress i {
651
+ background: linear-gradient(90deg, var(--bc-accent) 0%, color-mix(in srgb, var(--bc-accent) 35%, var(--bc-bg)) 50%, var(--bc-accent) 100%);
652
+ background-size: 200% 100%; animation: bc-shimmer 1.6s linear infinite;
653
+ }
654
+ /* Four short labels under the bar. Allowed to wrap rather than be cut: at this
655
+ width "Report received" ellipsised to "Report recei…". */
656
+ .as-steps {
657
+ list-style: none; display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px 6px;
658
+ margin: 8px 0 0; padding: 0; font-size: 10.5px; line-height: 1.3; color: var(--bc-faint);
659
+ }
660
+ .as-steps li { display: flex; align-items: flex-start; gap: 5px; min-width: 0; }
661
+ .as-steps li .lbl { min-width: 0; overflow-wrap: anywhere; }
662
+ .as-steps li .dot { flex: none; width: 7px; height: 7px; margin-top: 3px; border-radius: 50%; border: 1.5px solid currentColor; }
663
+ .as-steps li.done { color: var(--bc-muted); }
664
+ .as-steps li.done .dot { background: var(--bc-accent); border-color: var(--bc-accent); }
665
+ .assistant.resolved .as-steps li.done .dot { background: var(--bc-ok); border-color: var(--bc-ok); }
666
+ .as-steps li.active { color: var(--bc-fg); font-weight: 600; }
667
+ .as-steps li.active .dot { border-color: var(--bc-accent); background: var(--bc-bg); animation: as-pulse 1.2s ease-in-out infinite; }
668
+ @keyframes as-pulse { 50% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--bc-accent) 18%, transparent); } }
669
+ @media (prefers-reduced-motion: reduce) { .as-orb, .as-steps li.active .dot, .assistant.analyzing .as-progress i, .typing i { animation: none; } }
670
+
671
+ /* Quick answers under the confirm question — only on the latest message. */
672
+ .msg-row .quick { display: none; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
673
+ .msg-row:last-child .quick { display: flex; }
674
+ .quick button {
675
+ display: inline-flex; align-items: center; gap: 6px;
676
+ font: inherit; font-size: 13px; font-weight: 560; height: 34px; padding: 0 14px; border-radius: 999px; cursor: pointer;
677
+ border: 0; background: var(--bc-bg); color: var(--bc-fg); box-shadow: 0 0 0 1px var(--bc-border);
678
+ transition: background 160ms, color 160ms, transform 160ms var(--bc-ease);
679
+ animation: bc-item-in 300ms var(--bc-ease) both;
680
+ }
681
+ .quick button:first-child { background: var(--bc-accent); color: var(--bc-on-accent); box-shadow: none; }
682
+ .quick button:nth-child(2) { animation-delay: 60ms; }
683
+ .quick button svg { font-size: 13px; }
684
+ .quick button:hover { background: var(--bc-subtle); }
685
+ .quick button:first-child:hover { background: var(--bc-accent); filter: brightness(1.12) contrast(.96); }
686
+ .quick button:active { transform: scale(.97); }
687
+
688
+ /* ---------- the composer ---------- */
689
+ .composer {
690
+ flex: none; position: relative;
691
+ padding: 10px 12px max(12px, env(safe-area-inset-bottom, 0px));
692
+ /* The panel's own background, not --bc-elevated: the two are the same white
693
+ in light mode but differ in dark, where a raised composer reads as a band
694
+ laid over the panel rather than the foot of it. */
695
+ background: var(--bc-bg);
696
+ border-top: 1px solid var(--bc-border);
697
+ }
698
+ /* One rounded field holding the tools, the text and the send button — the
699
+ shape every chat box has, so nobody has to look for where to type. */
700
+ .composer .row {
701
+ display: flex; align-items: flex-end; gap: 4px;
702
+ padding: 5px; border-radius: 22px;
703
+ background: var(--bc-stone); border: 1px solid transparent;
704
+ transition: border-color 160ms, box-shadow 160ms, background 160ms;
705
+ }
706
+ .composer .row:focus-within { background: var(--bc-bg); border-color: var(--bc-accent); box-shadow: var(--bc-ring); }
707
+ .composer textarea {
708
+ flex: 1; min-width: 0; min-height: 34px; max-height: 120px; resize: none; padding: 7px 6px;
709
+ font: inherit; font-size: 13.5px; line-height: 1.45;
710
+ color: var(--bc-fg); background: transparent; border: 0; border-radius: 0;
711
+ }
712
+ .composer textarea:hover, .composer textarea:focus { border: 0; box-shadow: none; outline: none; }
713
+ .composer .tools { display: flex; gap: 0; grid-template-columns: none; }
714
+ .composer .tool {
715
+ width: 34px; height: 34px; min-height: 0; padding: 0; display: grid; place-items: center; cursor: pointer;
716
+ border-radius: 50%; border: 0; background: none; font-size: 17px;
717
+ color: var(--bc-muted);
718
+ }
719
+ .composer .tool svg { font-size: 17px; }
720
+ .composer .tool:hover:not(:disabled) { background: color-mix(in srgb, var(--bc-fg) 8%, transparent); color: var(--bc-fg); border: 0; transform: none; }
721
+ .composer .tool:hover:not(:disabled) svg { color: var(--bc-fg); }
722
+ .composer .tool:disabled { opacity: .4; cursor: default; }
723
+ .composer .tool.recording { color: var(--bc-danger); background: color-mix(in srgb, var(--bc-danger) 12%, transparent); }
724
+ .composer .tool.recording svg { color: var(--bc-danger); }
725
+ .composer .send {
726
+ width: 34px; height: 34px; flex: none; border-radius: 50%; cursor: pointer; font-size: 16px;
727
+ background: var(--bc-accent); border: 0; color: var(--bc-on-accent);
728
+ display: grid; place-items: center;
729
+ transition: transform 160ms var(--bc-ease), opacity 160ms, filter 160ms;
730
+ }
731
+ .composer .send:hover:not(:disabled) { filter: brightness(1.12) contrast(.96); transform: scale(1.04); }
732
+ .composer .send:disabled { opacity: .3; cursor: default; background: var(--bc-faint); }
733
+ .composer .staged { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 0 8px; }
734
+ .composer .chip {
735
+ display: flex; align-items: center; gap: 6px; max-width: 200px;
736
+ padding: 5px 7px 5px 10px; border-radius: 999px; font-size: var(--bc-fs-xs);
737
+ background: var(--bc-subtle); border: 1px solid var(--bc-border); color: var(--bc-muted);
738
+ animation: bc-item-in 240ms var(--bc-ease) both;
739
+ }
740
+ .composer .chip .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--bc-fg); }
741
+ .composer .chip button {
742
+ flex: none; width: 18px; height: 18px; display: grid; place-items: center;
743
+ border: 0; border-radius: 50%; background: none; color: var(--bc-faint); cursor: pointer;
744
+ }
745
+ .composer .chip button:hover { color: var(--bc-danger); }
746
+ .composer .chip.uploading { border-style: dashed; }
747
+ .composer .note { padding: 6px 6px 0; font-size: var(--bc-fs-xs); color: var(--bc-muted); }
748
+ .composer .note.err { color: var(--bc-danger); }
749
+ .reccount { font-variant-numeric: tabular-nums; }
750
+
751
+ /* Status carries the only colour in this list, so it is the thing the eye
752
+ lands on — which is what someone opening this screen came to check. */
753
+ .pill {
754
+ flex: none; display: inline-flex; align-items: center; gap: 5px;
755
+ font-size: 11px; font-weight: 600;
756
+ padding: 3px 9px; border-radius: 999px;
757
+ color: var(--bc-muted); background: var(--bc-stone);
758
+ }
759
+ .pill::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
760
+ /* The theme's own status colours, which already have a dark cut. */
761
+ .pill.open { color: var(--bc-warn); background: color-mix(in srgb, var(--bc-warn) 12%, var(--bc-bg)); }
762
+ .pill.in_progress { color: var(--bc-info); background: color-mix(in srgb, var(--bc-info) 12%, var(--bc-bg)); }
763
+ .pill.resolved { color: var(--bc-ok); background: color-mix(in srgb, var(--bc-ok) 12%, var(--bc-bg)); }
764
+ .pill.closed { color: var(--bc-muted); }
765
+
766
+ /* ---------- does the fix work ---------- */
767
+ .fix-verification {
768
+ flex: none; margin: 10px 12px 0; padding: 12px 14px;
769
+ border-radius: var(--bc-r-lg); border: 1px solid color-mix(in srgb, var(--bc-ok) 35%, var(--bc-border));
770
+ background: color-mix(in srgb, var(--bc-ok) 8%, var(--bc-bg));
771
+ font-size: var(--bc-fs-sm); color: var(--bc-fg);
772
+ animation: bc-rise 280ms var(--bc-ease) both;
773
+ }
774
+ .fix-verification p { margin: 0 0 8px; font-weight: 620; }
775
+ .fix-verification textarea { min-height: 56px; margin-bottom: 8px; font-size: 13px; background: var(--bc-bg); }
776
+ .fix-verification button {
777
+ padding: 7px 13px; font: inherit; font-size: 12.5px; font-weight: 620; border-radius: 999px; cursor: pointer;
778
+ border: 1px solid var(--bc-border); background: var(--bc-bg); color: var(--bc-fg);
779
+ }
780
+ .fix-verification button[data-result="CONFIRMED"] { background: var(--bc-ok); border-color: var(--bc-ok); color: #fff; }
781
+ .fix-verification .feedback-status { margin: 6px 0 0; font-weight: 450; color: var(--bc-danger); font-size: var(--bc-fs-xs); }
782
+ .fix-verification .feedback-status:empty { display: none; }
783
+
784
+ /* ---------- how did we do ----------
785
+ Quieter than the card above it: that one asks whether the fix works, which
786
+ the team acts on; this asks how it felt, which they learn from. The stars
787
+ carry the only colour, so the one thing to do here is the thing you see. */
788
+ .fix-rating {
789
+ --bc-star: #f0a81c;
790
+ flex: none; margin: 10px 12px 0; padding: 12px 14px;
791
+ border-radius: var(--bc-r-lg); border: 1px solid var(--bc-border); background: var(--bc-bg);
792
+ font-size: var(--bc-fs-sm); color: var(--bc-fg);
793
+ animation: bc-rise 280ms var(--bc-ease) both;
794
+ }
795
+ .fix-rating .fr-q { margin: 0 0 6px; font-weight: 620; }
796
+ .fix-rating .stars { display: flex; gap: 2px; margin: 0 0 2px -4px; }
797
+ .fix-rating .stars button {
798
+ display: inline-grid; place-items: center; width: 34px; height: 34px; padding: 0;
799
+ border: 0; border-radius: 8px; background: transparent; cursor: pointer;
800
+ color: color-mix(in srgb, var(--bc-fg) 18%, transparent);
801
+ transition: color 120ms ease, transform 120ms var(--bc-ease);
802
+ }
803
+ .fix-rating .stars button svg { width: 24px; height: 24px; }
804
+ .fix-rating .stars button.on { color: var(--bc-star); }
805
+ .fix-rating .stars button:hover:not(:disabled) { transform: scale(1.08); }
806
+ .fix-rating .stars button:focus-visible { outline: none; box-shadow: var(--bc-ring); }
807
+ .fix-rating .stars button:disabled { cursor: default; }
808
+ .fix-rating .fr-hint { margin: 2px 0 0; font-size: var(--bc-fs-xs); color: var(--bc-faint); }
809
+ .fix-rating .fr-said { margin: 6px 0 0; font-size: 13px; color: var(--bc-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
810
+ .fix-rating .fr-note { margin-top: 8px; }
811
+ .fix-rating .fr-note textarea { min-height: 52px; margin-bottom: 8px; font-size: 13px; }
812
+ .fix-rating .fr-note button {
813
+ padding: 7px 13px; font: inherit; font-size: 12.5px; font-weight: 620; border-radius: 999px; cursor: pointer;
814
+ border: 1px solid var(--bc-border); background: var(--bc-bg); color: var(--bc-fg);
815
+ }
816
+ .fix-rating .feedback-status { margin: 6px 0 0; color: var(--bc-danger); font-size: var(--bc-fs-xs); }
817
+ .fix-rating .feedback-status:empty { display: none; }
818
+ @media (prefers-reduced-motion: reduce) { .fix-rating .stars button { transition: none; } .fix-rating .stars button:hover { transform: none; } }
819
+
820
+ /* The rating on a history row: the score given, or a nudge to give one. */
821
+ .history .sub .rated { display: inline-flex; align-items: center; gap: 2px; color: var(--bc-muted); font-weight: 600; }
822
+ .history .sub .rated svg { width: 11px; height: 11px; color: #f0a81c; }
823
+ .history .sub .rate-ask { color: var(--bc-fg); font-weight: 600; }
824
+
825
+ /* A call in progress, in place of the form.
826
+ ---------------------------------------------------------------------------
827
+ Deliberately not styled as a chat. Nothing arrives here to read — the
828
+ conversation is happening out loud — so this is a state, a line of
829
+ reassurance about the report, and a way to hang up. */
830
+ .call { display: flex; flex-direction: column; padding: 26px 0 0; animation: bc-fade-in 240ms ease both; }
831
+ .call-head, .call-note { padding: 0 var(--bc-pad); }
832
+ .call-head { display: flex; align-items: center; gap: 9px; margin-bottom: 8px; }
833
+ .call-head strong { font-size: 15.5px; font-weight: 680; letter-spacing: -.01em; }
834
+ .call-dot {
835
+ width: 9px; height: 9px; border-radius: 50%; background: var(--bc-accent);
836
+ animation: bc-call-pulse 1.6s ease-in-out infinite;
837
+ }
838
+ .call-dot.off { background: var(--bc-faint); animation: none; }
839
+ @keyframes bc-call-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
840
+ /* Said while it is still ringing, because that is when somebody is deciding
841
+ whether it is worth waiting. */
842
+ .call-note { margin: 0 0 18px; font-size: 13px; color: var(--bc-muted); line-height: 1.55; }
843
+
844
+ /* ---------- the same call, on top of the report it is about ---------- */
845
+ /* A bar rather than its own screen, unlike the one the composer opens. The
846
+ thread underneath is exactly what both people are talking about, so taking it
847
+ off screen would be taking away the only shared reference. */
848
+ .livecall {
849
+ flex: none;
850
+ display: flex; align-items: center; gap: 9px;
851
+ margin: 10px 12px 0; padding: 9px 11px;
852
+ border: 1px solid var(--bc-accent-line); border-radius: var(--bc-r-md);
853
+ background: var(--bc-accent-soft); font-size: var(--bc-fs-sm); color: var(--bc-fg);
854
+ animation: bc-rise 240ms var(--bc-ease) both;
855
+ }
856
+ .livecall .who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
857
+ .livecall .call-kept { font-size: 11px; opacity: .75; }
858
+ .livecall .secs { font-variant-numeric: tabular-nums; color: var(--bc-muted); }
859
+ .livecall .act {
860
+ padding: 5px 11px; border-radius: 999px; border: 1px solid var(--bc-border);
861
+ background: var(--bc-bg); color: var(--bc-fg);
862
+ font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
863
+ }
864
+ .livecall .act:hover { background: var(--bc-subtle); }
865
+ .livecall .act.danger { border-color: transparent; background: var(--bc-danger); color: #fff; }
866
+ .livecall.over { color: var(--bc-muted); background: var(--bc-subtle); border-color: var(--bc-border); }
867
+ .call-note code { font-size: 11px; }
868
+ @media (prefers-reduced-motion: reduce) { .call-dot { animation: none; } }
869
+
870
+ /* div.done, not .done: the assistant's stepper marks finished steps li.done,
871
+ and this screen's padding was landing on every one of them. */
872
+ div.done { padding: 44px 26px 44px; text-align: center; animation: bc-fade-in 240ms ease both; }
873
+ .done .tick {
874
+ width: 52px; height: 52px; margin: 0 auto 16px; border-radius: 50%;
875
+ display: grid; place-items: center; font-size: 24px;
876
+ background: var(--bc-ok); color: #fff;
877
+ box-shadow: 0 0 0 7px color-mix(in srgb, var(--bc-ok) 12%, transparent);
878
+ animation: bc-pop 420ms var(--bc-ease) both;
879
+ }
880
+ .done h3 { margin: 0 0 6px; font-size: 18px; font-weight: 600; letter-spacing: -.02em; }
881
+ .done p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--bc-muted); }
882
+ .done code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; padding: 1px 5px; border-radius: 5px; background: var(--bc-subtle); color: var(--bc-fg); }
883
+ /* The likely wait. Set apart from the reference line above it by a rule rather
884
+ than by weight: one is bookkeeping and the other is the answer to the
885
+ question the reporter actually has, and they should not read as one
886
+ paragraph. */
887
+ .done .eta {
888
+ margin: 14px auto 0;
889
+ padding-top: 12px;
890
+ border-top: 1px solid var(--bc-border);
891
+ max-width: 30ch;
892
+ line-height: 1.5;
893
+ }
894
+ .done .eta strong { color: var(--bc-fg); font-weight: 620; }
895
+ /* The same figure on the report the reporter came back to. Sits under the
896
+ status pill in the header block, muted — it is context, not the headline the
897
+ status is. */
898
+ .report-head .eta-line {
899
+ grid-column: 1 / -1;
900
+ font-size: var(--bc-fs-xs);
901
+ color: var(--bc-muted);
902
+ margin-top: 2px;
903
+ }
904
+
905
+ /* ---------- recording bar ---------- */
906
+ .recbar {
907
+ position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom, 0px)); transform: translateX(-50%);
908
+ z-index: 2147483002;
909
+ display: flex; align-items: center; gap: 12px;
910
+ padding: 8px 8px 8px 16px; border-radius: 999px;
911
+ background: var(--bc-elevated); color: var(--bc-fg);
912
+ border: 1px solid var(--bc-border); box-shadow: var(--bc-shadow);
913
+ animation: bc-item-in 300ms var(--bc-ease) both;
914
+ }
915
+ .recbar .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--bc-danger); animation: bc-pulse 1.4s ease-in-out infinite; }
916
+ .recbar .time { font-variant-numeric: tabular-nums; font-size: 13px; font-weight: 650; min-width: 38px; }
917
+ .recbar .hint { font-size: 12px; color: var(--bc-muted); }
918
+ .recbar button { height: 34px; padding: 0 14px; font-size: 12.5px; font-weight: 620; border-radius: 999px; cursor: pointer; }
919
+ .recbar .stop { background: var(--bc-danger); border: 1px solid var(--bc-danger); color: #fff; }
920
+ .recbar .stop:hover { filter: brightness(1.08); }
921
+
922
+ /* ---------- phones ----------------------------------------------------------
923
+ This widget runs inside a shadow root on somebody else's site, so its own
924
+ stylesheet is the whole story: the host page cannot correct any of this, and
925
+ anything that goes wrong here goes wrong on their site, in front of their
926
+ visitor, at the moment that visitor is already annoyed enough to file a bug.
927
+ -------------------------------------------------------------------------- */
928
+
929
+ /* 100vh on a phone is the viewport with the browser chrome hidden, so the
930
+ panel could be taller than the space it actually has and put "Send report"
931
+ under the address bar. */
932
+ @supports (height: 100dvh) {
933
+ .panel { max-height: calc(100dvh - var(--bc-dock-h, 84px) - 52px); }
934
+ }
935
+
936
+ /* Short desktop windows: the chat height gives way to the space there is. */
937
+ @media (max-height: 820px) { .panel.is-chat { height: calc(100vh - var(--bc-dock-h, 84px) - 52px); } }
938
+
939
+ /* A phone gets the whole screen: a sheet that slides up from the bottom, with
940
+ the notch and the home indicator kept clear. The dock steps aside while it
941
+ is open — the sheet has its own close button, and a launcher floating over
942
+ the send button would be in the way of the one thing that matters. */
943
+ @media (max-width: 480px) {
944
+ .scrim { background: rgba(14,14,16,.4); }
945
+ .panel, .scrim.at-right .panel, .scrim.at-left .panel, .scrim.at-bottom .panel, .scrim.at-top .panel {
946
+ inset: 0; width: 100%; max-width: none; height: 100%; max-height: none;
947
+ border-radius: 0; border: 0;
948
+ animation: bc-sheet-in 380ms var(--bc-ease) both;
949
+ }
950
+ @supports (height: 100dvh) { .panel { height: 100dvh; max-height: none; } }
951
+ .panel.is-chat { height: 100%; }
952
+ .scrim.closing .panel { animation: bc-sheet-out 200ms ease both; }
953
+ .head { padding-top: max(14px, env(safe-area-inset-top, 0px)); padding-left: max(var(--bc-pad), env(safe-area-inset-left, 0px)); padding-right: max(12px, env(safe-area-inset-right, 0px)); }
954
+ .foot, .powered { padding-bottom: max(10px, env(safe-area-inset-bottom, 0px)); }
955
+ .foot + .powered { padding-bottom: max(9px, env(safe-area-inset-bottom, 0px)); }
956
+ .foot:has(+ .powered) { padding-bottom: 12px; }
957
+ .recbar .hint { display: none; }
958
+ .msg-row, .typing-row { max-width: 92%; }
959
+ /* The question chips are one row that scrolls sideways, edge to edge, rather
960
+ than three stacked rows pushing the form down the screen. */
961
+ .picks .picks-grid {
962
+ flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
963
+ margin: 0 calc(var(--bc-pad) * -1); padding: 0 var(--bc-pad) 2px;
964
+ scroll-snap-type: x proximity; scroll-padding: 0 var(--bc-pad);
965
+ }
966
+ .picks .picks-grid::-webkit-scrollbar { display: none; }
967
+ .picks .pick { flex: none; scroll-snap-align: start; }
968
+ .picks .pick .t { white-space: nowrap; }
969
+ }
970
+
971
+ @media (pointer: coarse) {
972
+ /* iOS zooms the whole page when a field it focuses is under 16px, and does
973
+ not zoom back out. On the host's site that is not the widget's page to
974
+ zoom — the visitor is left on a magnified checkout wondering what they
975
+ touched. !important because this is a property of the device, not a
976
+ preference about type. */
977
+ input, textarea, select { font-size: 16px !important; }
978
+
979
+ /* 44px is the floor a thumb hits reliably. Every control in this form was
980
+ under it, on the one surface in the product designed for someone who is
981
+ not an operator and gets no second attempt. */
982
+ .seg button { min-height: 44px; }
983
+ .tool, .known .issue .join { min-height: 44px; }
984
+ .picks .pick { min-height: 40px; }
985
+ button.primary, button.ghost { height: 44px; }
986
+ .composer .send, .composer .tool { width: 40px; height: 40px; min-height: 0; }
987
+ textarea { min-height: 104px; }
988
+ .composer textarea { min-height: 40px; }
989
+
990
+ /* The close button and the thumbnails' remove buttons are small glyphs whose
991
+ boxes cannot grow without moving what is beside them, so each keeps its
992
+ size and gets an invisible target drawn around it instead. The theme
993
+ button in the header is one of these. */
994
+ .head .icon-btn, .thumb .drop, .brand-credit { position: relative; }
995
+ .thumb .drop { position: absolute; opacity: 1; }
996
+ .head .icon-btn::after, .thumb .drop::after, .brand-credit::after {
997
+ content: '';
998
+ position: absolute;
999
+ inset: -8px;
1000
+ }
1001
+ }
1002
+
1003
+ /* Hover is a lie on a touchscreen: it latches on tap and stays. */
1004
+ @media (hover: none) {
1005
+ .tool:hover:not(:disabled) { transform: none; }
1006
+ }
1007
+ `;
1008
+ function le({ title: e, sub: t, id: i = "", back: w = !1, theme: b = "" }) {
1009
+ return `
1010
+ <div class="head">
1011
+ ${w ? `<button class="icon-btn back" type="button" data-act="back" aria-label="Back to your reports">${Ne}</button>` : ""}
1012
+ <span class="orb mark" aria-hidden="true"></span>
1013
+ <div class="titles">
1014
+ <h2${i ? ` id="${i}"` : ""}>${v(e)}</h2>
1015
+ <p>${v(t)}</p>
1016
+ </div>
1017
+ ${b ? `<button class="icon-btn theme" type="button" data-act="theme" title="Theme: ${v(b)}"
1018
+ aria-label="Theme: ${v(b)}. Change theme"><span class="m-light">${ke}</span><span class="m-dark">${$e}</span><span class="m-auto">${Se}</span></button>` : ""}
1019
+ <button class="icon-btn" type="button" data-act="close" aria-label="Close">${ce}</button>
1020
+ </div>`;
1021
+ }
1022
+ function re(e, t) {
1023
+ return `<div class="empty"><span class="empty-ico" aria-hidden="true">${t}</span>${v(e)}</div>`;
1024
+ }
1025
+ function Ke(e) {
1026
+ return Array.from({ length: e }, () => `
1027
+ <div class="row" aria-hidden="true">
1028
+ <span class="row-ico"></span>
1029
+ <span class="who"><span class="skel" style="width:70%"></span><span class="skel" style="width:40%;height:9px"></span></span>
1030
+ </div>`).join("") + '<span class="sr-only">Loading…</span>';
1031
+ }
1032
+ function Ue() {
1033
+ return `
1034
+ <div aria-hidden="true" style="display:flex;flex-direction:column;gap:10px;padding-top:6px">
1035
+ <span class="skel" style="width:42%;height:34px;border-radius:16px;align-self:center"></span>
1036
+ <span class="skel" style="width:66%;height:52px;border-radius:18px"></span>
1037
+ <span class="skel" style="width:48%;height:38px;border-radius:18px;align-self:flex-end"></span>
1038
+ </div>
1039
+ <span class="sr-only">Loading…</span>`;
1040
+ }
1041
+ function Ze(e) {
1042
+ const t = String(e).trim().split(/\s+/).filter(Boolean);
1043
+ return t.length ? (t[0][0] + (t.length > 1 ? t[t.length - 1][0] : "")).toUpperCase() : "?";
1044
+ }
1045
+ function Z(e) {
1046
+ const t = new Date(e);
1047
+ return Number.isFinite(t.getTime()) ? `${t.getFullYear()}-${t.getMonth()}-${t.getDate()}` : null;
1048
+ }
1049
+ function _e(e) {
1050
+ const t = new Date(e), i = /* @__PURE__ */ new Date(), w = new Date(i.getFullYear(), i.getMonth(), i.getDate() - 1);
1051
+ if (Z(t) === Z(i)) return "Today";
1052
+ if (Z(t) === Z(w)) return "Yesterday";
1053
+ try {
1054
+ return t.toLocaleDateString(void 0, {
1055
+ weekday: "short",
1056
+ day: "numeric",
1057
+ month: "short",
1058
+ ...t.getFullYear() !== i.getFullYear() ? { year: "numeric" } : {}
1059
+ });
1060
+ } catch {
1061
+ return ne(e);
1062
+ }
1063
+ }
1064
+ function ve(e) {
1065
+ const t = new Date(e);
1066
+ if (!Number.isFinite(t.getTime())) return "";
1067
+ try {
1068
+ return t.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
1069
+ } catch {
1070
+ return "";
1071
+ }
1072
+ }
1073
+ function pe(e) {
1074
+ return e.scrollHeight - e.scrollTop - e.clientHeight < 40;
1075
+ }
1076
+ function _(e, { smooth: t = !1 } = {}) {
1077
+ const i = typeof matchMedia == "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1078
+ t && !i && e.scrollTo ? e.scrollTo({ top: e.scrollHeight, behavior: "smooth" }) : e.scrollTop = e.scrollHeight;
1079
+ }
1080
+ function ie(e) {
1081
+ return {
1082
+ OPEN: "Open",
1083
+ IN_PROGRESS: "In progress",
1084
+ RESOLVED: "Fixed",
1085
+ CLOSED: "Closed"
1086
+ }[e] || String(e || "").toLowerCase();
1087
+ }
1088
+ function ne(e) {
1089
+ const t = new Date(e).getTime();
1090
+ if (!Number.isFinite(t)) return "";
1091
+ const i = Math.floor((Date.now() - t) / 864e5);
1092
+ if (i <= 0) return "today";
1093
+ if (i === 1) return "yesterday";
1094
+ if (i < 30) return `${i} days ago`;
1095
+ const w = Math.floor(i / 30);
1096
+ return w === 1 ? "a month ago" : `${w} months ago`;
1097
+ }
1098
+ function v(e) {
1099
+ return String(e).replace(/[&<>"']/g, (t) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[t]);
1100
+ }
1101
+ const Ge = 7 * 24 * 60 * 60 * 1e3;
1102
+ let J;
1103
+ function Xe() {
1104
+ return J || (J = new Promise((e, t) => {
1105
+ const i = indexedDB.open("bugcatch-drafts", 1);
1106
+ i.onupgradeneeded = () => i.result.createObjectStore("drafts"), i.onsuccess = () => e(i.result), i.onerror = () => t(i.error), i.onblocked = () => t(new Error("Draft storage is blocked."));
1107
+ }).catch((e) => {
1108
+ throw J = null, e;
1109
+ })), J;
1110
+ }
1111
+ function Je(e, t) {
1112
+ var i, w;
1113
+ return JSON.stringify([e.apiBase, e.projectId, t || ((i = e.user) == null ? void 0 : i.email) || ((w = e.user) == null ? void 0 : w.id) || "anonymous"]);
1114
+ }
1115
+ async function te(e, t) {
1116
+ const i = await Xe();
1117
+ return new Promise((w, b) => {
1118
+ const T = i.transaction("drafts", t === void 0 ? "readonly" : "readwrite"), x = T.objectStore("drafts"), g = t === void 0 ? x.get(e) : t === null ? x.delete(e) : x.put({ ...t, savedAt: Date.now() }, e);
1119
+ T.oncomplete = () => w(g.result), T.onerror = () => b(T.error), T.onabort = () => b(T.error || new Error("Draft was not saved."));
1120
+ });
1121
+ }
1122
+ async function et(e) {
1123
+ const t = await te(e);
1124
+ return t && Date.now() - t.savedAt > Ge ? (await te(e, null), null) : t || null;
1125
+ }
1126
+ const xe = [
1127
+ { value: "LOW", label: "Low" },
1128
+ { value: "MEDIUM", label: "Medium" },
1129
+ { value: "HIGH", label: "High" },
1130
+ { value: "CRITICAL", label: "Critical" }
1131
+ ], ye = "MEDIUM";
1132
+ function tt(e) {
1133
+ const t = e.attachments !== !1, i = t && Le(), w = t && Qe();
1134
+ return `
1135
+ <div class="compose">
1136
+ <div class="field">
1137
+ <label for="bc-title">What went wrong?</label>
1138
+ <input id="bc-title" type="text" placeholder="e.g. The checkout button does nothing" autocomplete="off"
1139
+ aria-describedby="bc-title-err" />
1140
+ <p class="field-err" id="bc-title-err" hidden></p>
1141
+ </div>
1142
+ <div class="field">
1143
+ <label for="bc-desc">Anything else that helps <span class="opt">(optional)</span></label>
1144
+ <textarea id="bc-desc" placeholder="What you were doing, what you expected, what happened instead."></textarea>
1145
+ </div>
1146
+ <!-- Drawn only when this project asks for an identifier and the report
1147
+ does not already carry one. Between the description and the
1148
+ severity because that is where it belongs in the reading order:
1149
+ it is a question about what they just typed. -->
1150
+ <div class="refs" hidden></div>
1151
+ <div class="field">
1152
+ <label id="bc-prio-l">How much is it getting in your way?</label>
1153
+ <div class="seg" role="group" aria-labelledby="bc-prio-l">
1154
+ ${xe.map((b) => `
1155
+ <button type="button" data-prio="${b.value}" aria-pressed="${b.value === ye}">${b.label}</button>
1156
+ `).join("")}
1157
+ </div>
1158
+ </div>
1159
+ ${t ? `
1160
+ <div class="field">
1161
+ <div class="field-head">
1162
+ <label id="bc-att-l">Show us <span class="opt">(optional)</span></label>
1163
+ <span class="count" aria-live="polite"></span>
1164
+ </div>
1165
+ <div class="tools" role="group" aria-labelledby="bc-att-l">
1166
+ ${i ? `<button class="tool" type="button" data-act="shot">${Ve}Screenshot</button>` : ""}
1167
+ ${w ? `<button class="tool" type="button" data-act="rec">${je}Record screen</button>` : ""}
1168
+ <button class="tool" type="button" data-act="file">${fe}Upload file</button>
1169
+ </div>
1170
+ <div class="thumbs"></div>
1171
+ <input type="file" accept="${be.join(",")}" multiple hidden />
1172
+ </div>` : ""}
1173
+ </div>`;
1174
+ }
1175
+ function at(e) {
1176
+ var oe, K, U, M;
1177
+ const {
1178
+ client: t,
1179
+ cfg: i,
1180
+ root: w,
1181
+ host: b,
1182
+ dock: T,
1183
+ scrim: x,
1184
+ $: g,
1185
+ say: m,
1186
+ titleInput: l,
1187
+ descInput: d,
1188
+ compose: L,
1189
+ sendBtn: y,
1190
+ note: k
1191
+ } = e, S = g(".thumbs"), q = x.querySelector("input[type=file]"), E = [];
1192
+ let n = ye, p = null;
1193
+ const a = document.createElement("div");
1194
+ a.setAttribute("role", "status"), a.className = "note", L.prepend(a);
1195
+ const s = Je(i, (oe = t.signedInAs()) == null ? void 0 : oe.email);
1196
+ let u = i.drafts === !1, r = !1, o = Promise.resolve(), f;
1197
+ function $() {
1198
+ if (clearTimeout(f), !u || r || i.drafts === !1) return o;
1199
+ const c = l.value || d.value || E.length ? {
1200
+ title: l.value,
1201
+ description: d.value,
1202
+ priority: n,
1203
+ items: E.map(({ blob: Q, mime: I, kind: C }) => ({ blob: Q, mime: I, kind: C }))
1204
+ } : null;
1205
+ return o = o.catch(() => {
1206
+ }).then(() => te(s, c)).then(() => {
1207
+ a.textContent = c ? "Draft saved on this device" : "";
1208
+ }).catch(() => {
1209
+ a.textContent = "Draft could not be saved. Keep this tab open.";
1210
+ }), o;
1211
+ }
1212
+ function A() {
1213
+ clearTimeout(f), f = setTimeout($, 250);
1214
+ }
1215
+ L.addEventListener("input", A), L.addEventListener("change", A);
1216
+ const j = () => {
1217
+ $();
1218
+ };
1219
+ window.addEventListener("pagehide", j);
1220
+ function B() {
1221
+ E.filter((c) => c.status === "failed").forEach(D);
1222
+ }
1223
+ window.addEventListener("online", B), i.drafts !== !1 && (L.inert = !0, et(s).then((c) => {
1224
+ e.closed || !c || (a.innerHTML = '<span>You have an unfinished report on this device.</span> <button type="button" data-resume>Resume report</button> <button type="button" data-discard>Discard</button>', a.querySelector("[data-resume]").onclick = () => {
1225
+ l.value = c.title || "", d.value = c.description || "", n = xe.some((Q) => Q.value === c.priority) ? c.priority : "MEDIUM", x.querySelectorAll("[data-prio]").forEach((Q) => Q.setAttribute("aria-pressed", String(Q.dataset.prio === n)));
1226
+ for (const Q of (c.items || []).slice(0, i.maxAttachments ?? 5)) O(Q);
1227
+ u = !0, $();
1228
+ }, a.querySelector("[data-discard]").onclick = () => {
1229
+ u = !0, $();
1230
+ });
1231
+ }).catch(() => {
1232
+ a.textContent = "Draft storage is unavailable. Keep this tab open.";
1233
+ }).finally(() => {
1234
+ L.inert = !1, a.querySelector("[data-resume]") || (u = !0);
1235
+ })), e.onDestroy(() => {
1236
+ var c;
1237
+ $(), clearTimeout(f), window.removeEventListener("pagehide", j), window.removeEventListener("online", B), p == null || p.cancel(), E.forEach((Q) => Q.previewUrl && URL.revokeObjectURL(Q.previewUrl)), (c = w.querySelector(".recbar")) == null || c.remove();
1238
+ }), x.querySelectorAll("[data-prio]").forEach((c) => {
1239
+ c.addEventListener("click", () => {
1240
+ n = c.dataset.prio, A(), x.querySelectorAll("[data-prio]").forEach((Q) => {
1241
+ Q.setAttribute("aria-pressed", String(Q === c));
1242
+ });
1243
+ });
1244
+ });
1245
+ const N = g("#bc-title-err");
1246
+ function h(c) {
1247
+ N.hidden = !c, N.innerHTML = c ? `${G}<span>${v(c)}</span>` : "", l.classList.toggle("invalid", !!c), l.setAttribute("aria-invalid", String(!!c));
1248
+ }
1249
+ l.addEventListener("input", () => {
1250
+ l.value.trim() && h("");
1251
+ });
1252
+ function z() {
1253
+ const c = E.some((C) => C.status === "uploading");
1254
+ y.disabled = e.sending || c || E.some((C) => C.status === "failed"), k.textContent = c ? "Waiting for the upload to finish…" : E.some((C) => C.status === "failed") ? "Retry or remove failed attachments before sending." : "";
1255
+ const Q = i.maxAttachments ?? 5;
1256
+ x.querySelectorAll(".tool").forEach((C) => {
1257
+ C.disabled = e.sending || E.length >= Q;
1258
+ });
1259
+ const I = g(".field-head .count");
1260
+ I && (I.textContent = E.length ? `${E.length} of ${Q}` : "");
1261
+ }
1262
+ e.refreshSendState = z;
1263
+ function R(c) {
1264
+ var Y;
1265
+ const Q = c.el, I = c.kind === "VIDEO" ? "Video" : "Shot", C = c.kind === "VIDEO" ? `<video src="${c.previewUrl}" muted playsinline preload="metadata"></video>` : `<img src="${c.previewUrl}" alt="" />`;
1266
+ let V = "";
1267
+ c.status === "uploading" ? V = `<div class="veil"><div class="spinner"></div><div class="bar"><i style="width:${Math.round(c.progress * 100)}%"></i></div></div>` : c.status === "failed" && (V = `<div class="veil err"><button type="button" data-act="retry" aria-label="Retry upload">${Be}</button>Failed</div>`), Q.innerHTML = `
1268
+ ${C}
1269
+ <span class="kind">${I} · ${P(c.size)}</span>
1270
+ <button class="drop" type="button" data-act="drop" aria-label="Remove ${I.toLowerCase()}">${ce}</button>
1271
+ ${V}`, Q.querySelector('[data-act="drop"]').addEventListener("click", () => H(c)), (Y = Q.querySelector('[data-act="retry"]')) == null || Y.addEventListener("click", () => D(c));
1272
+ }
1273
+ function H(c) {
1274
+ const Q = E.indexOf(c);
1275
+ Q >= 0 && E.splice(Q, 1), c.previewUrl && URL.revokeObjectURL(c.previewUrl), c.el.remove(), $(), z();
1276
+ }
1277
+ async function D(c) {
1278
+ c.status = "uploading", c.progress = 0, R(c), z();
1279
+ try {
1280
+ const { storageKey: Q, mime: I } = await t.uploadAttachment(c.blob, c.mime, (C) => {
1281
+ c.progress = C;
1282
+ const V = c.el.querySelector(".bar i");
1283
+ V && (V.style.width = `${Math.round(C * 100)}%`);
1284
+ });
1285
+ c.storageKey = Q, c.mime = I, c.status = "ready";
1286
+ } catch (Q) {
1287
+ c.status = "failed", m(Q.message.replace("[BugCatch] ", ""));
1288
+ }
1289
+ R(c), z();
1290
+ }
1291
+ function O({ blob: c, mime: Q, kind: I }) {
1292
+ const C = i.maxAttachmentBytes ?? 26214400;
1293
+ if (c.size > C) {
1294
+ m(`That file is ${P(c.size)} — the limit is ${P(C)}.`);
1295
+ return;
1296
+ }
1297
+ const V = Te(Q || c.type, be);
1298
+ if (!V) {
1299
+ m("Only PNG, JPEG, WebP, WebM and MP4 files can be attached to a report.");
1300
+ return;
1301
+ }
1302
+ const Y = {
1303
+ blob: c,
1304
+ kind: I,
1305
+ mime: V,
1306
+ size: c.size,
1307
+ previewUrl: URL.createObjectURL(c),
1308
+ status: "uploading",
1309
+ progress: 0,
1310
+ storageKey: null,
1311
+ el: document.createElement("div")
1312
+ };
1313
+ Y.el.className = "thumb", E.push(Y), S.appendChild(Y.el), m(""), $(), D(Y);
1314
+ }
1315
+ async function X(c) {
1316
+ b.style.opacity = "0", b.style.pointerEvents = "none", await new Promise((Q) => requestAnimationFrame(() => requestAnimationFrame(Q)));
1317
+ try {
1318
+ return await c();
1319
+ } finally {
1320
+ b.style.opacity = "", b.style.pointerEvents = "";
1321
+ }
1322
+ }
1323
+ (K = x.querySelector('[data-act="shot"]')) == null || K.addEventListener("click", async () => {
1324
+ try {
1325
+ const c = await X(Ee);
1326
+ O(c);
1327
+ } catch (c) {
1328
+ (c == null ? void 0 : c.name) !== "NotAllowedError" && m(`Screenshot failed: ${c.message}`);
1329
+ }
1330
+ }), (U = x.querySelector('[data-act="file"]')) == null || U.addEventListener("click", () => q.click()), q == null || q.addEventListener("change", () => {
1331
+ const c = (i.maxAttachments ?? 5) - E.length;
1332
+ [...q.files].slice(0, Math.max(c, 0)).forEach((Q) => {
1333
+ O({
1334
+ blob: Q,
1335
+ mime: Q.type,
1336
+ kind: Q.type.startsWith("video/") ? "VIDEO" : "SCREENSHOT"
1337
+ });
1338
+ }), q.value = "";
1339
+ }), (M = x.querySelector('[data-act="rec"]')) == null || M.addEventListener("click", async () => {
1340
+ const c = i.maxRecordingMs ?? 12e4;
1341
+ let Q;
1342
+ try {
1343
+ p = await qe({
1344
+ maxMs: c,
1345
+ onTick: (I) => {
1346
+ Q && (Q.querySelector(".time").textContent = ee(I));
1347
+ },
1348
+ onAutoStop: () => {
1349
+ p && W({ keep: !0 });
1350
+ }
1351
+ });
1352
+ } catch (I) {
1353
+ (I == null ? void 0 : I.name) !== "NotAllowedError" && m(`Recording failed: ${I.message}`);
1354
+ return;
1355
+ }
1356
+ x.style.display = "none", T.hidden = !0, Q = document.createElement("div"), Q.className = "recbar", Q.innerHTML = `
1357
+ <span class="dot"></span>
1358
+ <span class="time">0:00</span>
1359
+ <span class="hint">Recording — up to ${ee(Math.round(c / 1e3))}</span>
1360
+ <button class="stop" type="button">Stop</button>
1361
+ <button class="ghost" type="button">Discard</button>`, Q.querySelector(".stop").addEventListener("click", () => W({ keep: !0 })), Q.querySelector(".ghost").addEventListener("click", () => W({ keep: !1 })), w.appendChild(Q), Q.querySelector(".stop").focus();
1362
+ });
1363
+ async function W({ keep: c }) {
1364
+ var I;
1365
+ const Q = p;
1366
+ if (Q) {
1367
+ if (p = null, (I = w.querySelector(".recbar")) == null || I.remove(), x.style.display = "", T.hidden = !1, !c) {
1368
+ Q.cancel(), l.focus();
1369
+ return;
1370
+ }
1371
+ try {
1372
+ const C = await Q.stop();
1373
+ C.blob.size ? O(C) : m("That recording came back empty — nothing was captured.");
1374
+ } catch (C) {
1375
+ m(`Recording failed: ${C.message}`);
1376
+ }
1377
+ l.focus();
1378
+ }
1379
+ }
1380
+ e.isRecording = () => !!p, e.stopRecording = W, y.addEventListener("click", async () => {
1381
+ var I;
1382
+ const c = l.value;
1383
+ if (!c.trim()) {
1384
+ h("Add a few words about what went wrong — a short sentence is plenty."), l.focus();
1385
+ return;
1386
+ }
1387
+ if (e.holdForReference()) return;
1388
+ e.sending = !0, m(""), z();
1389
+ const Q = y.innerHTML;
1390
+ y.innerHTML = '<span class="spinner" aria-hidden="true"></span>Sending…';
1391
+ try {
1392
+ const C = await t.report({
1393
+ title: c,
1394
+ description: d.value,
1395
+ priority: n,
1396
+ references: e.answeredReference(),
1397
+ issue: ((I = e.pickedIssue()) == null ? void 0 : I.key) || null,
1398
+ attachments: E.filter((V) => V.status === "ready").map((V) => ({ type: V.kind, storageKey: V.storageKey, mime: V.mime }))
1399
+ });
1400
+ if (C != null && C.cancelled) {
1401
+ e.sending = !1, y.innerHTML = Q, z();
1402
+ return;
1403
+ }
1404
+ r = !0, clearTimeout(f), await o, i.drafts !== !1 && await te(s, null).catch(() => {
1405
+ }), e.succeed(C);
1406
+ } catch (C) {
1407
+ e.sending = !1, y.innerHTML = Q, m(C.message.replace("[BugCatch] ", "")), z(), e.recheckSignIn();
1408
+ }
1409
+ });
1410
+ }
1411
+ function nt(e) {
1412
+ const {
1413
+ client: t,
1414
+ say: i,
1415
+ titleInput: w,
1416
+ descInput: b,
1417
+ sendBtn: T,
1418
+ refsBox: x
1419
+ } = e;
1420
+ let g = { policy: "off", fields: [] }, m = !1, l = null, d = "", L = "", y = [], k = "ask", S = !1, q = !1;
1421
+ t.referenceFields().then((h) => {
1422
+ g = h, u();
1423
+ }).catch(() => {
1424
+ });
1425
+ function E() {
1426
+ return g.policy === "off" || !g.fields.length ? { found: [], conflicts: [], loose: [] } : ze(g.fields, [w.value, b.value]);
1427
+ }
1428
+ function n(h = E()) {
1429
+ return h.found.length > 0 && h.conflicts.length === 0;
1430
+ }
1431
+ function p() {
1432
+ return !l || !d.trim() ? [] : [{ key: l, value: d.trim().slice(0, 120) }];
1433
+ }
1434
+ const a = document.createElement("p");
1435
+ a.className = "ref-found", a.setAttribute("aria-live", "polite"), a.hidden = !0, b.closest(".field").after(a);
1436
+ let s;
1437
+ function u() {
1438
+ const h = E();
1439
+ if (n(h)) {
1440
+ const z = h.found.slice(0, 2).map((R) => `${v(R.label)} <b>${v(o(R.value))}</b>`).join(", ");
1441
+ a.innerHTML = `${F}<span>Found your ${z} — no need to add it again.</span>`, a.hidden = !1, m && !p().length && (m = !1, A());
1442
+ } else
1443
+ a.hidden = !0, a.innerHTML = "";
1444
+ }
1445
+ const r = () => {
1446
+ clearTimeout(s), s = setTimeout(u, 250);
1447
+ };
1448
+ w.addEventListener("input", r), b.addEventListener("input", r);
1449
+ function o(h) {
1450
+ return h.length > 20 ? `${h.slice(0, 8)}…${h.slice(-4)}` : h;
1451
+ }
1452
+ function f(h) {
1453
+ const z = h.trim().toLowerCase();
1454
+ return y.find((R) => R.value.toLowerCase() === z) || null;
1455
+ }
1456
+ function $(h) {
1457
+ d = h.value, h.kinds.length === 1 ? l = h.kinds[0].key : h.kinds.some((z) => z.key === l) || (l = null), L = "", q = !1;
1458
+ }
1459
+ function A() {
1460
+ var K, U;
1461
+ if (!m) {
1462
+ x.hidden = !0, x.innerHTML = "";
1463
+ return;
1464
+ }
1465
+ const h = g.fields.find((M) => M.key === l) || null, z = g.policy !== "required", R = f(d), H = R && R.kinds.length > 1 ? g.fields.filter((M) => R.kinds.some((c) => c.key === M.key)) : g.fields, D = y.length > 1 && y.every((M) => M.kinds.length === 1 && M.kinds[0].key === y[0].kinds[0].key), O = {
1466
+ confirm: y.length > 1 ? ["Which one is this about?", "We found more than one ID in your message. Pick the one this problem is about."] : ["Is this the right ID?", "This could be more than one kind of ID. Tell us which it is."],
1467
+ suggest: ["Is this your ID?", "We spotted this in your message. Tell us what it is so we can find your record."],
1468
+ ask: z ? ["One more thing", "So we can find your record — this helps us answer faster."] : ["We need one detail", "We need one of these to look this up."]
1469
+ }[k];
1470
+ x.hidden = !1;
1471
+ const X = H.length > 6;
1472
+ x.innerHTML = `
1473
+ <div class="ask-head">
1474
+ <span class="avatar orb" aria-hidden="true"></span>
1475
+ <div>
1476
+ <strong>${v(O[0])}</strong>
1477
+ <p class="why">${v(O[1])}</p>
1478
+ </div>
1479
+ </div>
1480
+ ${y.length > 1 ? `
1481
+ <div class="chips cands" role="group" aria-label="IDs found in your message">
1482
+ ${y.map((M, c) => `
1483
+ <button type="button" data-ref-cand="${c}" title="${v(M.value)}"
1484
+ aria-pressed="${!!R && R === M}">${v(o(M.value))}</button>
1485
+ `).join("")}
1486
+ </div>` : ""}
1487
+ ${H.length > 1 && !X && !D ? `
1488
+ <div class="chips" role="group" aria-label="Which detail you are giving">
1489
+ ${H.map((M) => `
1490
+ <button type="button" data-ref-kind="${v(M.key)}" aria-pressed="${!!h && M.key === h.key}">${v(M.label)}</button>
1491
+ `).join("")}
1492
+ </div>` : ""}
1493
+ <div class="row">
1494
+ ${X ? `
1495
+ <select class="ref-kind" aria-label="Which detail you are giving">
1496
+ ${h ? "" : '<option value="" selected disabled>What is it?</option>'}
1497
+ ${H.map((M) => `
1498
+ <option value="${v(M.key)}" ${h && M.key === h.key ? "selected" : ""}>${v(M.label)}</option>
1499
+ `).join("")}
1500
+ </select>` : ""}
1501
+ <input class="ref-value${L ? " invalid" : ""}" type="${(h == null ? void 0 : h.inputMode) === "email" ? "email" : "text"}"
1502
+ inputmode="${v((h == null ? void 0 : h.inputMode) === "numeric" || (h == null ? void 0 : h.inputMode) === "tel" ? "numeric" : "text")}"
1503
+ value="${v(d)}"
1504
+ placeholder="${v(h ? h.placeholder || h.label : "Your ID")}"
1505
+ aria-label="${v(h ? h.label : "Your ID")}" ${L ? 'aria-invalid="true"' : ""} />
1506
+ </div>
1507
+ ${h != null && h.hint ? `<p class="hint">${v(h.hint)}</p>` : ""}
1508
+ ${L ? `<p class="field-err" role="alert">${G}<span>${v(L)}</span></p>` : ""}
1509
+ ${z ? `<button type="button" class="link" data-act="ref-skip">${k === "ask" ? "I don't have this — send without it" : "Not sure — send as it is"}</button>` : ""}`, x.querySelectorAll("[data-ref-cand]").forEach((M) => {
1510
+ M.addEventListener("click", () => {
1511
+ $(y[Number(M.dataset.refCand)]), A();
1512
+ });
1513
+ }), x.querySelectorAll("[data-ref-kind]").forEach((M) => {
1514
+ M.addEventListener("click", () => {
1515
+ l = M.dataset.refKind, L = "", q = !1, A();
1516
+ });
1517
+ });
1518
+ const W = x.querySelector(".ref-value");
1519
+ W.addEventListener("input", () => {
1520
+ d = W.value, L = "", q = !1;
1521
+ }), W.addEventListener("keydown", (M) => {
1522
+ M.key === "Enter" && (M.preventDefault(), T.click());
1523
+ }), (K = x.querySelector(".ref-kind")) == null || K.addEventListener("change", (M) => {
1524
+ var c;
1525
+ l = M.target.value, L = "", A(), (c = x.querySelector(".ref-value")) == null || c.focus();
1526
+ }), (U = x.querySelector('[data-act="ref-skip"]')) == null || U.addEventListener("click", () => {
1527
+ S = !0, m = !1, l = null, d = "", L = "", q = !1, A(), i("");
1528
+ }), ((!h && d ? x.querySelector("[data-ref-kind], .ref-kind") : null) || W).focus();
1529
+ }
1530
+ function j(h) {
1531
+ if (y = h.conflicts.length ? h.conflicts : h.loose.slice(0, 4), k = h.conflicts.length ? "confirm" : h.loose.length ? "suggest" : "ask", m = !0, y.length)
1532
+ if (y.length === 1) $(y[0]);
1533
+ else {
1534
+ d = "";
1535
+ const [z] = y;
1536
+ l = y.every((H) => H.kinds.length === 1 && H.kinds[0].key === z.kinds[0].key) ? z.kinds[0].key : null;
1537
+ }
1538
+ else
1539
+ l = l || g.fields[0].key;
1540
+ A();
1541
+ }
1542
+ function B() {
1543
+ if (g.policy === "off" || !g.fields.length || p().length) return !1;
1544
+ const h = E();
1545
+ return n(h) || S && g.policy !== "required" ? !1 : h;
1546
+ }
1547
+ function N() {
1548
+ if (m && d.trim() && !l)
1549
+ return L = "Pick what this ID is.", A(), !0;
1550
+ const h = B();
1551
+ if (h)
1552
+ return m ? A() : j(h), i(""), !0;
1553
+ if (l && d.trim()) {
1554
+ const z = g.fields.find((R) => R.key === l);
1555
+ if (!q && z && !Me(z, d))
1556
+ return L = `That does not look like a ${z.label.toLowerCase()}. Press Send again to use it anyway.`, q = !0, A(), !0;
1557
+ }
1558
+ return !1;
1559
+ }
1560
+ e.holdForReference = N, e.answeredReference = p;
1561
+ }
1562
+ const ot = 3;
1563
+ function rt(e) {
1564
+ const { client: t, cfg: i, notices: w } = e;
1565
+ let b = [];
1566
+ function T() {
1567
+ const g = b.slice(0, ot);
1568
+ w.replaceChildren(...g.map((m) => Ce(m, { where: "panel", onDismiss: x }))), w.hidden = g.length === 0;
1569
+ }
1570
+ function x(g) {
1571
+ t.dismissAnnouncement(g), b = b.filter((m) => m !== g), T(), e.titleInput.focus();
1572
+ }
1573
+ i.announcements !== !1 && typeof t.announcements == "function" && t.announcements().then((g) => {
1574
+ e.closed || (b = g, T());
1575
+ }).catch(() => {
1576
+ });
1577
+ }
1578
+ function it(e) {
1579
+ const {
1580
+ client: t,
1581
+ cfg: i,
1582
+ titleInput: w,
1583
+ known: b
1584
+ } = e;
1585
+ let T = [], x = "", g = null, m = null;
1586
+ function l() {
1587
+ i.knownIssues !== !1 && t.knownIssues().then((S) => {
1588
+ T = S, y();
1589
+ }).catch(() => {
1590
+ });
1591
+ }
1592
+ let d = null;
1593
+ function L(S) {
1594
+ if (i.knownIssues === !1) return;
1595
+ clearTimeout(d), m == null || m.abort();
1596
+ const q = (S || "").trim();
1597
+ if (q.length < 3) {
1598
+ l();
1599
+ return;
1600
+ }
1601
+ d = setTimeout(() => {
1602
+ m = new AbortController(), t.matchKnownIssues(q, { signal: m.signal }).then((E) => {
1603
+ E.length && (T = E, y());
1604
+ }).catch(() => {
1605
+ });
1606
+ }, 250);
1607
+ }
1608
+ function y() {
1609
+ if (!T.length) {
1610
+ b.hidden = !0, b.innerHTML = "";
1611
+ return;
1612
+ }
1613
+ b.hidden = !1, b.innerHTML = `
1614
+ <p class="known-h">Already reported</p>
1615
+ ${T.map((S) => `
1616
+ <div class="issue ${S.joined ? "is-joined" : ""}">
1617
+ <span class="who">
1618
+ <span class="t">${v(S.title)}</span>
1619
+ <span class="meta">${v(k(S))}</span>
1620
+ </span>
1621
+ <button class="join" type="button" data-join="${v(S.id)}"
1622
+ ${S.joined ? "disabled" : ""}>${S.joined ? "Following" : "Me too"}</button>
1623
+ </div>
1624
+ ${g === S.id ? `
1625
+ <div class="ask">
1626
+ <input type="email" class="join-email" placeholder="you@example.com"
1627
+ aria-label="Your email, so we can tell you when this is fixed" />
1628
+ <button class="ghost" type="button" data-join-confirm="${v(S.id)}">Follow</button>
1629
+ </div>` : ""}
1630
+ `).join("")}
1631
+ ${x ? `<p class="err">${v(x)}</p>` : ""}`;
1632
+ }
1633
+ function k(S) {
1634
+ const q = S.status === "IN_PROGRESS" ? "Being worked on now" : "Not fixed yet", E = S.affectedCount === 0 ? "" : ` · ${S.affectedCount} ${S.affectedCount === 1 ? "person has" : "people have"} said this is happening to them`;
1635
+ return `${q} · reported ${ne(S.since)}${E}`;
1636
+ }
1637
+ b.addEventListener("click", async (S) => {
1638
+ var a, s, u, r, o;
1639
+ const q = S.target.closest("[data-join]"), E = S.target.closest("[data-join-confirm]");
1640
+ if (!q && !E) return;
1641
+ const n = (E || q).getAttribute(E ? "data-join-confirm" : "data-join");
1642
+ x = "";
1643
+ const p = ((s = (a = b.querySelector(".join-email")) == null ? void 0 : a.value) == null ? void 0 : s.trim()) || ((u = t.signedInAs()) == null ? void 0 : u.email) || ((r = t.config.user) == null ? void 0 : r.email);
1644
+ if (!p) {
1645
+ g = n, y(), (o = b.querySelector(".join-email")) == null || o.focus();
1646
+ return;
1647
+ }
1648
+ try {
1649
+ const f = await t.meToo(n, { email: p });
1650
+ g = null, T = T.map(($) => $.id === n ? { ...$, joined: !0, affectedCount: f.affectedCount ?? $.affectedCount } : $), y();
1651
+ } catch (f) {
1652
+ x = f.message.replace("[BugCatch] ", ""), y();
1653
+ }
1654
+ }), l(), w.addEventListener("input", () => L(w.value));
1655
+ }
1656
+ function st(e) {
1657
+ const {
1658
+ client: t,
1659
+ cfg: i,
1660
+ titleInput: w,
1661
+ descInput: b,
1662
+ picks: T
1663
+ } = e;
1664
+ let x = [], g = null;
1665
+ function m() {
1666
+ if (!x.length) {
1667
+ T.hidden = !0, T.innerHTML = "";
1668
+ return;
1669
+ }
1670
+ T.hidden = !1, T.innerHTML = `
1671
+ <p class="picks-h">Get an answer right away</p>
1672
+ <div class="picks-grid" role="group" aria-label="Common questions">
1673
+ ${x.map((l, d) => `
1674
+ <button type="button" class="pick" data-pick="${v(l.key)}"
1675
+ ${l.hint ? `title="${v(l.hint)}" aria-describedby="bc-pick-h${d}"` : ""}
1676
+ aria-pressed="${(g == null ? void 0 : g.key) === l.key}">
1677
+ <span class="t">${v(l.title)}</span>
1678
+ ${l.hint ? `<span class="h sr-only" id="bc-pick-h${d}">${v(l.hint)}</span>` : ""}
1679
+ </button>`).join("")}
1680
+ </div>
1681
+ ${g ? `<p class="picks-note">${F}<span>Our assistant will check your account and reply here. Add anything else below, then send.</span></p>` : ""}`;
1682
+ }
1683
+ T.addEventListener("click", (l) => {
1684
+ const d = l.target.closest("[data-pick]");
1685
+ if (!d) return;
1686
+ const L = x.find((k) => k.key === d.dataset.pick);
1687
+ if (!L) return;
1688
+ const y = g && w.value.trim() === g.title;
1689
+ (g == null ? void 0 : g.key) === L.key ? (g = null, y && (w.value = "")) : (g = L, (!w.value.trim() || y) && (w.value = L.title)), w.dispatchEvent(new Event("input", { bubbles: !0 })), m(), g && b.focus();
1690
+ }), i.agentIssues !== !1 && typeof t.agentIssues == "function" && t.agentIssues().then((l) => {
1691
+ x = l, m();
1692
+ }).catch(() => {
1693
+ }), e.pickedIssue = () => g;
1694
+ }
1695
+ function lt(e) {
1696
+ const {
1697
+ client: t,
1698
+ scrim: i,
1699
+ say: w,
1700
+ callBtn: b,
1701
+ close: T
1702
+ } = e;
1703
+ let x = !1;
1704
+ function g() {
1705
+ t.callKind().then((d) => {
1706
+ d && (x = !0, e.preloadCall(), b.hidden = e.mustSignIn());
1707
+ }).catch(() => {
1708
+ });
1709
+ }
1710
+ g(), b.addEventListener("click", async () => {
1711
+ e.sending = !0, w(""), e.refreshSendState(), b.disabled = !0, b.innerHTML = '<span class="spinner"></span>Ringing…';
1712
+ try {
1713
+ const d = await t.startCall();
1714
+ m(d);
1715
+ } catch (d) {
1716
+ e.sending = !1, b.disabled = !1, b.innerHTML = `${ae}Talk to the team`, e.refreshSendState(), d.filed ? e.succeed(d.filed, { callFailed: d.reason || "The call could not be started." }) : (w(d.message.replace("[BugCatch] ", "")), e.recheckSignIn());
1717
+ }
1718
+ });
1719
+ function m(d) {
1720
+ const L = i.querySelector(".panel"), y = d != null && d.bugId ? String(d.bugId).slice(0, 8) : null;
1721
+ L.innerHTML = `
1722
+ <div class="call">
1723
+ <div class="call-head">
1724
+ <span class="call-dot" aria-hidden="true"></span>
1725
+ <strong class="call-state">Ringing…</strong>
1726
+ </div>
1727
+ <p class="call-note">
1728
+ We're ringing the team now. Someone will pick up if they're free — your report${y ? ` <code>${v(y)}</code>` : ""} is already filed either way, so nothing is lost if nobody answers.
1729
+ </p>
1730
+ <div class="foot">
1731
+ <span class="spacer"></span>
1732
+ <button class="primary" type="button" data-act="hangup">Cancel</button>
1733
+ </div>
1734
+ </div>`;
1735
+ const k = L.querySelector(".call-state"), S = L.querySelector(".call-dot"), q = {
1736
+ ringing: "Ringing the team…",
1737
+ connecting: "Someone answered — connecting…",
1738
+ connected: "Connected",
1739
+ missed: "Nobody was free",
1740
+ ended: "Call ended",
1741
+ failed: "The call dropped"
1742
+ }, E = ["ended", "failed", "missed"], n = t.on("call", ({ state: p }) => {
1743
+ if (k.textContent = q[p] || p, S.classList.toggle("off", E.includes(p)), E.includes(p) && (L.querySelector('[data-act="hangup"]').textContent = "Close", p === "missed")) {
1744
+ const a = L.querySelector(".call-note");
1745
+ a && (a.textContent = "Nobody was free to pick up just now. Your report is filed and the team will reply in the chat.");
1746
+ }
1747
+ });
1748
+ L.querySelector('[data-act="hangup"]').addEventListener("click", () => {
1749
+ n(), t.endCall(), T();
1750
+ });
1751
+ }
1752
+ function l(d, L, y) {
1753
+ const k = document.createElement("div");
1754
+ k.className = "livecall", k.innerHTML = `
1755
+ <span class="call-dot" aria-hidden="true"></span>
1756
+ <span class="who" role="status" aria-live="polite">Calling the team…</span>
1757
+ <span class="secs" hidden></span>
1758
+ ${y != null && y.recorded ? '<span class="call-kept">This call is recorded.</span>' : ""}
1759
+ <button class="act" type="button" data-act="mute" hidden>Mute</button>
1760
+ <button class="act danger" type="button" data-act="hangup">Cancel</button>`, d.insertBefore(k, L);
1761
+ const S = k.querySelector(".who"), q = k.querySelector(".secs"), E = k.querySelector(".call-dot"), n = k.querySelector('[data-act="mute"]'), p = k.querySelector('[data-act="hangup"]');
1762
+ let a = null;
1763
+ const s = () => {
1764
+ a && (clearInterval(a), a = null);
1765
+ }, u = (o) => {
1766
+ s(), E.classList.add("off"), k.classList.add("over"), S.textContent = o, q.hidden = !0, n.hidden = !0, p.textContent = "Dismiss", p.classList.remove("danger");
1767
+ }, r = t.on("call", ({ state: o }) => {
1768
+ if (o === "ringing" && (S.textContent = "Calling the team…"), o === "connecting" && (S.textContent = "Someone answered — connecting…"), o === "connected") {
1769
+ S.textContent = "You're on a call with the team", n.hidden = !1, q.hidden = !1;
1770
+ const f = Date.now();
1771
+ s(), a = setInterval(() => {
1772
+ q.textContent = ee(Math.floor((Date.now() - f) / 1e3));
1773
+ }, 1e3), q.textContent = "0s";
1774
+ }
1775
+ o === "missed" && u("Nobody was free. Send a message instead and the team will pick it up."), o === "ended" && u("Call ended."), o === "failed" && u("The call dropped.");
1776
+ });
1777
+ n.addEventListener("click", () => {
1778
+ const o = y.setMuted(!y.isMuted());
1779
+ n.textContent = o ? "Unmute" : "Mute", S.textContent = o ? "You're muted" : "You're on a call with the team";
1780
+ }), p.addEventListener("click", () => {
1781
+ r(), s(), t.endCall(), k.remove(), e.refreshThread(e.openReportId());
1782
+ });
1783
+ }
1784
+ e.offerCall = g, e.callOffered = () => x, e.showLiveCall = l;
1785
+ }
1786
+ function ct(e) {
1787
+ var r;
1788
+ const {
1789
+ client: t,
1790
+ say: i,
1791
+ compose: w,
1792
+ sendBtn: b,
1793
+ historyBtn: T,
1794
+ callBtn: x,
1795
+ whoami: g
1796
+ } = e;
1797
+ let m = { mode: t.lastLoginMode || "off", signedIn: null }, l = "idle", d = ((r = t.config.user) == null ? void 0 : r.email) || "", L = "", y = !1, k = "";
1798
+ const S = () => m.mode === "required" && !m.signedIn;
1799
+ function q() {
1800
+ const o = S();
1801
+ w.hidden = o, b.hidden = o, T && (T.hidden = o), o ? x.hidden = !0 : e.callOffered() && (x.hidden = !1);
1802
+ }
1803
+ function E() {
1804
+ var f;
1805
+ if (m.mode === "off") {
1806
+ g.hidden = !0, g.innerHTML = "";
1807
+ return;
1808
+ }
1809
+ g.hidden = !1, g.classList.toggle("asking", l !== "idle");
1810
+ const o = k ? `<div class="after err">${v(k)}</div>` : "";
1811
+ if (l === "idle") {
1812
+ if (m.signedIn) {
1813
+ g.innerHTML = `<span class="who">Signed in as <strong>${v(m.signedIn.email)}</strong></span><button type="button" class="link" data-act="signout">Sign out</button>`;
1814
+ return;
1815
+ }
1816
+ const $ = (f = t.config.user) == null ? void 0 : f.email;
1817
+ g.innerHTML = `<span class="who">${m.mode === "required" ? "Sign in to report a bug here." : $ ? `Reporting as <strong>${v($)}</strong>.` : "Reporting anonymously — sign in and we can tell you what happens next."}</span><button type="button" class="link" data-act="signin">Sign in</button>`;
1818
+ return;
1819
+ }
1820
+ if (l === "email") {
1821
+ g.innerHTML = `
1822
+ <p class="why">Sign in with any email address — we'll send you a code.
1823
+ It's how you hear back about this report${m.mode === "required" ? ", and this project asks for it" : ""}.</p>
1824
+ <div class="row">
1825
+ <input type="email" data-fld="email" value="${v(d)}"
1826
+ placeholder="you@example.com" autocomplete="email" spellcheck="false"
1827
+ aria-label="Your email address" ${y ? "disabled" : ""} />
1828
+ <button type="button" class="primary" data-act="code" ${y ? "disabled" : ""}>
1829
+ ${y ? "Sending…" : "Send code"}
1830
+ </button>
1831
+ </div>
1832
+ ${o}
1833
+ ${m.mode === "optional" ? '<div class="after"><button type="button" class="link" data-act="skip">Not now</button></div>' : ""}`;
1834
+ return;
1835
+ }
1836
+ g.innerHTML = `
1837
+ <p class="why">We sent a code to <strong>${v(d)}</strong>. It expires in ten minutes.</p>
1838
+ <div class="row">
1839
+ <input type="text" class="code" data-fld="code" value="${v(L)}"
1840
+ placeholder="123456" inputmode="numeric" autocomplete="one-time-code"
1841
+ maxlength="8" aria-label="The code we emailed you" ${y ? "disabled" : ""} />
1842
+ <button type="button" class="primary" data-act="confirm" ${y ? "disabled" : ""}>
1843
+ ${y ? "Checking…" : "Sign in"}
1844
+ </button>
1845
+ </div>
1846
+ ${o}
1847
+ <div class="after">
1848
+ <button type="button" class="link" data-act="resend" ${y ? "disabled" : ""}>Send it again</button>
1849
+ <button type="button" class="link" data-act="signin-back" ${y ? "disabled" : ""}>Use another address</button>
1850
+ </div>`;
1851
+ }
1852
+ function n({ focus: o = !1 } = {}) {
1853
+ var $;
1854
+ if (E(), q(), !o || y) return;
1855
+ const f = g.querySelector("[data-fld]");
1856
+ f && (f.focus(), ($ = f.setSelectionRange) == null || $.call(f, f.value.length, f.value.length));
1857
+ }
1858
+ g.addEventListener("input", (o) => {
1859
+ const f = o.target.closest("[data-fld]");
1860
+ f && (f.dataset.fld === "email" ? d = f.value : L = f.value);
1861
+ }), g.addEventListener("keydown", (o) => {
1862
+ o.key !== "Enter" || !o.target.closest("[data-fld]") || (o.preventDefault(), l === "email" ? p() : a());
1863
+ });
1864
+ async function p({ resent: o = !1 } = {}) {
1865
+ const f = d.trim();
1866
+ if (!f) {
1867
+ k = "Enter the address to send the code to.", n({ focus: !0 });
1868
+ return;
1869
+ }
1870
+ y = !0, k = "", n();
1871
+ try {
1872
+ await t.signIn(f), d = f, o || (L = ""), l = "code";
1873
+ } catch ($) {
1874
+ k = $.message;
1875
+ } finally {
1876
+ y = !1, n({ focus: !0 });
1877
+ }
1878
+ }
1879
+ async function a() {
1880
+ const o = L.trim();
1881
+ if (!o) {
1882
+ k = "Enter the code we emailed you.", n({ focus: !0 });
1883
+ return;
1884
+ }
1885
+ y = !0, k = "", n();
1886
+ try {
1887
+ const f = await t.confirmSignIn(d.trim(), o);
1888
+ m.signedIn = f, l = "idle", L = "", i(""), e.offerCall();
1889
+ } catch (f) {
1890
+ k = f.message;
1891
+ } finally {
1892
+ y = !1, n({ focus: l !== "idle" });
1893
+ }
1894
+ }
1895
+ g.addEventListener("click", async (o) => {
1896
+ const f = o.target.closest("button[data-act]");
1897
+ if (!f) return;
1898
+ const $ = f.dataset.act;
1899
+ if ($ === "signin") {
1900
+ l = "email", k = "", n({ focus: !0 });
1901
+ return;
1902
+ }
1903
+ if ($ === "skip") {
1904
+ l = "idle", k = "", n();
1905
+ return;
1906
+ }
1907
+ if ($ === "signin-back") {
1908
+ l = "email", k = "", L = "", n({ focus: !0 });
1909
+ return;
1910
+ }
1911
+ if ($ === "code") {
1912
+ p();
1913
+ return;
1914
+ }
1915
+ if ($ === "resend") {
1916
+ p({ resent: !0 });
1917
+ return;
1918
+ }
1919
+ if ($ === "confirm") {
1920
+ a();
1921
+ return;
1922
+ }
1923
+ if ($ === "signout") {
1924
+ y = !0, n();
1925
+ try {
1926
+ await t.signOut();
1927
+ } finally {
1928
+ m.signedIn = null, y = !1, l = m.mode === "required" ? "email" : "idle", n({ focus: l !== "idle" });
1929
+ }
1930
+ }
1931
+ });
1932
+ async function s({ announce: o = !1 } = {}) {
1933
+ const f = await t.loginState();
1934
+ m = { mode: f.mode, signedIn: f.signedIn }, t.lastLoginMode = f.mode, S() && l === "idle" && (l = "email"), m.signedIn && (l = "idle"), o && f.signedOutReason && i(f.signedOutReason, "info"), n();
1935
+ }
1936
+ s({ announce: !0 }).catch(() => {
1937
+ });
1938
+ function u() {
1939
+ m.mode !== "off" && s().catch(() => {
1940
+ });
1941
+ }
1942
+ e.mustSignIn = S, e.recheckSignIn = u;
1943
+ }
1944
+ function ue(e) {
1945
+ const t = e || [];
1946
+ if (!t.length) return "";
1947
+ let i = null, w = null;
1948
+ return t.map((b) => {
1949
+ var d, L, y, k;
1950
+ const T = Z(b.at), x = T && T !== i ? `<div class="day-sep" role="separator">${v(_e(b.at))}</div>` : "";
1951
+ if (x && (i = T, w = null), !b.from)
1952
+ return w = null, x + pt(b);
1953
+ const g = {
1954
+ from: b.from,
1955
+ author: (d = b.reply) == null ? void 0 : d.author,
1956
+ automated: (L = b.reply) == null ? void 0 : L.automated,
1957
+ body: (y = b.reply) == null ? void 0 : y.body,
1958
+ at: b.at,
1959
+ attachments: (k = b.reply) == null ? void 0 : k.attachments
1960
+ }, m = dt(g), l = w && w.who === m && Math.abs(new Date(b.at).getTime() - new Date(w.at).getTime()) < 5 * 6e4;
1961
+ return w = { who: m, at: b.at }, x + we(g, l ? "cont" : "");
1962
+ }).join("");
1963
+ }
1964
+ function dt(e) {
1965
+ return e.from === "reporter" ? "me" : e.automated ? "assistant" : `team:${e.author || ""}`;
1966
+ }
1967
+ function pt(e) {
1968
+ const t = ve(e.at);
1969
+ return `
1970
+ <div class="sys k-${v(e.kind)}">
1971
+ <span class="dot" aria-hidden="true"></span>${v(e.summary || "")}${t ? `
1972
+ <span class="t">· ${v(t)}</span>` : ""}
1973
+ </div>`;
1974
+ }
1975
+ function we(e, t = "") {
1976
+ const i = e.from === "reporter", w = e.body || "", b = e.attachments || [], T = !i && !!e.automated, x = w ? `<span class="msg-text">${v(w)}</span>` : "", g = b.length ? `<div class="att">${b.map(ht).join("")}</div>` : "";
1977
+ let m = "", l = "";
1978
+ if (T)
1979
+ m = '<span class="msg-who auto">Assistant <span class="tag">AI</span></span>', l = '<span class="av bot orb" aria-hidden="true"></span>';
1980
+ else if (!i) {
1981
+ const L = e.author || "Support team";
1982
+ m = `<span class="msg-who">${v(L)}</span>`, l = `<span class="av" aria-hidden="true">${v(Ze(L))}</span>`;
1983
+ }
1984
+ t.includes("cont") && (m = "");
1985
+ const d = T && /it goes to the same person/i.test(w) ? `<div class="quick" role="group" aria-label="Answer">
1986
+ <button type="button" data-quick="Yes, this is the problem">${F}That's right</button>
1987
+ <button type="button" data-quick="No, that is not the problem">That's not it</button>
1988
+ </div>` : T && He.test(w) ? `<div class="quick" role="group" aria-label="Did this answer your question?">
1989
+ <button type="button" data-quick="Yes, that answers it">${F}Yes, that answers it</button>
1990
+ <button type="button" data-quick="No, I still need help">No, I still need help</button>
1991
+ </div>` : "";
1992
+ return `
1993
+ <div class="msg-row ${i ? "mine" : "theirs"} ${T ? "auto" : ""} ${t}">
1994
+ ${l}
1995
+ ${m}
1996
+ <div class="bubble ${w ? "" : "silent"}">${x}${g}</div>
1997
+ ${d}
1998
+ <span class="msg-when">${v(ve(e.at) || ne(e.at))}</span>
1999
+ </div>`;
2000
+ }
2001
+ function ut() {
2002
+ return `
2003
+ <div class="typing-row">
2004
+ <span class="av bot orb" aria-hidden="true"></span>
2005
+ <span class="msg-who auto">Assistant <span class="tag">AI</span></span>
2006
+ <div class="bubble typing" role="img" aria-label="The assistant is working on it"><i></i><i></i><i></i></div>
2007
+ </div>`;
2008
+ }
2009
+ function ht(e) {
2010
+ const t = v(e.name || "attachment");
2011
+ if (!e.url) return `<span class="file"><span class="nm">${t}</span></span>`;
2012
+ const i = v(e.url);
2013
+ return e.type === "SCREENSHOT" ? `<a href="${i}" target="_blank" rel="noopener noreferrer"><img src="${i}" alt="${t}" loading="lazy"></a>` : e.type === "AUDIO" ? `<audio controls preload="metadata" src="${i}"></audio>` : e.type === "VIDEO" ? `<video controls preload="metadata" src="${i}"></video>` : `
2014
+ <a class="file" href="${i}" target="_blank" rel="noopener noreferrer" download>
2015
+ <span class="nm">${t}</span>
2016
+ ${e.size ? `<span class="sz">${v(P(e.size))}</span>` : ""}
2017
+ </a>`;
2018
+ }
2019
+ function bt(e, t, i, w) {
2020
+ var u;
2021
+ const { client: b, cfg: T } = e, x = document.createElement("div");
2022
+ x.className = "composer", x.innerHTML = `
2023
+ <div class="staged" hidden></div>
2024
+ <div class="row">
2025
+ <div class="tools">
2026
+ ${T.attachments === !1 ? "" : `
2027
+ <button class="tool" type="button" data-act="attach" title="Attach a file" aria-label="Attach a file">${fe}</button>
2028
+ <button class="tool" type="button" data-act="voice" title="Record a voice note" aria-label="Record a voice note">${de}</button>`}
2029
+ <button class="tool" type="button" data-act="livecall" title="Call the team" aria-label="Call the team" hidden>${ae}</button>
2030
+ </div>
2031
+ <textarea rows="1" placeholder="Write a reply…" aria-label="Write a reply"></textarea>
2032
+ <button class="send" type="button" data-act="send" title="Send" aria-label="Send" disabled>${ge}</button>
2033
+ </div>
2034
+ <div class="note" hidden></div>
2035
+ <input type="file" hidden multiple accept="${Ae.join(",")}">`, t.appendChild(x);
2036
+ const g = x.querySelector(".staged"), m = x.querySelector("textarea"), l = x.querySelector('[data-act="send"]'), d = x.querySelector('[data-act="voice"]'), L = x.querySelector('[data-act="livecall"]'), y = x.querySelector("input[type=file]"), k = x.querySelector(".note"), S = [];
2037
+ let q = null, E = !1;
2038
+ function n(r, o = "") {
2039
+ k.hidden = !r, k.className = `note ${o}`, k.textContent = r || "";
2040
+ }
2041
+ function p() {
2042
+ const r = S.filter((o) => o.storageKey);
2043
+ l.disabled = E || !m.value.trim() && !r.length, g.hidden = !S.length, g.innerHTML = S.map((o, f) => `
2044
+ <span class="chip ${o.storageKey ? "" : "uploading"}">
2045
+ <span class="nm">${v(o.name)}</span>
2046
+ <span class="sz">${o.storageKey ? P(o.size) : "sending…"}</span>
2047
+ <button type="button" data-drop="${f}" aria-label="Remove ${v(o.name)}">${ce}</button>
2048
+ </span>`).join("");
2049
+ for (const o of g.querySelectorAll("[data-drop]"))
2050
+ o.addEventListener("click", () => {
2051
+ S.splice(Number(o.dataset.drop), 1), p();
2052
+ });
2053
+ }
2054
+ async function a(r, o, f) {
2055
+ const $ = { name: f, size: r.size, storageKey: null };
2056
+ S.push($), p();
2057
+ try {
2058
+ const { storageKey: A, mime: j } = await b.uploadAttachment(r, o);
2059
+ $.storageKey = A, $.mime = j;
2060
+ } catch (A) {
2061
+ S.splice(S.indexOf($), 1), n(A.message || "That file could not be sent.", "err");
2062
+ }
2063
+ p();
2064
+ }
2065
+ m.addEventListener("input", () => {
2066
+ m.style.height = "auto", m.style.height = `${Math.min(m.scrollHeight, 120)}px`, p();
2067
+ }), m.addEventListener("keydown", (r) => {
2068
+ r.key === "Enter" && !r.shiftKey && !r.isComposing && (r.preventDefault(), s());
2069
+ }), (u = x.querySelector('[data-act="attach"]')) == null || u.addEventListener("click", () => y.click()), y.addEventListener("change", () => {
2070
+ n("");
2071
+ for (const r of y.files) {
2072
+ if (r.size > b.config.maxAttachmentBytes) {
2073
+ n(`${r.name} is larger than ${P(b.config.maxAttachmentBytes)}.`, "err");
2074
+ continue;
2075
+ }
2076
+ a(r, r.type, r.name);
2077
+ }
2078
+ y.value = "";
2079
+ }), d && !Ie() && (d.disabled = !0, d.title = "This browser cannot record audio"), d == null || d.addEventListener("click", async () => {
2080
+ if (q) {
2081
+ const r = q;
2082
+ q = null, d.classList.remove("recording"), d.innerHTML = de, n("");
2083
+ const { blob: o, mime: f } = await r.stop();
2084
+ o.size && a(o, f, "Voice note");
2085
+ return;
2086
+ }
2087
+ try {
2088
+ n(""), q = await Re({
2089
+ maxMs: b.config.maxRecordingMs,
2090
+ onTick: (r) => n(`Recording — ${ee(r)}. Tap the mic to stop.`),
2091
+ onAutoStop: () => d.click()
2092
+ }), d.classList.add("recording"), d.innerHTML = Oe;
2093
+ } catch {
2094
+ q = null, n("The microphone is not available — check the browser permission.", "err");
2095
+ }
2096
+ }), b.canCallTeam(w).then((r) => {
2097
+ r && (L.hidden = !1, e.preloadCall());
2098
+ }).catch(() => {
2099
+ }), L.addEventListener("click", async () => {
2100
+ L.disabled = !0, n("");
2101
+ try {
2102
+ const r = await b.startLiveCall(w);
2103
+ e.showLiveCall(t, i, r);
2104
+ } catch (r) {
2105
+ n(r.message.replace("[BugCatch] ", ""), "err");
2106
+ } finally {
2107
+ L.disabled = !1;
2108
+ }
2109
+ }), l.addEventListener("click", s), i.addEventListener("click", (r) => {
2110
+ var f, $;
2111
+ const o = ($ = (f = r.target).closest) == null ? void 0 : $.call(f, "[data-quick]");
2112
+ !o || E || (m.value = o.dataset.quick, s());
2113
+ });
2114
+ async function s() {
2115
+ var f;
2116
+ const r = m.value.trim(), o = S.filter(($) => $.storageKey);
2117
+ if (!(E || !r && !o.length)) {
2118
+ E = !0, p(), n(""), i.insertAdjacentHTML("beforeend", we({
2119
+ from: "reporter",
2120
+ body: r,
2121
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2122
+ attachments: o.map(($) => ({ type: "FILE", name: $.name, size: $.size }))
2123
+ }, "pending")), _(i, { smooth: !0 });
2124
+ try {
2125
+ await b.replyToReport(w, {
2126
+ body: r,
2127
+ attachments: o.map(($) => ({
2128
+ storageKey: $.storageKey,
2129
+ mime: $.mime,
2130
+ name: $.name,
2131
+ size: $.size
2132
+ }))
2133
+ }), m.value = "", m.style.height = "auto", S.length = 0, await e.refreshThread(w);
2134
+ } catch ($) {
2135
+ (f = i.querySelector(".msg-row.pending")) == null || f.classList.replace("pending", "failed"), n($.message || "That could not be sent — try again.", "err");
2136
+ } finally {
2137
+ E = !1, p();
2138
+ }
2139
+ }
2140
+ }
2141
+ p();
2142
+ }
2143
+ const se = /* @__PURE__ */ new WeakMap();
2144
+ function he(e) {
2145
+ return JSON.stringify(e ? [e.score, e.current, e.comment || null] : null);
2146
+ }
2147
+ function ft(e) {
2148
+ return e.rating ? ` · <span class="rated" aria-label="You rated this ${Number(e.rating)} out of 5">${me}${Number(e.rating)}</span>` : e.ratingAsked ? ' · <span class="rate-ask">Rate it</span>' : "";
2149
+ }
2150
+ function gt(e) {
2151
+ var E;
2152
+ const {
2153
+ client: t,
2154
+ scrim: i,
2155
+ close: w
2156
+ } = e;
2157
+ let b = null;
2158
+ function T(n, { callFailed: p = null } = {}) {
2159
+ if (n != null && n.id && !p) {
2160
+ m(n.id, { fresh: !0 }).then((a) => {
2161
+ a || x(n, { callFailed: p });
2162
+ });
2163
+ return;
2164
+ }
2165
+ x(n, { callFailed: p });
2166
+ }
2167
+ function x(n, { callFailed: p = null } = {}) {
2168
+ var r;
2169
+ const a = n != null && n.id ? String(n.id).slice(0, 8) : null, s = (r = n == null ? void 0 : n.estimate) != null && r.label ? String(n.estimate.label) : null, u = p ? `<p class="eta">${v(p)} Your report is filed, and the team will reply in the chat.</p>` : "";
2170
+ i.querySelector(".panel").innerHTML = `
2171
+ <div class="done">
2172
+ <div class="tick">${F}</div>
2173
+ <h3>Thanks — that's on its way.</h3>
2174
+ <p>${a ? `Filed as <code>${v(a)}</code>.` : "The team has what they need to reproduce it."}</p>
2175
+ ${u}
2176
+ ${s ? `<p class="eta">Reports like this usually take <strong>${v(s)}</strong>. We'll let you know when it moves.</p>` : ""}
2177
+ </div>`, setTimeout(w, u ? 6e3 : s ? 4e3 : 2e3);
2178
+ }
2179
+ async function g() {
2180
+ const n = i.querySelector(".panel");
2181
+ clearTimeout(l), b = null, n.classList.remove("is-chat"), n.innerHTML = `
2182
+ ${le({ title: "Your reports", sub: "Where each one has got to.", theme: e.themeLabel() })}
2183
+ <div class="history" aria-busy="true">${Ke(3)}</div>`, n.querySelector('[data-act="close"]').addEventListener("click", w);
2184
+ let p = [];
2185
+ try {
2186
+ ({ reports: p } = await t.myReports());
2187
+ } catch {
2188
+ const s = n.querySelector(".history");
2189
+ s && (s.innerHTML = re("Couldn't load your reports just now.", G));
2190
+ return;
2191
+ }
2192
+ const a = n.querySelector(".history");
2193
+ if (a) {
2194
+ a.removeAttribute("aria-busy"), a.innerHTML = p.length ? p.map((s, u) => `
2195
+ <button class="row" type="button" data-report="${v(s.id)}" style="animation-delay:${Math.min(u, 8) * 30}ms">
2196
+ <span class="row-ico" aria-hidden="true">${De}</span>
2197
+ <span class="who">
2198
+ <span class="ttl">${v(s.title)}</span>
2199
+ <span class="sub">${v(s.reference)} · ${v(ne(s.createdAt))}${ft(s)}</span>
2200
+ </span>
2201
+ <span class="pill ${v(String(s.status).toLowerCase())}">${v(ie(s.status))}</span>
2202
+ <span class="chev" aria-hidden="true">${We}</span>
2203
+ </button>`).join("") : re("You haven't reported anything yet.", Ye);
2204
+ for (const s of a.querySelectorAll("[data-report]"))
2205
+ s.addEventListener("click", () => m(s.dataset.report));
2206
+ }
2207
+ }
2208
+ async function m(n, { fresh: p = !1 } = {}) {
2209
+ var f;
2210
+ const a = i.querySelector(".panel");
2211
+ clearTimeout(l), a.classList.add("is-chat"), a.innerHTML = `
2212
+ ${le({
2213
+ back: !0,
2214
+ title: p ? "Looking into it" : "Your report",
2215
+ sub: p ? "It may ask you a question here." : "Updates, and a way to reply.",
2216
+ theme: e.themeLabel()
2217
+ })}
2218
+ <div class="history chatting" aria-busy="true"><div class="thread">${Ue()}</div></div>`, a.querySelector('[data-act="close"]').addEventListener("click", w), a.querySelector('[data-act="back"]').addEventListener("click", g);
2219
+ let s = null;
2220
+ try {
2221
+ s = await t.myReport(n);
2222
+ } catch {
2223
+ s = null;
2224
+ }
2225
+ const u = a.querySelector(".history");
2226
+ if (!u || !a.querySelector('[data-act="back"]')) return !1;
2227
+ if (!s)
2228
+ return p ? (a.classList.remove("is-chat"), !1) : (u.removeAttribute("aria-busy"), u.innerHTML = re("Couldn't load that report just now.", G), !1);
2229
+ b = n, t.markReportRead(n);
2230
+ const r = ue(s.timeline);
2231
+ u.classList.add("chatting"), u.removeAttribute("aria-busy"), u.innerHTML = `
2232
+ <div class="report-head">
2233
+ <span class="ttl">${v(s.title)}</span>
2234
+ <span class="sub">${v(s.reference)} · ${v(s.project || "")}</span>
2235
+ <span class="pill ${v(String(s.status).toLowerCase())}">${v(ie(s.status))}</span>
2236
+ ${(f = s.estimate) != null && f.label ? `<span class="eta-line">Usually takes ${v(String(s.estimate.label))}</span>` : ""}
2237
+ </div>
2238
+ <div class="thread" role="log" aria-live="polite" aria-label="Conversation">${r}</div>`;
2239
+ const o = u.querySelector(".thread");
2240
+ return se.set(o, r), _(o), L(u, s), y(u, s), s.canReply && bt(e, u, o, n), d(u, s, n), _(o), !0;
2241
+ }
2242
+ let l = null;
2243
+ function d(n, p, a) {
2244
+ var h, z, R;
2245
+ clearTimeout(l);
2246
+ const s = p.assistant, u = n.querySelector(".thread");
2247
+ let r = n.querySelector(".assistant");
2248
+ if (!s) {
2249
+ r == null || r.remove(), (h = u == null ? void 0 : u.querySelector(".typing-row")) == null || h.remove();
2250
+ return;
2251
+ }
2252
+ const o = (r == null ? void 0 : r.dataset.phase) || null;
2253
+ r || (r = document.createElement("div"), r.setAttribute("role", "status"), r.setAttribute("aria-live", "polite"), (z = n.querySelector(".report-head")) == null || z.after(r)), r.className = `assistant ${v(s.phase)}`, r.dataset.phase = s.phase;
2254
+ const f = s.steps || [], $ = f.filter((H) => H.state === "done").length, A = f.some((H) => H.state === "active") ? 0.5 : 0, j = f.length ? Math.round(($ + A) / f.length * 100) : 0, B = `
2255
+ <div class="as-body${o && o !== s.phase ? " entering" : ""}">
2256
+ <div class="as-top">
2257
+ <span class="as-orb${s.phase === "calling" || s.phase === "resolved" ? "" : " orb"}" aria-hidden="true">${s.phase === "calling" ? ae : s.phase === "resolved" ? F : ""}</span>
2258
+ <div class="as-text">
2259
+ <div class="as-title">${v(s.title)}</div>
2260
+ <div class="as-detail">${v(s.detail)}</div>
2261
+ </div>
2262
+ </div>
2263
+ </div>
2264
+ ${f.length ? `
2265
+ <div class="as-progress" role="progressbar" aria-label="Progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${j}"><i style="width:${j}%"></i></div>
2266
+ <ol class="as-steps">${f.map((H) => `<li class="${v(H.state)}"${H.state === "active" ? ' aria-current="step"' : ""}><span class="dot"></span><span class="lbl">${v(H.label)}</span></li>`).join("")}</ol>` : ""}`, N = JSON.stringify([s.phase, s.title, s.detail, f]);
2267
+ if (r.dataset.key !== N) {
2268
+ const H = r.querySelector(".as-progress i"), D = H == null ? void 0 : H.style.width;
2269
+ r.innerHTML = B, r.dataset.key = N;
2270
+ const O = r.querySelector(".as-progress i");
2271
+ O && D && (O.style.width = D, requestAnimationFrame(() => {
2272
+ O.style.width = `${j}%`;
2273
+ }));
2274
+ }
2275
+ if (u) {
2276
+ const H = pe(u), D = u.querySelector(".typing-row");
2277
+ s.phase !== "analyzing" ? D == null || D.remove() : (!D || D !== u.lastElementChild) && (D == null || D.remove(), u.insertAdjacentHTML("beforeend", ut()), H && _(u));
2278
+ }
2279
+ s.phase === "analyzing" ? l = setTimeout(() => S(a), 3e3) : s.phase === "calling" ? l = setTimeout(() => S(a), 1e4) : s.phase === "needs_info" && o !== "needs_info" && ((R = n.querySelector(".composer textarea")) == null || R.focus());
2280
+ }
2281
+ function L(n, p) {
2282
+ var s, u;
2283
+ if ((s = n.querySelector(".fix-verification")) == null || s.remove(), !p.resolvedAt || !["RESOLVED", "CLOSED"].includes(p.status)) return;
2284
+ const a = document.createElement("div");
2285
+ a.className = "fix-verification", p.verification ? a.textContent = p.verification === "CONFIRMED" ? "You confirmed that this fix works." : "Your feedback was recorded." : (a.innerHTML = '<p>Does the fix work for you?</p><textarea maxlength="4000" aria-label="Fix feedback" placeholder="What happens when you try again? (optional)"></textarea><button type="button" data-result="CONFIRMED">It works now</button> <button type="button" data-result="STILL_HAPPENING">Still happening</button><p class="feedback-status" role="status"></p>', a.querySelectorAll("[data-result]").forEach((r) => {
2286
+ r.onclick = async () => {
2287
+ a.querySelectorAll("button").forEach((o) => {
2288
+ o.disabled = !0;
2289
+ });
2290
+ try {
2291
+ await t.verifyFix(p.id, { result: r.dataset.result, resolvedAt: p.resolvedAt, note: a.querySelector("textarea").value }), await m(p.id);
2292
+ } catch (o) {
2293
+ a.querySelector(".feedback-status").textContent = o.message, a.querySelectorAll("button").forEach((f) => {
2294
+ f.disabled = !1;
2295
+ });
2296
+ }
2297
+ };
2298
+ })), (u = n.querySelector(".report-head")) == null || u.after(a);
2299
+ }
2300
+ function y(n, p) {
2301
+ var u;
2302
+ let a = n.querySelector(".fix-rating");
2303
+ if (!["RESOLVED", "CLOSED"].includes(p.status) || !p.ratingAsked) {
2304
+ a == null || a.remove();
2305
+ return;
2306
+ }
2307
+ a && a.dataset.key === he(p.rating) || (a || (a = document.createElement("div"), a.className = "fix-rating", (u = n.querySelector(".fix-verification") || n.querySelector(".report-head")) == null || u.after(a)), k(a, p, p.rating || null));
2308
+ }
2309
+ function k(n, p, a, { justRated: s = !1 } = {}) {
2310
+ var N;
2311
+ n.dataset.key = he(a);
2312
+ const u = a != null && a.current ? a.score : 0, r = a ? a.current ? s ? "Thanks — the team will see that." : `You rated this ${a.score} out of 5.` : `You rated an earlier fix ${a.score} out of 5. How did this one go?` : "How did we do?", o = s && (a == null ? void 0 : a.current) && !a.comment;
2313
+ n.innerHTML = `
2314
+ <p class="fr-q">${v(r)}</p>
2315
+ <div class="stars" role="group" aria-label="Rate how this was handled, 1 to 5">${[1, 2, 3, 4, 5].map((h) => `
2316
+ <button type="button" data-score="${h}" aria-pressed="${h === u}" aria-label="${h} out of 5${h === 1 ? ", poor" : h === 5 ? ", great" : ""}" class="${h <= u ? "on" : ""}">${me}</button>`).join("")}
2317
+ </div>
2318
+ ${a != null && a.current && a.comment ? `<p class="fr-said">“${v(a.comment)}”</p>` : ""}
2319
+ ${o ? `
2320
+ <div class="fr-note">
2321
+ <textarea maxlength="2000" aria-label="Add a note to your rating" placeholder="${a.score <= 2 ? "What went wrong? (optional)" : "Anything to add? (optional)"}"></textarea>
2322
+ <button type="button" data-act="rating-note">Add note</button>
2323
+ </div>` : ""}
2324
+ ${a ? "" : '<p class="fr-hint">1 is poor, 5 is great.</p>'}
2325
+ <p class="feedback-status" role="status"></p>`;
2326
+ const f = [...n.querySelectorAll("[data-score]")], $ = (h) => f.forEach((z) => z.classList.toggle("on", Number(z.dataset.score) <= h)), A = (h) => n.querySelectorAll("button, textarea").forEach((z) => {
2327
+ z.disabled = h;
2328
+ }), j = n.querySelector(".feedback-status");
2329
+ async function B(h, z) {
2330
+ var R;
2331
+ A(!0), j.textContent = "";
2332
+ try {
2333
+ const H = await t.rate(p.id, { score: h, comment: z });
2334
+ k(n, p, H, { justRated: !0 }), (R = n.querySelector(".fr-note textarea")) == null || R.focus();
2335
+ } catch (H) {
2336
+ j.textContent = H.message, A(!1), $(u);
2337
+ }
2338
+ }
2339
+ for (const h of f) {
2340
+ const z = Number(h.dataset.score);
2341
+ h.addEventListener("mouseenter", () => $(z)), h.addEventListener("focus", () => $(z)), h.addEventListener("blur", () => $(u)), h.addEventListener("click", () => B(z));
2342
+ }
2343
+ n.querySelector(".stars").addEventListener("mouseleave", () => $(u)), (N = n.querySelector('[data-act="rating-note"]')) == null || N.addEventListener("click", () => {
2344
+ var z;
2345
+ const h = n.querySelector(".fr-note textarea").value.trim();
2346
+ if (!h) {
2347
+ (z = n.querySelector(".fr-note")) == null || z.remove();
2348
+ return;
2349
+ }
2350
+ B(a.score, h);
2351
+ });
2352
+ }
2353
+ async function S(n) {
2354
+ const p = i.querySelector(".history.chatting"), a = p == null ? void 0 : p.querySelector(".thread");
2355
+ if (!a || b !== n) return;
2356
+ let s = null;
2357
+ try {
2358
+ s = await t.myReport(n);
2359
+ } catch {
2360
+ }
2361
+ if (e.closed || !s || b !== n) return;
2362
+ L(p, s), y(p, s);
2363
+ const u = pe(a), r = ue(s.timeline);
2364
+ se.get(a) !== r && (a.innerHTML = r, se.set(a, r)), d(p, s, n), u && _(a);
2365
+ const o = p.querySelector(".report-head .pill");
2366
+ o && (o.className = `pill ${String(s.status).toLowerCase()}`, o.textContent = ie(s.status));
2367
+ }
2368
+ const q = t.on("update", (n) => {
2369
+ !b || (n == null ? void 0 : n.bugId) !== b || (t.markReportRead(b), S(b));
2370
+ });
2371
+ e.onDestroy(() => {
2372
+ q(), clearTimeout(l);
2373
+ }), (E = i.querySelector('[data-act="history"]')) == null || E.addEventListener("click", g), e.succeed = T, e.refreshThread = S, e.openReportId = () => b;
2374
+ }
2375
+ function xt({
2376
+ root: e,
2377
+ host: t,
2378
+ dock: i,
2379
+ client: w,
2380
+ onClose: b,
2381
+ preloadCall: T = () => {
2382
+ },
2383
+ themeControls: x = null
2384
+ }) {
2385
+ mt(e);
2386
+ const g = w.config, m = () => (x == null ? void 0 : x.label()) || "", l = document.createElement("div");
2387
+ l.className = `scrim ${i.classList.contains("at-left") ? "at-left" : "at-right"} ${i.classList.contains("at-top") ? "at-top" : "at-bottom"}`, l.style.setProperty("--bc-dock-h", `${Math.round(i.getBoundingClientRect().height)}px`), l.innerHTML = `
2388
+ <div class="panel" role="dialog" aria-modal="true" aria-labelledby="bc-h">
2389
+ ${le({
2390
+ id: "bc-h",
2391
+ // Not "Tell us what went wrong": that is the first field's own label,
2392
+ // a few pixels below. The header names the errand the launcher named.
2393
+ title: "Report a bug",
2394
+ sub: "We attach the page details for you.",
2395
+ theme: m()
2396
+ })}
2397
+ <div class="body">
2398
+ <div class="slot" role="alert"></div>
2399
+ <!-- What the project's team is telling visitors. Drawn only when it
2400
+ has published something. -->
2401
+ <div class="notices" hidden></div>
2402
+ <div class="whoami" hidden></div>
2403
+ <!-- The questions the assistant can answer from their account and
2404
+ close by itself. Drawn only when the project offers any. -->
2405
+ <div class="picks" hidden></div>
2406
+ <div class="known" hidden></div>
2407
+ ${tt(g)}
2408
+ </div>
2409
+ <div class="foot">
2410
+ <span class="note" aria-live="polite"></span>
2411
+ ${w.hasReports() ? '<button class="quiet" type="button" data-act="history">Your reports</button>' : ""}
2412
+ <span class="spacer"></span>
2413
+ <!-- Hidden until the API confirms this project can take a call, which
2414
+ most cannot. Drawn here rather than as a second launcher because it
2415
+ is the same errand as Send report — the difference is only whether
2416
+ the reporter would rather say it than type it. -->
2417
+ <button class="ghost" type="button" data-act="call" hidden>${ae}Talk to the team</button>
2418
+ <button class="primary" type="button" data-act="send">Send report ${ge}</button>
2419
+ </div>
2420
+ <div class="powered">Powered by
2421
+ <a class="brand-credit" href="https://bugcatch.sphoro.com" target="_blank" rel="noopener noreferrer" aria-label="BugCatch">${Pe}</a>
2422
+ </div>
2423
+ </div>`;
2424
+ const d = (u) => l.querySelector(u), L = d(".slot"), y = [], k = {
2425
+ client: w,
2426
+ cfg: g,
2427
+ root: e,
2428
+ host: t,
2429
+ dock: i,
2430
+ scrim: l,
2431
+ $: d,
2432
+ titleInput: d("#bc-title"),
2433
+ descInput: d("#bc-desc"),
2434
+ compose: d(".compose"),
2435
+ sendBtn: d('[data-act="send"]'),
2436
+ callBtn: d('[data-act="call"]'),
2437
+ historyBtn: d('[data-act="history"]'),
2438
+ note: d(".foot .note"),
2439
+ whoami: d(".whoami"),
2440
+ refsBox: d(".refs"),
2441
+ notices: d(".notices"),
2442
+ known: d(".known"),
2443
+ picks: d(".picks"),
2444
+ // A report or a call is on its way. Escape and a click outside wait for it.
2445
+ sending: !1,
2446
+ closed: !1,
2447
+ say: s,
2448
+ close: p,
2449
+ preloadCall: T,
2450
+ themeLabel: m,
2451
+ onDestroy: (u) => {
2452
+ y.push(u);
2453
+ },
2454
+ // compose.js
2455
+ refreshSendState: null,
2456
+ isRecording: null,
2457
+ stopRecording: null,
2458
+ // refs.js
2459
+ holdForReference: null,
2460
+ answeredReference: null,
2461
+ // picks.js
2462
+ pickedIssue: null,
2463
+ // call.js
2464
+ offerCall: null,
2465
+ callOffered: null,
2466
+ showLiveCall: null,
2467
+ // signin.js
2468
+ mustSignIn: null,
2469
+ recheckSignIn: null,
2470
+ // thread.js
2471
+ succeed: null,
2472
+ refreshThread: null,
2473
+ openReportId: null
2474
+ };
2475
+ at(k), nt(k), rt(k), it(k), st(k), lt(k), ct(k), gt(k);
2476
+ const S = document.activeElement, q = document.documentElement.style, E = q.overflow;
2477
+ q.overflow = "hidden";
2478
+ function n() {
2479
+ k.closed || (y.forEach((u) => u()), k.closed = !0, q.overflow = E, document.removeEventListener("keydown", a, !0), l.remove(), i.hidden = !1, t.style.opacity = "", b == null || b());
2480
+ }
2481
+ function p() {
2482
+ var u;
2483
+ if (!k.closed) {
2484
+ l.classList.add("closing"), setTimeout(n, 160);
2485
+ try {
2486
+ (u = S == null ? void 0 : S.focus) == null || u.call(S);
2487
+ } catch {
2488
+ }
2489
+ }
2490
+ }
2491
+ function a(u) {
2492
+ if (u.key === "Escape") {
2493
+ if (u.stopPropagation(), k.isRecording()) {
2494
+ k.stopRecording({ keep: !1 });
2495
+ return;
2496
+ }
2497
+ k.sending || p();
2498
+ return;
2499
+ }
2500
+ if (u.key !== "Tab" || k.isRecording()) return;
2501
+ const r = [...l.querySelectorAll(
2502
+ "button:not(:disabled), input:not([hidden]), textarea, select, [href]"
2503
+ )].filter((A) => A.offsetParent !== null || A === document.activeElement);
2504
+ if (!r.length) return;
2505
+ const o = r[0], f = r[r.length - 1], $ = l.getRootNode().activeElement;
2506
+ u.shiftKey && $ === o ? (u.preventDefault(), f.focus()) : !u.shiftKey && $ === f && (u.preventDefault(), o.focus());
2507
+ }
2508
+ document.addEventListener("keydown", a, !0), l.addEventListener("click", (u) => {
2509
+ u.target === l && !k.sending && p();
2510
+ }), d('[data-act="close"]').addEventListener("click", p), l.addEventListener("click", (u) => {
2511
+ const r = u.target.closest('[data-act="theme"]');
2512
+ if (!r || !x) return;
2513
+ const o = x.cycle();
2514
+ r.title = `Theme: ${o.label}`, r.setAttribute("aria-label", `Theme: ${o.label}. Change theme`);
2515
+ });
2516
+ function s(u, r = "err") {
2517
+ L.innerHTML = u ? `<div class="msg ${r}">${r === "err" ? G : ""}<span>${v(u)}</span></div>` : "";
2518
+ }
2519
+ return e.appendChild(l), k.refreshSendState(), k.titleInput.focus(), { destroy: n, close: p };
2520
+ }
2521
+ function mt(e) {
2522
+ if (e.querySelector("style[data-panel]")) return;
2523
+ const t = document.createElement("style");
2524
+ t.setAttribute("data-panel", ""), t.textContent = Fe;
2525
+ const i = e.querySelector("style");
2526
+ i ? i.after(t) : e.prepend(t);
2527
+ }
2528
+ export {
2529
+ xt as openPanel
2530
+ };
2531
+ //# sourceMappingURL=bugcatch-panel.es.js.map