tacilytics 0.0.0-stage → 2.0.2

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 (307) hide show
  1. package/.env.example +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +253 -2
  4. package/cli.js +173 -0
  5. package/day-salt.js +55 -0
  6. package/package.json +56 -4
  7. package/public/app.js +788 -0
  8. package/public/assets/DISCLAIMER-simple-icons.md +51 -0
  9. package/public/assets/LICENSE-lucide.txt +43 -0
  10. package/public/assets/LICENSE-simple-icons.md +30 -0
  11. package/public/assets/cosmic-bg.webp +0 -0
  12. package/public/assets/flags/LICENSE-flag-icons.txt +21 -0
  13. package/public/assets/flags/ad.svg +150 -0
  14. package/public/assets/flags/ae.svg +6 -0
  15. package/public/assets/flags/af.svg +81 -0
  16. package/public/assets/flags/ag.svg +14 -0
  17. package/public/assets/flags/ai.svg +29 -0
  18. package/public/assets/flags/al.svg +5 -0
  19. package/public/assets/flags/am.svg +5 -0
  20. package/public/assets/flags/ao.svg +13 -0
  21. package/public/assets/flags/aq.svg +5 -0
  22. package/public/assets/flags/ar.svg +32 -0
  23. package/public/assets/flags/arab.svg +109 -0
  24. package/public/assets/flags/as.svg +72 -0
  25. package/public/assets/flags/asean.svg +13 -0
  26. package/public/assets/flags/at.svg +4 -0
  27. package/public/assets/flags/au.svg +8 -0
  28. package/public/assets/flags/aw.svg +186 -0
  29. package/public/assets/flags/ax.svg +18 -0
  30. package/public/assets/flags/az.svg +8 -0
  31. package/public/assets/flags/ba.svg +12 -0
  32. package/public/assets/flags/bb.svg +6 -0
  33. package/public/assets/flags/bd.svg +4 -0
  34. package/public/assets/flags/be.svg +7 -0
  35. package/public/assets/flags/bf.svg +7 -0
  36. package/public/assets/flags/bg.svg +5 -0
  37. package/public/assets/flags/bh.svg +4 -0
  38. package/public/assets/flags/bi.svg +15 -0
  39. package/public/assets/flags/bj.svg +14 -0
  40. package/public/assets/flags/bl.svg +5 -0
  41. package/public/assets/flags/bm.svg +97 -0
  42. package/public/assets/flags/bn.svg +36 -0
  43. package/public/assets/flags/bo.svg +673 -0
  44. package/public/assets/flags/bq.svg +5 -0
  45. package/public/assets/flags/br.svg +45 -0
  46. package/public/assets/flags/bs.svg +13 -0
  47. package/public/assets/flags/bt.svg +89 -0
  48. package/public/assets/flags/bv.svg +13 -0
  49. package/public/assets/flags/bw.svg +7 -0
  50. package/public/assets/flags/by.svg +18 -0
  51. package/public/assets/flags/bz.svg +145 -0
  52. package/public/assets/flags/ca.svg +4 -0
  53. package/public/assets/flags/cc.svg +19 -0
  54. package/public/assets/flags/cd.svg +5 -0
  55. package/public/assets/flags/cefta.svg +13 -0
  56. package/public/assets/flags/cf.svg +15 -0
  57. package/public/assets/flags/cg.svg +12 -0
  58. package/public/assets/flags/ch.svg +9 -0
  59. package/public/assets/flags/ci.svg +7 -0
  60. package/public/assets/flags/ck.svg +9 -0
  61. package/public/assets/flags/cl.svg +13 -0
  62. package/public/assets/flags/cm.svg +15 -0
  63. package/public/assets/flags/cn.svg +11 -0
  64. package/public/assets/flags/co.svg +7 -0
  65. package/public/assets/flags/cp.svg +7 -0
  66. package/public/assets/flags/cr.svg +7 -0
  67. package/public/assets/flags/cu.svg +13 -0
  68. package/public/assets/flags/cv.svg +13 -0
  69. package/public/assets/flags/cw.svg +14 -0
  70. package/public/assets/flags/cx.svg +15 -0
  71. package/public/assets/flags/cy.svg +6 -0
  72. package/public/assets/flags/cz.svg +5 -0
  73. package/public/assets/flags/de.svg +5 -0
  74. package/public/assets/flags/dg.svg +130 -0
  75. package/public/assets/flags/dj.svg +13 -0
  76. package/public/assets/flags/dk.svg +5 -0
  77. package/public/assets/flags/dm.svg +152 -0
  78. package/public/assets/flags/do.svg +121 -0
  79. package/public/assets/flags/dz.svg +5 -0
  80. package/public/assets/flags/eac.svg +48 -0
  81. package/public/assets/flags/ec.svg +138 -0
  82. package/public/assets/flags/ee.svg +5 -0
  83. package/public/assets/flags/eg.svg +38 -0
  84. package/public/assets/flags/eh.svg +16 -0
  85. package/public/assets/flags/er.svg +8 -0
  86. package/public/assets/flags/es-ct.svg +4 -0
  87. package/public/assets/flags/es-ga.svg +187 -0
  88. package/public/assets/flags/es-pv.svg +5 -0
  89. package/public/assets/flags/es.svg +544 -0
  90. package/public/assets/flags/et.svg +14 -0
  91. package/public/assets/flags/eu.svg +28 -0
  92. package/public/assets/flags/fi.svg +5 -0
  93. package/public/assets/flags/fj.svg +120 -0
  94. package/public/assets/flags/fk.svg +90 -0
  95. package/public/assets/flags/fm.svg +11 -0
  96. package/public/assets/flags/fo.svg +12 -0
  97. package/public/assets/flags/fr.svg +5 -0
  98. package/public/assets/flags/ga.svg +7 -0
  99. package/public/assets/flags/gb-eng.svg +5 -0
  100. package/public/assets/flags/gb-nir.svg +132 -0
  101. package/public/assets/flags/gb-sct.svg +4 -0
  102. package/public/assets/flags/gb-wls.svg +9 -0
  103. package/public/assets/flags/gb.svg +7 -0
  104. package/public/assets/flags/gd.svg +27 -0
  105. package/public/assets/flags/ge.svg +6 -0
  106. package/public/assets/flags/gf.svg +5 -0
  107. package/public/assets/flags/gg.svg +9 -0
  108. package/public/assets/flags/gh.svg +6 -0
  109. package/public/assets/flags/gi.svg +32 -0
  110. package/public/assets/flags/gl.svg +4 -0
  111. package/public/assets/flags/gm.svg +14 -0
  112. package/public/assets/flags/gn.svg +7 -0
  113. package/public/assets/flags/gp.svg +5 -0
  114. package/public/assets/flags/gq.svg +23 -0
  115. package/public/assets/flags/gr.svg +16 -0
  116. package/public/assets/flags/gs.svg +133 -0
  117. package/public/assets/flags/gt.svg +204 -0
  118. package/public/assets/flags/gu.svg +19 -0
  119. package/public/assets/flags/gw.svg +13 -0
  120. package/public/assets/flags/gy.svg +9 -0
  121. package/public/assets/flags/hk.svg +8 -0
  122. package/public/assets/flags/hm.svg +8 -0
  123. package/public/assets/flags/hn.svg +18 -0
  124. package/public/assets/flags/hr.svg +58 -0
  125. package/public/assets/flags/ht.svg +116 -0
  126. package/public/assets/flags/hu.svg +7 -0
  127. package/public/assets/flags/ic.svg +7 -0
  128. package/public/assets/flags/id.svg +4 -0
  129. package/public/assets/flags/ie.svg +7 -0
  130. package/public/assets/flags/il.svg +14 -0
  131. package/public/assets/flags/im.svg +36 -0
  132. package/public/assets/flags/in.svg +25 -0
  133. package/public/assets/flags/io.svg +130 -0
  134. package/public/assets/flags/iq.svg +10 -0
  135. package/public/assets/flags/ir.svg +219 -0
  136. package/public/assets/flags/is.svg +12 -0
  137. package/public/assets/flags/it.svg +7 -0
  138. package/public/assets/flags/je.svg +62 -0
  139. package/public/assets/flags/jm.svg +8 -0
  140. package/public/assets/flags/jo.svg +16 -0
  141. package/public/assets/flags/jp.svg +11 -0
  142. package/public/assets/flags/ke.svg +23 -0
  143. package/public/assets/flags/kg.svg +4 -0
  144. package/public/assets/flags/kh.svg +61 -0
  145. package/public/assets/flags/ki.svg +36 -0
  146. package/public/assets/flags/km.svg +16 -0
  147. package/public/assets/flags/kn.svg +14 -0
  148. package/public/assets/flags/kp.svg +15 -0
  149. package/public/assets/flags/kr.svg +24 -0
  150. package/public/assets/flags/kw.svg +13 -0
  151. package/public/assets/flags/ky.svg +103 -0
  152. package/public/assets/flags/kz.svg +36 -0
  153. package/public/assets/flags/la.svg +12 -0
  154. package/public/assets/flags/lb.svg +15 -0
  155. package/public/assets/flags/lc.svg +8 -0
  156. package/public/assets/flags/li.svg +43 -0
  157. package/public/assets/flags/lk.svg +22 -0
  158. package/public/assets/flags/lr.svg +14 -0
  159. package/public/assets/flags/ls.svg +8 -0
  160. package/public/assets/flags/lt.svg +7 -0
  161. package/public/assets/flags/lu.svg +5 -0
  162. package/public/assets/flags/lv.svg +6 -0
  163. package/public/assets/flags/ly.svg +13 -0
  164. package/public/assets/flags/ma.svg +4 -0
  165. package/public/assets/flags/mc.svg +6 -0
  166. package/public/assets/flags/md.svg +70 -0
  167. package/public/assets/flags/me.svg +116 -0
  168. package/public/assets/flags/mf.svg +5 -0
  169. package/public/assets/flags/mg.svg +7 -0
  170. package/public/assets/flags/mh.svg +7 -0
  171. package/public/assets/flags/mk.svg +5 -0
  172. package/public/assets/flags/ml.svg +7 -0
  173. package/public/assets/flags/mm.svg +12 -0
  174. package/public/assets/flags/mn.svg +14 -0
  175. package/public/assets/flags/mo.svg +9 -0
  176. package/public/assets/flags/mp.svg +86 -0
  177. package/public/assets/flags/mq.svg +5 -0
  178. package/public/assets/flags/mr.svg +6 -0
  179. package/public/assets/flags/ms.svg +29 -0
  180. package/public/assets/flags/mt.svg +58 -0
  181. package/public/assets/flags/mu.svg +8 -0
  182. package/public/assets/flags/mv.svg +6 -0
  183. package/public/assets/flags/mw.svg +10 -0
  184. package/public/assets/flags/mx.svg +382 -0
  185. package/public/assets/flags/my.svg +26 -0
  186. package/public/assets/flags/mz.svg +21 -0
  187. package/public/assets/flags/na.svg +16 -0
  188. package/public/assets/flags/nc.svg +13 -0
  189. package/public/assets/flags/ne.svg +6 -0
  190. package/public/assets/flags/nf.svg +9 -0
  191. package/public/assets/flags/ng.svg +6 -0
  192. package/public/assets/flags/ni.svg +129 -0
  193. package/public/assets/flags/nl.svg +5 -0
  194. package/public/assets/flags/no.svg +7 -0
  195. package/public/assets/flags/np.svg +13 -0
  196. package/public/assets/flags/nr.svg +12 -0
  197. package/public/assets/flags/nu.svg +10 -0
  198. package/public/assets/flags/nz.svg +36 -0
  199. package/public/assets/flags/om.svg +115 -0
  200. package/public/assets/flags/pa.svg +14 -0
  201. package/public/assets/flags/pc.svg +33 -0
  202. package/public/assets/flags/pe.svg +4 -0
  203. package/public/assets/flags/pf.svg +19 -0
  204. package/public/assets/flags/pg.svg +9 -0
  205. package/public/assets/flags/ph.svg +6 -0
  206. package/public/assets/flags/pk.svg +15 -0
  207. package/public/assets/flags/pl.svg +6 -0
  208. package/public/assets/flags/pm.svg +5 -0
  209. package/public/assets/flags/pn.svg +53 -0
  210. package/public/assets/flags/pr.svg +13 -0
  211. package/public/assets/flags/ps.svg +6 -0
  212. package/public/assets/flags/pt.svg +57 -0
  213. package/public/assets/flags/pw.svg +11 -0
  214. package/public/assets/flags/py.svg +157 -0
  215. package/public/assets/flags/qa.svg +4 -0
  216. package/public/assets/flags/re.svg +5 -0
  217. package/public/assets/flags/ro.svg +7 -0
  218. package/public/assets/flags/rs.svg +292 -0
  219. package/public/assets/flags/ru.svg +5 -0
  220. package/public/assets/flags/rw.svg +13 -0
  221. package/public/assets/flags/sa.svg +25 -0
  222. package/public/assets/flags/sb.svg +13 -0
  223. package/public/assets/flags/sc.svg +7 -0
  224. package/public/assets/flags/sd.svg +13 -0
  225. package/public/assets/flags/se.svg +4 -0
  226. package/public/assets/flags/sg.svg +13 -0
  227. package/public/assets/flags/sh-ac.svg +689 -0
  228. package/public/assets/flags/sh-hl.svg +164 -0
  229. package/public/assets/flags/sh-ta.svg +76 -0
  230. package/public/assets/flags/sh.svg +7 -0
  231. package/public/assets/flags/si.svg +18 -0
  232. package/public/assets/flags/sj.svg +7 -0
  233. package/public/assets/flags/sk.svg +9 -0
  234. package/public/assets/flags/sl.svg +7 -0
  235. package/public/assets/flags/sm.svg +75 -0
  236. package/public/assets/flags/sn.svg +8 -0
  237. package/public/assets/flags/so.svg +11 -0
  238. package/public/assets/flags/sr.svg +6 -0
  239. package/public/assets/flags/ss.svg +8 -0
  240. package/public/assets/flags/st.svg +16 -0
  241. package/public/assets/flags/sv.svg +593 -0
  242. package/public/assets/flags/sx.svg +56 -0
  243. package/public/assets/flags/sy.svg +6 -0
  244. package/public/assets/flags/sz.svg +34 -0
  245. package/public/assets/flags/tc.svg +50 -0
  246. package/public/assets/flags/td.svg +7 -0
  247. package/public/assets/flags/tf.svg +15 -0
  248. package/public/assets/flags/tg.svg +14 -0
  249. package/public/assets/flags/th.svg +7 -0
  250. package/public/assets/flags/tj.svg +22 -0
  251. package/public/assets/flags/tk.svg +5 -0
  252. package/public/assets/flags/tl.svg +13 -0
  253. package/public/assets/flags/tm.svg +204 -0
  254. package/public/assets/flags/tn.svg +4 -0
  255. package/public/assets/flags/to.svg +10 -0
  256. package/public/assets/flags/tr.svg +8 -0
  257. package/public/assets/flags/tt.svg +5 -0
  258. package/public/assets/flags/tv.svg +9 -0
  259. package/public/assets/flags/tw.svg +34 -0
  260. package/public/assets/flags/tz.svg +13 -0
  261. package/public/assets/flags/ua.svg +6 -0
  262. package/public/assets/flags/ug.svg +30 -0
  263. package/public/assets/flags/um.svg +9 -0
  264. package/public/assets/flags/un.svg +16 -0
  265. package/public/assets/flags/us.svg +9 -0
  266. package/public/assets/flags/uy.svg +28 -0
  267. package/public/assets/flags/uz.svg +30 -0
  268. package/public/assets/flags/va.svg +190 -0
  269. package/public/assets/flags/vc.svg +8 -0
  270. package/public/assets/flags/ve.svg +26 -0
  271. package/public/assets/flags/vg.svg +59 -0
  272. package/public/assets/flags/vi.svg +28 -0
  273. package/public/assets/flags/vn.svg +11 -0
  274. package/public/assets/flags/vu.svg +21 -0
  275. package/public/assets/flags/wf.svg +5 -0
  276. package/public/assets/flags/ws.svg +7 -0
  277. package/public/assets/flags/xk.svg +5 -0
  278. package/public/assets/flags/xx.svg +4 -0
  279. package/public/assets/flags/ye.svg +7 -0
  280. package/public/assets/flags/yt.svg +5 -0
  281. package/public/assets/flags/za.svg +17 -0
  282. package/public/assets/flags/zm.svg +27 -0
  283. package/public/assets/flags/zw.svg +21 -0
  284. package/public/assets/fonts/Inter-OFL.txt +93 -0
  285. package/public/assets/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  286. package/public/assets/fonts/inter-latin-wght-normal.woff2 +0 -0
  287. package/public/assets/icons.svg +200 -0
  288. package/public/assets/nyx-logo-256.png +0 -0
  289. package/public/assets/tacilytics-mark.svg +16 -0
  290. package/public/index.html +239 -0
  291. package/public/style.css +415 -0
  292. package/public/tacilytics.js +184 -0
  293. package/sdk/flutter/CHANGELOG.md +6 -0
  294. package/sdk/flutter/LICENSE +21 -0
  295. package/sdk/flutter/README.md +41 -0
  296. package/sdk/flutter/dart_test.yaml +3 -0
  297. package/sdk/flutter/lib/nyx_analytics.dart +13 -0
  298. package/sdk/flutter/lib/src/beacon.dart +87 -0
  299. package/sdk/flutter/lib/src/client.dart +177 -0
  300. package/sdk/flutter/lib/tacilytics.dart +13 -0
  301. package/sdk/flutter/pubspec.lock +221 -0
  302. package/sdk/flutter/pubspec.yaml +18 -0
  303. package/sdk/flutter/test/alias_test.dart +13 -0
  304. package/sdk/flutter/test/beacon_test.dart +46 -0
  305. package/sdk/flutter/test/e2e_test.dart +18 -0
  306. package/server.js +896 -0
  307. package/tz-country.js +32 -0
@@ -0,0 +1,415 @@
1
+ /* Inter (SIL OFL 1.1, see assets/fonts/Inter-OFL.txt) — self-hosted, no third-party requests */
2
+ @font-face { font-family: 'Inter'; font-style: normal; font-display: swap; font-weight: 100 900; src: url('/assets/fonts/inter-latin-wght-normal.woff2') format('woff2-variations'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
3
+ @font-face { font-family: 'Inter'; font-style: normal; font-display: swap; font-weight: 100 900; src: url('/assets/fonts/inter-latin-ext-wght-normal.woff2') format('woff2-variations'); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
4
+
5
+ /* Tacilytics — calm, professional dark theme (14.10.2026). One accent, no glow, numbers first. */
6
+ :root {
7
+ --accent: #5b8def;
8
+ --accent-2: #34d399;
9
+ --accent-soft: rgba(91, 141, 239, 0.14);
10
+ --accent-line: rgba(91, 141, 239, 0.45);
11
+ --green: #34d399;
12
+ --red: #f87171;
13
+ --yellow: #fbbf24;
14
+ --bg: #0f1115;
15
+ --bg2: #14161c;
16
+ --surface: #171a21;
17
+ --surface-2: #1d2129;
18
+ --line: #262b35;
19
+ --line-strong: #323946;
20
+ --text: #e6e8ec;
21
+ --text-dim: #8b93a3;
22
+ --text-faint: #5f6675;
23
+ /* legacy names, mapped so older rules keep rendering in the new palette */
24
+ --pink: var(--accent); --purple: var(--accent); --violet: var(--accent); --magenta: var(--accent); --cyan: #8fb3f5;
25
+ --glass: var(--surface); --glass-border: var(--line);
26
+ --glow-pink: transparent; --glow-purple: transparent;
27
+ --grad: var(--accent);
28
+ --grad-text: none;
29
+ --radius: 12px;
30
+ --radius-sm: 8px;
31
+ --radius-lg: 14px;
32
+ /* chart colours read by app.js */
33
+ --chart-visitors: #5b8def;
34
+ --chart-pageviews: #8b93a3;
35
+ }
36
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
37
+ html { scroll-behavior: smooth; }
38
+ body {
39
+ font-family: 'Inter', ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
40
+ font-feature-settings: 'cv11', 'ss01', 'tnum';
41
+ background: var(--bg); color: var(--text); line-height: 1.5; overflow-x: hidden; min-height: 100vh;
42
+ -webkit-font-smoothing: antialiased;
43
+ }
44
+ a { color: var(--accent); text-decoration: none; }
45
+ a:hover { text-decoration: underline; }
46
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
47
+
48
+ .container { max-width: 1140px; margin: 0 auto; padding: 22px; }
49
+ .grad-text { color: var(--text); }
50
+
51
+ /* ── Header ── */
52
+ header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 6px 0 18px; border-bottom: 1px solid var(--line); margin-bottom: 22px; flex-wrap: wrap; }
53
+ .brand { display: flex; align-items: center; gap: 13px; }
54
+ .brand .logo { width: 34px; height: 34px; border-radius: 9px; }
55
+
56
+ .brand h1 { font-size: 1.15rem; font-weight: 650; letter-spacing: -.01em; line-height: 1.1; }
57
+ .brand .subtitle { font-size: 0.74rem; color: var(--text-dim); margin-top: 2px; }
58
+ .header-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
59
+
60
+ .btn { border: 1px solid var(--accent); cursor: pointer; font-weight: 600; border-radius: var(--radius-sm); padding: 8px 14px; font-size: 0.84rem; font-family: inherit; transition: background .15s, border-color .15s; color: #fff; background: var(--accent); }
61
+ .btn:hover { background: #4a7de0; border-color: #4a7de0; }
62
+ .btn.ghost { background: var(--surface); border: 1px solid var(--line); color: var(--text); }
63
+ .btn.ghost:hover { background: var(--surface-2); border-color: var(--line-strong); }
64
+ .btn.danger:hover { border-color: var(--red); color: var(--red); background: rgba(248,113,113,.06); }
65
+
66
+ select, input { background: var(--surface); border: 1px solid var(--line); color: var(--text); padding: 8px 12px; border-radius: var(--radius-sm); font-size: 0.84rem; font-family: inherit; outline: none; transition: border .15s; }
67
+ select:focus, input:focus { border-color: var(--accent-line); }
68
+ .realtime { display: flex; align-items: center; gap: 7px; font-size: 0.8rem; color: var(--text-dim); padding: 6px 12px; background: var(--surface); border: 1px solid var(--line); border-radius: 999px; }
69
+ .realtime .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
70
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
71
+
72
+ /* ── toolbar ── */
73
+ .toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 22px; flex-wrap: wrap; }
74
+ .periods { display: inline-flex; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-sm); padding: 3px; gap: 2px; }
75
+ .periods button { background: transparent; border: none; color: var(--text-dim); padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 0.8rem; font-weight: 550; font-family: inherit; transition: background .15s, color .15s; }
76
+ .periods button.active { background: var(--surface-2); color: var(--text); box-shadow: inset 0 0 0 1px var(--line-strong); }
77
+ .periods button:hover:not(.active) { color: var(--text); }
78
+
79
+ /* ── toolbar right (range + actions) ── */
80
+ .toolbar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
81
+ .range { display: inline-flex; align-items: center; gap: 6px; }
82
+ .range input[type=date] { padding: 8px 10px; font-size: 0.8rem; color-scheme: dark; }
83
+ .range .arrow { color: var(--text-dim); }
84
+
85
+ /* ── segmentation filter chips ── */
86
+ .filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 18px; }
87
+ .filters .flabel { font-size: 0.74rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.08em; }
88
+ .chip { display: inline-flex; align-items: center; gap: 7px; padding: 5px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 550; background: var(--accent-soft); border: 1px solid var(--accent-line); color: var(--text); }
89
+ .chip .k { color: var(--text-dim); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; }
90
+ .chip button { background: none; border: none; color: var(--text-dim); cursor: pointer; font-size: 1rem; line-height: 1; padding: 0; transition: color .2s; }
91
+ .chip button:hover { color: var(--pink); }
92
+ .chip.clear { background: transparent; border-color: transparent; color: var(--text-dim); cursor: pointer; }
93
+ .chip.clear:hover { color: var(--pink); }
94
+
95
+ /* ── panel header tabs ── */
96
+ .panel h3 .tabs { margin-left: auto; display: inline-flex; gap: 2px; background: var(--bg2); border: 1px solid var(--line); border-radius: 7px; padding: 2px; }
97
+ .panel h3 .tabs button { background: transparent; border: none; color: var(--text-dim); padding: 3px 9px; border-radius: 5px; cursor: pointer; font-size: 0.72rem; font-weight: 550; font-family: inherit; transition: all .15s; }
98
+ .panel h3 .tabs button.active { background: var(--surface-2); color: var(--text); box-shadow: inset 0 0 0 1px var(--line-strong); }
99
+
100
+ /* ── glass cards ── */
101
+ .card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px 20px; position: relative; overflow: hidden; }
102
+ .card:hover { border-color: var(--line-strong); }
103
+ .stats { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; margin-bottom: 18px; }
104
+ .stat .label { font-size: 0.74rem; color: var(--text-dim); font-weight: 550; margin-bottom: 6px; }
105
+ .stat .value { font-size: 1.75rem; font-weight: 650; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
106
+ .trend { font-size: 0.74rem; font-weight: 700; margin-top: 6px; display: inline-block; }
107
+ .trend.up { color: var(--green); }
108
+ .trend.down { color: var(--red); }
109
+
110
+ /* ── chart ── */
111
+ .chart-card { padding: 22px; margin-bottom: 18px; }
112
+ .chart-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
113
+ .chart-head strong { font-size: 0.92rem; font-weight: 600; }
114
+ .chart-head .legend { display: flex; gap: 14px; font-size: 0.78rem; color: var(--text-dim); }
115
+ .chart-head .legend span { display: inline-flex; align-items: center; gap: 6px; }
116
+ .chart-head .legend .leg { display: inline-flex; align-items: center; gap: 6px; background: none; border: 0; padding: 3px 8px; border-radius: 8px; font: inherit; color: var(--text-dim); cursor: pointer; transition: background .15s, opacity .15s; }
117
+ .chart-head .legend .leg:hover { background: rgba(255,255,255,.06); }
118
+ .chart-head .legend .leg[aria-pressed="false"] { opacity: .38; text-decoration: line-through; }
119
+ .chart-head .legend .leg[aria-pressed="false"] .swatch { background: var(--text-dim) !important; }
120
+ .swatch { width: 10px; height: 10px; border-radius: 3px; }
121
+ #chart { width: 100%; height: 220px; display: block; }
122
+ .tip { position: fixed; pointer-events: none; background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: 8px; padding: 7px 10px; font-size: 0.78rem; opacity: 0; transition: opacity .12s; z-index: 50; white-space: nowrap; box-shadow: 0 8px 24px rgba(0,0,0,.4); }
123
+
124
+ /* ── panels ── */
125
+ .panels { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
126
+ .panel h3 { font-size: 0.86rem; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; font-weight: 600; color: var(--text); }
127
+ .panel h3 .em { font-size: .95rem; filter: grayscale(1); opacity: .7; }
128
+ .bars { display: flex; flex-direction: column; gap: 3px; }
129
+ .bar { position: relative; display: flex; align-items: center; justify-content: space-between; padding: 7px 10px; border-radius: 6px; font-size: 0.84rem; overflow: hidden; }
130
+ .bar .fill { position: absolute; inset: 0 auto 0 0; background: var(--accent-soft); border-radius: 6px; z-index: 0; transition: width .4s cubic-bezier(.4,0,.2,1); }
131
+ .bar .name { position: relative; z-index: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 76%; }
132
+ .bar .val { position: relative; z-index: 1; color: var(--text-dim); font-variant-numeric: tabular-nums; font-weight: 600; }
133
+ .bar:hover .fill { background: rgba(91,141,239,.22); }
134
+ .bar.clickable { cursor: pointer; }
135
+ .bar.clickable:hover { box-shadow: inset 0 0 0 1px var(--accent-line); }
136
+ .bar .cr { position: relative; z-index: 1; color: var(--accent); font-size: 0.74rem; font-weight: 600; margin-left: 8px; font-variant-numeric: tabular-nums; }
137
+ .empty { color: var(--text-dim); font-size: 0.85rem; text-align: center; padding: 26px 0; }
138
+
139
+ /* ── modals ── */
140
+ .modal-overlay { position: fixed; inset: 0; background: rgba(5,6,9,.66); display: flex; align-items: center; justify-content: center; z-index: 1000; animation: fadeIn .2s ease; padding: 20px; }
141
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
142
+ .modal { background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-lg); padding: 32px 30px; width: 100%; max-width: 420px; text-align: center; position: relative; overflow: hidden; box-shadow: 0 24px 64px rgba(0,0,0,.5); animation: slideUp .25s cubic-bezier(.16,1,.3,1); }
143
+
144
+ @keyframes slideUp { from { transform: translateY(20px) scale(.97); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }
145
+ .modal .logo { width: 48px; height: 48px; margin: 0 auto 14px; display: block; border-radius: 12px; }
146
+ .modal .logo.em { font-size: 2rem; width: auto; height: auto; filter: grayscale(1); opacity: .75; }
147
+ .modal h2 { font-size: 1.2rem; font-weight: 650; margin-bottom: 6px; }
148
+ .modal p { color: var(--text-dim); font-size: 0.88rem; margin-bottom: 22px; }
149
+ .modal input { width: 100%; margin-bottom: 14px; text-align: center; }
150
+ .modal .btn { width: 100%; padding: 13px; }
151
+ .err { color: var(--red); font-size: 0.82rem; min-height: 18px; margin-bottom: 8px; }
152
+ .snippet { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; font-family: ui-monospace, 'JetBrains Mono', Menlo, monospace; font-size: 0.76rem; color: #c9d4e8; text-align: left; word-break: break-all; margin: 12px 0; line-height: 1.55; }
153
+ /* ── live visitors modal ── */
154
+ .modal.live { max-width: 600px; text-align: left; }
155
+ .live-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
156
+ .live-head h2 { margin: 0; }
157
+ .live-sub { color: var(--text-dim); font-size: 0.82rem; margin: 6px 0 16px; }
158
+ #live-spark { width: 100%; height: 70px; display: block; margin-bottom: 18px; }
159
+ .feed-label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-dim); margin-bottom: 8px; }
160
+ .feed { max-height: 340px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
161
+ .feed-row { display: flex; align-items: center; gap: 12px; padding: 9px 11px; border-radius: 10px; transition: background .2s; }
162
+ .feed-row:hover { background: var(--surface-2); }
163
+ .feed-row .fl { font-size: 1.1rem; flex-shrink: 0; }
164
+ .feed-row .fw { flex: 1; min-width: 0; display: flex; flex-direction: column; }
165
+ .feed-row .path { font-weight: 600; font-size: 0.88rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
166
+ .feed-row .goal { font-weight: 600; font-size: 0.86rem; color: var(--accent); }
167
+ .feed-row .fm { font-size: 0.74rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
168
+ .feed-row .ft { font-size: 0.74rem; color: var(--text-dim); flex-shrink: 0; font-variant-numeric: tabular-nums; }
169
+ .feed::-webkit-scrollbar { width: 6px; }
170
+ .feed::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 3px; }
171
+
172
+ .hide { display: none !important; }
173
+
174
+ @media (max-width: 760px) { .stats { grid-template-columns: repeat(2, 1fr); } .panels { grid-template-columns: 1fr; } }
175
+
176
+ /* ── funnels ── */
177
+ .funnels { display: flex; flex-direction: column; gap: 18px; }
178
+ .funnel-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
179
+ .funnel-del { background: none; border: none; color: var(--muted, #888); cursor: pointer; font-size: 1.1rem; }
180
+ .funnel-del:hover { color: #f87171; }
181
+ .funnel-step { display: grid; grid-template-columns: 200px 1fr; gap: 10px; align-items: center; margin: 4px 0; }
182
+ .funnel-label { font-size: .82rem; color: var(--muted, #aaa); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
183
+ .funnel-bar-wrap { position: relative; display: flex; align-items: center; gap: 8px; }
184
+ .funnel-bar { height: 16px; border-radius: 4px; background: var(--accent); min-width: 3px; transition: width .4s; }
185
+ .funnel-nums { font-size: .78rem; color: var(--muted, #aaa); white-space: nowrap; }
186
+ .funnel-nums .drop { color: #f87171; }
187
+ .step-row { display: grid; grid-template-columns: 90px 1fr; gap: 8px; margin: 6px 0; }
188
+ @media (max-width: 640px) { .funnel-step { grid-template-columns: 1fr; gap: 2px; } }
189
+
190
+ /* ── 13.10.2026: comparison bar, commerce cards, shop insights ── */
191
+ .compare { display: flex; flex-wrap: wrap; gap: 10px 28px; padding: 12px 18px; margin: 0 0 16px; font-size: .86rem; }
192
+ .compare .k { color: var(--text-dim); margin-right: 8px; text-transform: uppercase; letter-spacing: .05em; font-size: .72rem; }
193
+ .compare .up { color: var(--green); font-weight: 600; }
194
+ .compare .down { color: var(--red); font-weight: 600; }
195
+ .commerce { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 14px; margin: 0 0 16px; }
196
+ .hint { font-size: .74rem; font-weight: 400; margin-left: 6px; }
197
+ p.hint { margin: -4px 0 8px; }
198
+ .pay-note { margin: 10px 2px 0; font-size: .78rem; color: var(--text-dim); }
199
+ .muted { color: var(--text-dim); }
200
+ .yoy { display: block; margin-top: 4px; font-size: .72rem; color: var(--text-dim); }
201
+ .yoy.up { color: var(--green); } .yoy.down { color: var(--red); }
202
+
203
+ /* embed dialog with website / app tabs (13.10.2026) */
204
+ .modal.modal-wide { max-width: 620px; max-height: calc(100vh - 40px); overflow-y: auto; }
205
+ .snip-tabs { display: flex; justify-content: center; gap: 6px; margin: 0 0 14px; }
206
+ .snip-step { text-align: left; font-size: .8rem; margin: 6px 0 0 !important; }
207
+ .snip-hint { text-align: left; font-size: .78rem; margin: -2px 0 12px !important; }
208
+ .snip-hint code, .snip-step code { color: var(--cyan); }
209
+ .snip-hint a { color: var(--cyan); }
210
+ .modal-wide .snippet { white-space: pre-wrap; word-break: normal; overflow-wrap: anywhere; }
211
+ .snip-tabs { display: inline-flex !important; background: var(--bg2); border: 1px solid var(--line); border-radius: 8px; padding: 3px; gap: 2px !important; }
212
+ .snip-tabs button { background: transparent; border: none; color: var(--text-dim); padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 0.8rem; font-weight: 550; font-family: inherit; transition: all .15s; }
213
+ .snip-tabs button.active { background: var(--surface-2); color: var(--text); box-shadow: inset 0 0 0 1px var(--line-strong); }
214
+
215
+ /* 14.10.2026 professional theme: layout polish */
216
+ .container { max-width: 1200px; padding: 0 24px 48px; }
217
+ .stats { gap: 12px; }
218
+ .panels { gap: 12px; }
219
+ .chart-card { padding: 18px 20px; }
220
+ .trend { font-size: 0.74rem; font-weight: 600; font-variant-numeric: tabular-nums; }
221
+ .funnel-label, .funnel-nums { color: var(--text-dim); }
222
+ .empty { color: var(--text-faint); }
223
+ .footer-note { margin-top: 28px; font-size: .74rem; color: var(--text-faint); text-align: center; }
224
+ .footer-note a { color: var(--text-dim); }
225
+
226
+ /* ── 14.10.2026 v2: modern polish (Fabian: „hübscher, moderner“) ── */
227
+ :root {
228
+ --accent: #6d8cff;
229
+ --accent-2: #a78bfa;
230
+ --accent-soft: rgba(109, 140, 255, 0.13);
231
+ --accent-line: rgba(109, 140, 255, 0.42);
232
+ --chart-visitors: #6d8cff;
233
+ --chart-pageviews: #a78bfa;
234
+ --bg: #0b0d12;
235
+ --surface: rgba(23, 26, 34, 0.72);
236
+ --surface-2: #1e222c;
237
+ --line: rgba(255, 255, 255, 0.07);
238
+ --line-strong: rgba(255, 255, 255, 0.12);
239
+ --radius: 16px;
240
+ --shadow: 0 1px 0 rgba(255,255,255,.04) inset, 0 10px 30px -12px rgba(0,0,0,.55);
241
+ }
242
+ body {
243
+ background:
244
+ radial-gradient(1100px 520px at 12% -10%, rgba(109,140,255,.10), transparent 60%),
245
+ radial-gradient(900px 480px at 100% 0%, rgba(167,139,250,.07), transparent 55%),
246
+ var(--bg);
247
+ background-attachment: fixed;
248
+ font-size: 14.5px; letter-spacing: -0.006em;
249
+ }
250
+ /* sticky glass header */
251
+ header {
252
+ position: sticky; top: 0; z-index: 40; margin: 0 -24px 22px; padding: 14px 24px;
253
+ background: rgba(11,13,18,.72); backdrop-filter: saturate(160%) blur(14px); -webkit-backdrop-filter: saturate(160%) blur(14px);
254
+ border-bottom: 1px solid var(--line);
255
+ }
256
+ .brand .logo { width: 32px; height: 32px; border-radius: 9px; box-shadow: 0 0 0 1px var(--line-strong), 0 6px 20px -6px rgba(109,140,255,.55); }
257
+ .brand h1 { font-size: 1.08rem; font-weight: 700; letter-spacing: -.02em; }
258
+ /* cards: soft glass, gentle lift */
259
+ .card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); backdrop-filter: blur(10px); transition: border-color .2s, transform .2s, box-shadow .2s; }
260
+ .card:hover { border-color: var(--line-strong); }
261
+ .stats .card:hover, .commerce .card:hover { transform: translateY(-1px); box-shadow: var(--shadow), 0 14px 34px -18px rgba(109,140,255,.35); }
262
+ /* KPI cards: big tabular numbers, trend as pill, mini sparkline */
263
+ .stat { padding: 18px 20px 14px; min-height: 124px; }
264
+ .stat .label { font-size: .76rem; font-weight: 500; color: var(--text-dim); letter-spacing: 0; text-transform: none; }
265
+ .stat .value { font-size: 2rem; font-weight: 700; letter-spacing: -.035em; line-height: 1.1; }
266
+ .trend { display: inline-flex; align-items: center; gap: 3px; padding: 2px 8px; border-radius: 999px; font-size: .72rem; font-weight: 600; margin-top: 8px; }
267
+ .trend.up { color: #4ade80; background: rgba(74,222,128,.10); }
268
+ .trend.down { color: #fb7185; background: rgba(251,113,133,.10); }
269
+ .trend:empty { display: none; }
270
+ .spark { position: absolute; left: 0; right: 0; bottom: 0; width: 100%; height: 38px; pointer-events: none; opacity: .9; }
271
+ .stat > .label, .stat > .value, .stat > .trend, .stat > .yoy { position: relative; z-index: 1; }
272
+ /* first KPI gets a subtle accent edge */
273
+ .stats .stat:first-child { background: linear-gradient(160deg, rgba(109,140,255,.16), rgba(23,26,34,.72) 55%); border-color: rgba(109,140,255,.28); }
274
+ .commerce .stat:first-child { background: linear-gradient(160deg, rgba(74,222,128,.12), rgba(23,26,34,.72) 55%); border-color: rgba(74,222,128,.22); }
275
+ /* segmented controls */
276
+ .periods { background: rgba(255,255,255,.03); border-color: var(--line); border-radius: 12px; padding: 4px; }
277
+ .periods button { border-radius: 9px; padding: 7px 13px; }
278
+ .periods button.active { background: var(--surface-2); box-shadow: 0 1px 0 rgba(255,255,255,.06) inset, 0 2px 8px rgba(0,0,0,.35); }
279
+ .btn { border-radius: 10px; padding: 8px 14px; box-shadow: 0 6px 18px -8px rgba(109,140,255,.7); }
280
+ .btn:hover { background: #5b7cf5; border-color: #5b7cf5; }
281
+ .btn.ghost { box-shadow: none; background: rgba(255,255,255,.03); }
282
+ .btn.ghost:hover { background: rgba(255,255,255,.06); }
283
+ select, input { border-radius: 10px; background: rgba(255,255,255,.03); }
284
+ .realtime { background: rgba(74,222,128,.07); border-color: rgba(74,222,128,.2); color: #b8f5cd; }
285
+ .realtime .dot { box-shadow: 0 0 0 3px rgba(74,222,128,.18); }
286
+ /* chart card */
287
+ .chart-card { padding: 20px 22px 14px; }
288
+ .chart-head strong { font-size: .95rem; font-weight: 650; }
289
+ #chart { height: 240px; }
290
+ .chart-head .legend .leg { border-radius: 999px; padding: 4px 10px; }
291
+ .swatch { width: 8px; height: 8px; border-radius: 50%; }
292
+ /* panels */
293
+ .panel h3 { font-size: .9rem; font-weight: 650; letter-spacing: -.01em; }
294
+ .panel h3 .em { filter: none; opacity: .9; }
295
+ .bar { border-radius: 8px; padding: 8px 11px; }
296
+ .bar .fill { border-radius: 8px; background: linear-gradient(90deg, rgba(109,140,255,.20), rgba(167,139,250,.08)); }
297
+ .bar:hover .fill { background: linear-gradient(90deg, rgba(109,140,255,.30), rgba(167,139,250,.14)); }
298
+ .bar .val { color: var(--text); font-weight: 600; opacity: .85; }
299
+ .panel h3 .tabs { border-radius: 9px; background: rgba(255,255,255,.03); }
300
+ .panel h3 .tabs button { border-radius: 7px; }
301
+ /* modals */
302
+ .modal { background: rgba(23,26,34,.92); backdrop-filter: blur(18px); border-radius: 20px; box-shadow: 0 30px 80px rgba(0,0,0,.6), 0 0 0 1px var(--line-strong); }
303
+ .modal-overlay { backdrop-filter: blur(6px); }
304
+ /* login screen gets a hero glow */
305
+ #login { background: radial-gradient(700px 420px at 50% 30%, rgba(109,140,255,.16), transparent 65%), rgba(6,7,10,.86); }
306
+ #login .modal .logo { width: 56px; height: 56px; border-radius: 15px; box-shadow: 0 10px 30px -8px rgba(109,140,255,.7); }
307
+ /* fade-in on load */
308
+ @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
309
+ #dash > * { animation: rise .35s ease both; }
310
+ #dash > *:nth-child(2) { animation-delay: .04s; } #dash > *:nth-child(3) { animation-delay: .08s; } #dash > *:nth-child(4) { animation-delay: .12s; }
311
+ .chip { border-radius: 999px; }
312
+ @media (max-width: 760px) { header { margin: 0 -16px 18px; padding: 12px 16px; } .container { padding: 0 16px 40px; } .stat .value { font-size: 1.6rem; } }
313
+ .stats .stat { padding-bottom: 40px; min-height: 0; }
314
+ .commerce .stat { min-height: 0; }
315
+ .spark { height: 32px; opacity: .8; }
316
+ /* page paths read better in a tight mono with tabular feel */
317
+ #p-pages .bar .name, #p-404 .bar .name, .feed-row .path { font-family: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace; font-size: .8rem; letter-spacing: -.01em; }
318
+
319
+ /* ── 14.10.2026 v3: own colour (ink teal + coral), icons instead of emoji ── */
320
+ :root {
321
+ --accent: #2dd4bf; /* teal: primary */
322
+ --accent-strong: #14b8a6;
323
+ --accent-2: #ff7a5c; /* coral: highlights, the "one dot" */
324
+ --accent-soft: rgba(45, 212, 191, 0.12);
325
+ --accent-line: rgba(45, 212, 191, 0.40);
326
+ --chart-visitors: #2dd4bf;
327
+ --chart-pageviews: #ff9f85;
328
+ --bg: #071114;
329
+ --surface: rgba(14, 28, 32, 0.72);
330
+ --surface-2: #11252a;
331
+ --line: rgba(180, 255, 240, 0.07);
332
+ --line-strong: rgba(180, 255, 240, 0.13);
333
+ --text: #e7f3f1;
334
+ --text-dim: #8aa5a2;
335
+ --text-faint: #5b7471;
336
+ --cyan: #7ee8d8;
337
+ }
338
+ body {
339
+ background:
340
+ radial-gradient(1100px 560px at 8% -12%, rgba(45,212,191,.13), transparent 60%),
341
+ radial-gradient(800px 460px at 104% -4%, rgba(255,122,92,.08), transparent 55%),
342
+ var(--bg);
343
+ background-attachment: fixed;
344
+ }
345
+ header { background: rgba(7,17,20,.74); }
346
+ .brand .logo { box-shadow: 0 0 0 1px var(--line-strong), 0 8px 22px -8px rgba(45,212,191,.6); }
347
+ .btn { background: var(--accent-strong); border-color: var(--accent-strong); color: #04211d; font-weight: 650; box-shadow: 0 6px 18px -8px rgba(45,212,191,.75); }
348
+ .btn:hover { background: var(--accent); border-color: var(--accent); }
349
+ .btn.ghost { color: var(--text); }
350
+ .stats .stat:first-child { background: linear-gradient(160deg, rgba(45,212,191,.16), rgba(14,28,32,.72) 55%); border-color: rgba(45,212,191,.28); }
351
+ .commerce .stat:first-child { background: linear-gradient(160deg, rgba(255,122,92,.14), rgba(14,28,32,.72) 55%); border-color: rgba(255,122,92,.26); }
352
+ .stats .card:hover, .commerce .card:hover { box-shadow: var(--shadow), 0 14px 34px -18px rgba(45,212,191,.35); }
353
+ .bar .fill { background: linear-gradient(90deg, rgba(45,212,191,.20), rgba(45,212,191,.05)); }
354
+ .bar:hover .fill { background: linear-gradient(90deg, rgba(45,212,191,.30), rgba(255,122,92,.10)); }
355
+ .bar .cr { color: var(--accent-2); }
356
+ .realtime { background: rgba(45,212,191,.08); border-color: rgba(45,212,191,.22); color: #b9f5ea; }
357
+ .realtime .dot { background: var(--accent); box-shadow: 0 0 0 3px rgba(45,212,191,.2); }
358
+ .chip { background: var(--accent-soft); border-color: var(--accent-line); }
359
+ .trend.up { color: #5eead4; background: rgba(45,212,191,.12); }
360
+ .trend.down { color: #ff9f85; background: rgba(255,122,92,.12); }
361
+ .feed-row .goal { color: var(--accent-2); }
362
+ .funnel-bar { background: linear-gradient(90deg, var(--accent), #5eead4); }
363
+ #login { background: radial-gradient(700px 420px at 50% 30%, rgba(45,212,191,.18), transparent 65%), rgba(4,10,12,.88); }
364
+ #login .modal .logo { box-shadow: 0 12px 32px -8px rgba(45,212,191,.7); }
365
+ :focus-visible { outline-color: var(--accent); }
366
+ /* icons */
367
+ .ico { width: 16px; height: 16px; flex: 0 0 auto; vertical-align: -3px; display: inline-block; }
368
+ .ico.ui { color: var(--text-dim); }
369
+ .ico.h { width: 17px; height: 17px; color: var(--accent); }
370
+ .ico.big { width: 30px; height: 30px; color: var(--accent); }
371
+ .ico.brand { width: 15px; height: 15px; }
372
+ .btn .ico { width: 15px; height: 15px; margin-right: 6px; vertical-align: -2px; color: currentColor; }
373
+ .btn.ghost .ico { color: var(--text-dim); }
374
+ .snip-tabs button .ico { margin-right: 6px; width: 14px; height: 14px; vertical-align: -2px; }
375
+ .flag { width: 18px; height: 13.5px; border-radius: 3px; object-fit: cover; box-shadow: 0 0 0 1px rgba(255,255,255,.08); flex: 0 0 auto; }
376
+ .bar .name { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }
377
+ .bar .name .txt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
378
+ .chip .flag, .chip .ico { margin-right: 4px; }
379
+ .chip button .ico { width: 13px; height: 13px; margin: 0; }
380
+ .range .arrow .ico { width: 14px; height: 14px; }
381
+ .feed-row .fl { display: inline-flex; width: 20px; justify-content: center; }
382
+ .pay-note .ico { width: 14px; height: 14px; vertical-align: -2px; }
383
+ .modal .logo.em { filter: none; opacity: 1; display: grid; place-items: center; width: 56px; height: 56px; margin: 0 auto 14px; border-radius: 16px; background: var(--accent-soft); box-shadow: inset 0 0 0 1px var(--accent-line); }
384
+
385
+ #p-pages .bar .name .txt, #p-404 .bar .name .txt, .feed-row .path { font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace; font-size: .78rem; font-weight: 400; color: #cfe3e0; }
386
+ #p-pages .bar .name, #p-404 .bar .name { font-family: inherit; }
387
+
388
+ /* ── header v4: floating card, exactly as wide as the content ── */
389
+ header {
390
+ position: sticky; top: 12px; z-index: 40;
391
+ margin: 14px 0 22px; padding: 12px 14px 12px 16px;
392
+ border: 1px solid var(--line-strong); border-radius: 16px;
393
+ background: linear-gradient(180deg, rgba(20,40,44,.78), rgba(10,22,25,.78));
394
+ backdrop-filter: saturate(160%) blur(16px); -webkit-backdrop-filter: saturate(160%) blur(16px);
395
+ box-shadow: 0 1px 0 rgba(255,255,255,.04) inset, 0 18px 40px -24px rgba(0,0,0,.8), 0 0 0 1px rgba(45,212,191,.04);
396
+ }
397
+ header::after { content: ""; position: absolute; left: 18px; right: 18px; bottom: -1px; height: 1px; background: linear-gradient(90deg, transparent, rgba(45,212,191,.45), rgba(255,122,92,.35), transparent); pointer-events: none; }
398
+ .brand { gap: 12px; }
399
+ .brand .logo { width: 36px; height: 36px; border-radius: 10px; }
400
+ .brand h1 { font-size: 1.12rem; }
401
+ .brand .subtitle { font-size: .76rem; color: var(--text-faint); margin-top: 1px; }
402
+ .header-right { gap: 8px; }
403
+ .header-right .realtime, .header-right select, .header-right .btn { height: 36px; border-radius: 10px; }
404
+ .header-right select { background-color: rgba(255,255,255,.03); border: 1px solid var(--line-strong); padding: 0 34px 0 12px; }
405
+ .header-right .btn.ghost { background: rgba(255,255,255,.03); border-color: var(--line-strong); }
406
+ .header-right .btn.ghost:hover { background: rgba(45,212,191,.08); border-color: var(--accent-line); }
407
+ .header-right .realtime { display: inline-flex; align-items: center; gap: 7px; padding: 0 12px; }
408
+ .realtime .dot { animation: taci-pulse 2.4s ease-in-out infinite; }
409
+ @keyframes taci-pulse { 0%,100% { box-shadow: 0 0 0 3px rgba(45,212,191,.18); } 50% { box-shadow: 0 0 0 6px rgba(45,212,191,.05); } }
410
+ @media (prefers-reduced-motion: reduce) { .realtime .dot { animation: none; } }
411
+ @media (max-width: 760px) { header { margin: 10px 0 18px; top: 8px; padding: 10px 12px; } .brand .subtitle { display: none; } }
412
+
413
+ /* native dropdown lists follow the dark theme (otherwise light text on a white popup) */
414
+ select { color-scheme: dark; }
415
+ select option { background: #0c1a1d; color: var(--text); }
@@ -0,0 +1,184 @@
1
+ /* Tacilytics tracker — cookieless, ~2KB. (tacit: we count, we don't tell.) Embed:
2
+ <script defer data-domain="example.com" src="https://your-server/tacilytics.js"></script>
3
+ (/nyx.js is served as an alias for sites set up before the rename — same file.)
4
+
5
+ Optional data-* flags on the <script> tag:
6
+ data-honor-dnt respect Do-Not-Track / Global Privacy Control (skip all tracking)
7
+ data-track-outbound auto-record clicks on links to other domains as events
8
+ data-track-404 record a "404" event when the page is an error page
9
+ (value = CSS selector that only exists on error pages, e.g. "body.is-act-error")
10
+ data-track-search record "Search" events: value = query-string parameter, e.g. "search"
11
+ (results counted via data-search-results selector, default ".product-box")
12
+ Purchases: tacilytics('Purchase', { value: 49.90, currency: 'EUR', items: 3 }) on the thank-you page
13
+ Custom events: tacilytics('Signup') · tacilytics('Signup', { plan: 'pro' })
14
+ window.nyx / window.nyxConfig still work as aliases. */
15
+ (function () {
16
+ try {
17
+ var s = document.currentScript || document.querySelector('script[data-domain][src*="tacilytics.js"], script[data-domain][src*="nyx.js"]');
18
+ var domain = s && s.getAttribute('data-domain');
19
+ if (!s || !domain) return;
20
+
21
+ // Privacy signals (opt-in): if the visitor asked not to be tracked, honour it and do nothing.
22
+ if (s.hasAttribute('data-honor-dnt')) {
23
+ var dnt = navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
24
+ if (navigator.globalPrivacyControl === true || dnt === '1' || dnt === 'yes') return;
25
+ }
26
+
27
+ var endpoint = new URL(s.src).origin + '/api/collect';
28
+ var tz = '';
29
+ try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch (e) {}
30
+ var lang = (navigator.language || '').slice(0, 12);
31
+ var qs = new URLSearchParams(location.search);
32
+ var first = true;
33
+
34
+ function post(payload) {
35
+ // ignore local/preview hosts
36
+ if (location.hostname === 'localhost' || location.hostname === '127.0.0.1' || location.protocol === 'file:') return;
37
+ try {
38
+ var body = JSON.stringify(payload);
39
+ // text/plain keeps it a CORS-"simple" request, so the cross-origin beacon
40
+ // sends without a preflight (which sendBeacon cannot do).
41
+ if (navigator.sendBeacon && navigator.sendBeacon(endpoint, new Blob([body], { type: 'text/plain' }))) return;
42
+ fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: body, keepalive: true, mode: 'no-cors' });
43
+ } catch (e) {}
44
+ }
45
+
46
+ function send(name, props) {
47
+ var payload = {
48
+ d: domain,
49
+ p: location.pathname,
50
+ r: name ? '' : (first ? document.referrer : ''),
51
+ tz: tz,
52
+ l: lang,
53
+ s: qs.get('utm_source') || '',
54
+ um: qs.get('utm_medium') || '',
55
+ uc: qs.get('utm_campaign') || '',
56
+ };
57
+ if (name) { payload.n = name; if (props && typeof props === 'object') payload.pr = props; }
58
+ else first = false;
59
+ post(payload);
60
+ }
61
+
62
+ // ── engagement time + scroll depth: report on leave ──
63
+ var acc = 0, since = (document.visibilityState === 'visible') ? Date.now() : 0, engPath = location.pathname;
64
+ var maxScroll = 0;
65
+ function measureScroll() {
66
+ var de = document.documentElement, b = document.body || de;
67
+ var h = Math.max(de.scrollHeight, b.scrollHeight), vh = window.innerHeight || de.clientHeight;
68
+ var y = window.pageYOffset || de.scrollTop || 0;
69
+ // short pages that fit the screen count as fully seen
70
+ var pct = h <= vh ? 100 : Math.round(((y + vh) / h) * 100);
71
+ if (pct > maxScroll) maxScroll = Math.min(100, pct);
72
+ }
73
+ var scrollTimer = 0;
74
+ window.addEventListener('scroll', function () { if (!scrollTimer) scrollTimer = setTimeout(function () { scrollTimer = 0; measureScroll(); }, 200); }, { passive: true });
75
+ window.addEventListener('load', measureScroll);
76
+ function engaged() { return Math.round((acc + (since ? Date.now() - since : 0)) / 1000); }
77
+ function flushEngagement() {
78
+ var secs = engaged();
79
+ measureScroll();
80
+ if (secs >= 1 || maxScroll > 0) post({ d: domain, p: engPath, e: 'engagement', dur: secs, sc: maxScroll });
81
+ }
82
+ function resetEngagement() { acc = 0; since = Date.now(); engPath = location.pathname; maxScroll = 0; }
83
+ document.addEventListener('visibilitychange', function () {
84
+ if (document.visibilityState === 'hidden') { if (since) { acc += Date.now() - since; since = 0; } flushEngagement(); }
85
+ else if (!since) { since = Date.now(); }
86
+ });
87
+ window.addEventListener('pagehide', flushEngagement);
88
+
89
+ // ── SPA navigation: count a pageview on pushState / replaceState / back-forward ──
90
+ var lastPath = location.pathname;
91
+ function onNav() {
92
+ if (location.pathname === lastPath) return;
93
+ flushEngagement(); // close out the page we're leaving
94
+ lastPath = location.pathname;
95
+ resetEngagement(); // start timing the new page
96
+ send();
97
+ }
98
+ var push = history.pushState;
99
+ history.pushState = function () { push.apply(this, arguments); onNav(); if (window.__tacilyticsSearchCheck) window.__tacilyticsSearchCheck(); };
100
+ var replace = history.replaceState;
101
+ history.replaceState = function () { replace.apply(this, arguments); onNav(); if (window.__tacilyticsSearchCheck) window.__tacilyticsSearchCheck(); };
102
+ window.addEventListener('popstate', onNav);
103
+
104
+ // ── custom events / goals: tacilytics('Signup') · tacilytics('Signup', { plan: 'pro' }) ──
105
+ window.tacilytics = function (name, props) { if (name && typeof name === 'string') send(name.slice(0, 80), props); };
106
+ window.nyx = window.tacilytics; // alias for installs from before the rename
107
+
108
+ // ── optional automatic outbound-link tracking ──
109
+ if (s.hasAttribute('data-track-outbound')) {
110
+ document.addEventListener('click', function (ev) {
111
+ var a = ev.target && ev.target.closest && ev.target.closest('a[href]');
112
+ if (!a) return;
113
+ try {
114
+ var u = new URL(a.href, location.href);
115
+ if ((u.protocol === 'http:' || u.protocol === 'https:') && u.hostname !== location.hostname) {
116
+ window.tacilytics('Outbound Link', { url: u.hostname + u.pathname });
117
+ }
118
+ } catch (e) {}
119
+ }, true);
120
+ }
121
+
122
+ // ── 404 pages: one event with the broken URL and where the visitor came from ──
123
+ var cfg = window.tacilyticsConfig || window.nyxConfig || {};
124
+ var sel404 = s.getAttribute('data-track-404') || cfg.track404 || '';
125
+ function check404() {
126
+ try {
127
+ if (sel404 && document.querySelector(sel404)) {
128
+ var from = '';
129
+ try { var ru = new URL(document.referrer); from = ru.hostname === location.hostname ? ru.pathname : ru.hostname; } catch (e) {}
130
+ window.tacilytics('404', { url: location.pathname, from: from });
131
+ }
132
+ } catch (e) {}
133
+ }
134
+
135
+ // ── site search: query + number of results ──
136
+ // classic results page: data-track-search="search" (?search=…)
137
+ // overlay search in the URL hash: tacilyticsConfig.searchHash = { param: 'mbai', key: 'query' } (#mbai=query%3D…)
138
+ // tacilyticsConfig.searchResults / searchEmpty: CSS selectors to count hits / detect "no results"
139
+ var searchParam = s.getAttribute('data-track-search') || cfg.searchParam || '';
140
+ var searchHash = cfg.searchHash || null;
141
+ var resSel = s.getAttribute('data-search-results') || cfg.searchResults || '.product-box';
142
+ var emptySel = cfg.searchEmpty || '';
143
+ var lastQ = '', searchTimer = 0;
144
+ function reportSearch(q) {
145
+ q = (q || '').trim().toLowerCase().slice(0, 60);
146
+ if (!q || q === lastQ) return;
147
+ clearTimeout(searchTimer);
148
+ // wait until the results have rendered (and the visitor stopped typing)
149
+ searchTimer = setTimeout(function () {
150
+ if (q === lastQ) return;
151
+ lastQ = q;
152
+ var n = 0;
153
+ try {
154
+ if (emptySel && document.querySelector(emptySel)) n = 0;
155
+ else n = document.querySelectorAll(resSel).length;
156
+ } catch (e) {}
157
+ window.tacilytics('Search', { q: q, results: n });
158
+ }, 2500);
159
+ }
160
+ function searchFromHash() {
161
+ if (!searchHash) return '';
162
+ var h = new URLSearchParams(location.hash.replace(/^#/, '')).get(searchHash.param);
163
+ if (!h) return '';
164
+ return new URLSearchParams(h).get(searchHash.key || 'query') || '';
165
+ }
166
+ function checkSearch() {
167
+ if (searchParam) reportSearch(new URLSearchParams(location.search).get(searchParam) || '');
168
+ if (searchHash) reportSearch(searchFromHash());
169
+ }
170
+ if (searchHash) {
171
+ window.addEventListener('hashchange', function () { reportSearch(searchFromHash()); });
172
+ window.__tacilyticsSearchCheck = window.__nyxSearchCheck = function () { reportSearch(searchFromHash()); }; // overlay updates the hash via history API
173
+ }
174
+ function afterLoad(fn) { if (document.readyState === 'complete' || document.readyState === 'interactive') setTimeout(fn, 0); else document.addEventListener('DOMContentLoaded', fn); }
175
+ afterLoad(function () { check404(); checkSearch(); });
176
+
177
+ // ── initial pageview ──
178
+ if (document.visibilityState === 'prerender') {
179
+ document.addEventListener('visibilitychange', function f() {
180
+ if (document.visibilityState !== 'prerender') { document.removeEventListener('visibilitychange', f); resetEngagement(); send(); }
181
+ });
182
+ } else { send(); }
183
+ } catch (e) {}
184
+ })();
@@ -0,0 +1,6 @@
1
+ ## 0.2.0
2
+ - Renamed to Tacilytics: package `tacilytics`, classes `Tacilytics`, `TacilyticsBeacon`, `TacilyticsRouteObserver`.
3
+ - `package:tacilytics/nyx_analytics.dart` keeps the old names as deprecated aliases.
4
+
5
+ ## 0.1.0
6
+ - First release: screen views, events, search, purchase, route observer, consent switch, per-launch session id.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nyx & Fabian Budde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.