arkaos 4.13.1 → 4.13.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.
- package/VERSION +1 -1
- package/bin/arka-doctor +15 -1
- package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
- package/core/governance/evidence_checks.py +85 -4
- package/core/workflow/__pycache__/design_authorization.cpython-313.pyc +0 -0
- package/core/workflow/__pycache__/frontend_gate.cpython-313.pyc +0 -0
- package/core/workflow/design_authorization.py +106 -0
- package/core/workflow/frontend_gate.py +54 -22
- package/dashboard/app/app.config.ts +2 -2
- package/dashboard/app/assets/css/main.css +285 -13
- package/dashboard/app/components/ArkaConstellation.vue +239 -0
- package/dashboard/app/components/ArkaCountUp.vue +35 -0
- package/dashboard/app/components/ArkaGlowCard.vue +25 -0
- package/dashboard/app/components/ArkaLiveFeed.vue +122 -0
- package/dashboard/app/components/ArkaNavGroupLabel.vue +11 -0
- package/dashboard/app/components/ArkaPageHero.vue +42 -0
- package/dashboard/app/components/ArkaSection.vue +20 -0
- package/dashboard/app/components/ArkaStarfield.vue +3 -0
- package/dashboard/app/components/ArkaStatCard.vue +38 -0
- package/dashboard/app/components/ArkaSystemPill.vue +54 -0
- package/dashboard/app/components/ArkaTrendChart.vue +68 -0
- package/dashboard/app/composables/useCountUp.ts +30 -0
- package/dashboard/app/composables/useDashboard.ts +2 -1
- package/dashboard/app/composables/useTaskStream.ts +39 -0
- package/dashboard/app/layouts/default.vue +11 -2
- package/dashboard/app/pages/design-lab.vue +257 -0
- package/dashboard/app/pages/index.vue +268 -213
- package/dashboard/nuxt.config.ts +21 -0
- package/dashboard/package.json +1 -0
- package/departments/content/skills/video-produce/SKILL.md +9 -1
- package/departments/content/skills/video-setup/SKILL.md +15 -4
- package/installer/doctor.js +13 -8
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/start-dashboard.sh +3 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.13.
|
|
1
|
+
4.13.2
|
package/bin/arka-doctor
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# ============================================================================
|
|
3
3
|
# ARKA OS — Doctor (Health Check System)
|
|
4
4
|
# Usage: arka doctor [--fix] [--json]
|
|
5
|
-
#
|
|
5
|
+
# 17 modular checks: fail = critical, warn = advisory
|
|
6
6
|
# ============================================================================
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
|
|
@@ -362,6 +362,20 @@ else
|
|
|
362
362
|
fi
|
|
363
363
|
run_check "plugins" "warn" "$PLUGINS_DESC" "$PLUGINS_OK" "Run: bash install.sh"
|
|
364
364
|
|
|
365
|
+
# ─── Check 17: Hyperframes Skills (video production) ──────────────────────
|
|
366
|
+
# Parity with installer/doctor.js `hyperframes-skills` (PR-C2): same name,
|
|
367
|
+
# same sentinel, warn-only — video production is opt-in.
|
|
368
|
+
HF_OK=false
|
|
369
|
+
HF_DESC="Hyperframes skills AUSENTE (video-as-code editing for /content video)"
|
|
370
|
+
for hf_name in hyperframes hyperframes-core; do
|
|
371
|
+
if [ -f "$HOME/.claude/skills/$hf_name/SKILL.md" ]; then
|
|
372
|
+
HF_OK=true
|
|
373
|
+
HF_DESC="Hyperframes skills installed ($hf_name)"
|
|
374
|
+
break
|
|
375
|
+
fi
|
|
376
|
+
done
|
|
377
|
+
run_check "hyperframes-skills" "warn" "$HF_DESC" "$HF_OK" "Run /content video-setup in Claude Code"
|
|
378
|
+
|
|
365
379
|
# ─── Output ────────────────────────────────────────────────────────────────
|
|
366
380
|
TOTAL=$((PASS_COUNT + WARN_COUNT + FAIL_COUNT))
|
|
367
381
|
|
|
Binary file
|
|
@@ -185,20 +185,101 @@ def _mypy_configured(project_dir: Path) -> bool:
|
|
|
185
185
|
|
|
186
186
|
# ─── Individual checks ──────────────────────────────────────────────────
|
|
187
187
|
|
|
188
|
+
_LINTABLE_PY = frozenset({".py"})
|
|
189
|
+
_LINTABLE_JS = frozenset({".js", ".jsx", ".ts", ".tsx", ".vue", ".mjs", ".cjs"})
|
|
190
|
+
_LINTABLE_PHP = frozenset({".php"})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _scoped_files(
|
|
194
|
+
project_dir: Path, changed: list[str] | None, exts: frozenset[str],
|
|
195
|
+
) -> list[str]:
|
|
196
|
+
"""Changed files that live inside project_dir and carry a lintable ext."""
|
|
197
|
+
if not changed:
|
|
198
|
+
return []
|
|
199
|
+
root = project_dir.resolve()
|
|
200
|
+
out: list[str] = []
|
|
201
|
+
for raw in changed:
|
|
202
|
+
p = Path(raw)
|
|
203
|
+
candidate = p if p.is_absolute() else project_dir / p
|
|
204
|
+
try:
|
|
205
|
+
rel = candidate.resolve().relative_to(root)
|
|
206
|
+
except (ValueError, OSError):
|
|
207
|
+
continue
|
|
208
|
+
if candidate.suffix.lower() in exts and candidate.is_file():
|
|
209
|
+
out.append(str(rel))
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _labelled(result: CheckResult, label: str) -> CheckResult:
|
|
214
|
+
"""Prefix the reported command with the lint scope label."""
|
|
215
|
+
result.command = f"{label} {result.command}".strip()
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _lint_scoped(
|
|
220
|
+
project_dir: Path, changed: list[str], timeout: int,
|
|
221
|
+
) -> CheckResult | None:
|
|
222
|
+
"""Lint only the changed files when the detected linter supports it.
|
|
223
|
+
|
|
224
|
+
Pre-existing project-wide debt is master's debt, not this change's —
|
|
225
|
+
the same principle _check_security_grep already applies to its diff
|
|
226
|
+
scope. Returns None when no scoped run applies (caller falls back).
|
|
227
|
+
"""
|
|
228
|
+
if shutil.which("ruff"):
|
|
229
|
+
files = _scoped_files(project_dir, changed, _LINTABLE_PY)
|
|
230
|
+
if files:
|
|
231
|
+
result = _run("lint", ["ruff", "check", *files], project_dir, timeout)
|
|
232
|
+
return _labelled(result, f"lint(scoped: {len(files)} file(s))")
|
|
233
|
+
eslint = project_dir / "node_modules" / ".bin" / "eslint"
|
|
234
|
+
if eslint.is_file():
|
|
235
|
+
files = _scoped_files(project_dir, changed, _LINTABLE_JS)
|
|
236
|
+
if files:
|
|
237
|
+
result = _run("lint", [str(eslint), *files], project_dir, timeout)
|
|
238
|
+
return _labelled(result, f"lint(scoped: {len(files)} file(s))")
|
|
239
|
+
pint = project_dir / "vendor" / "bin" / "pint"
|
|
240
|
+
if pint.is_file():
|
|
241
|
+
files = _scoped_files(project_dir, changed, _LINTABLE_PHP)
|
|
242
|
+
if files:
|
|
243
|
+
result = _run(
|
|
244
|
+
"lint", [str(pint), "--test", *files], project_dir, timeout,
|
|
245
|
+
)
|
|
246
|
+
return _labelled(result, f"lint(scoped: {len(files)} file(s))")
|
|
247
|
+
return None
|
|
248
|
+
|
|
188
249
|
|
|
189
250
|
def _check_lint(
|
|
190
251
|
project_dir: Path, changed: list[str] | None,
|
|
191
252
|
test_command: str | None, timeout: int,
|
|
192
253
|
) -> CheckResult:
|
|
254
|
+
if changed:
|
|
255
|
+
scoped = _lint_scoped(project_dir, changed, timeout)
|
|
256
|
+
if scoped is not None:
|
|
257
|
+
return scoped
|
|
258
|
+
lintable = (
|
|
259
|
+
_scoped_files(project_dir, changed, _LINTABLE_PY)
|
|
260
|
+
or _scoped_files(project_dir, changed, _LINTABLE_JS)
|
|
261
|
+
or _scoped_files(project_dir, changed, _LINTABLE_PHP)
|
|
262
|
+
)
|
|
263
|
+
if not lintable:
|
|
264
|
+
return _skip("lint", "changed files contain no lintable sources")
|
|
193
265
|
if _has_python(project_dir, changed) and shutil.which("ruff"):
|
|
194
|
-
return
|
|
266
|
+
return _labelled(
|
|
267
|
+
_run("lint", ["ruff", "check", "."], project_dir, timeout),
|
|
268
|
+
"lint(project-wide)",
|
|
269
|
+
)
|
|
195
270
|
if _package_json_script(project_dir, "lint"):
|
|
196
|
-
return
|
|
197
|
-
|
|
271
|
+
return _labelled(
|
|
272
|
+
_run(
|
|
273
|
+
"lint", ["npm", "run", "--silent", "lint"], project_dir, timeout,
|
|
274
|
+
),
|
|
275
|
+
"lint(project-wide)",
|
|
198
276
|
)
|
|
199
277
|
pint = project_dir / "vendor" / "bin" / "pint"
|
|
200
278
|
if pint.is_file():
|
|
201
|
-
return
|
|
279
|
+
return _labelled(
|
|
280
|
+
_run("lint", [str(pint), "--test"], project_dir, timeout),
|
|
281
|
+
"lint(project-wide)",
|
|
282
|
+
)
|
|
202
283
|
return _skip("lint", "no lint tooling detected (ruff/eslint/pint)")
|
|
203
284
|
|
|
204
285
|
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Persistent per-session design authorization — frontend-gate resilience.
|
|
2
|
+
|
|
3
|
+
The frontend gate reasons over a 20-message transcript window. Two
|
|
4
|
+
structural blind spots produce false denies in hard mode (the same class
|
|
5
|
+
the flow enforcer solved with ``flow_authorization``, "652 false
|
|
6
|
+
blocks"):
|
|
7
|
+
|
|
8
|
+
- the current turn's ``[arka:design]`` marker is invisible to that
|
|
9
|
+
turn's own PreToolUse hook (a denied tool call never persists its
|
|
10
|
+
turn's text to the transcript), and
|
|
11
|
+
- a tool-heavy sequence serializes as ``<tool_use:...>`` placeholders
|
|
12
|
+
and rolls the marker's text block out of the window.
|
|
13
|
+
|
|
14
|
+
Fix: **persist-on-observe + consult-before-deny**. Whenever ``evaluate``
|
|
15
|
+
observes a structured marker in the window it confirms it here; when the
|
|
16
|
+
window shows nothing, a valid persisted confirmation for the session
|
|
17
|
+
still allows. Deliberately NO turn-grace tier: a session with no design
|
|
18
|
+
evidence at all must keep denying in hard mode (excellence-mandate), and
|
|
19
|
+
the first confirmation has a natural path — emit the marker in a turn
|
|
20
|
+
whose tools are not gated (loading skills / reading the design system),
|
|
21
|
+
which persists to the transcript and confirms on the next evaluation.
|
|
22
|
+
|
|
23
|
+
State dir: ``/tmp/arkaos-design-auth`` (override via
|
|
24
|
+
``ARKA_DESIGN_AUTH_DIR``).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import time
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from core.shared import safe_session_id as _safe_session_id_module
|
|
35
|
+
from core.shared.temp_paths import arkaos_temp_dir
|
|
36
|
+
|
|
37
|
+
_safe_session_id = _safe_session_id_module.safe_session_id
|
|
38
|
+
|
|
39
|
+
# A working session; a stale /tmp record expires on its own.
|
|
40
|
+
DEFAULT_TTL_SECONDS = 12 * 60 * 60
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _base_dir() -> Path:
|
|
44
|
+
override = os.environ.get("ARKA_DESIGN_AUTH_DIR", "").strip()
|
|
45
|
+
return Path(override) if override else arkaos_temp_dir("arkaos-design-auth")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _auth_path(session_id: str) -> Path | None:
|
|
49
|
+
safe = _safe_session_id(session_id)
|
|
50
|
+
return (_base_dir() / f"{safe}.json") if safe else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read(path: Path | None) -> dict:
|
|
54
|
+
if path is None or not path.exists():
|
|
55
|
+
return {}
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
58
|
+
except (json.JSONDecodeError, OSError):
|
|
59
|
+
return {}
|
|
60
|
+
return data if isinstance(data, dict) else {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def confirm(session_id: str, marker: str) -> None:
|
|
64
|
+
"""Persist a confirmed design authorization (structured marker seen)."""
|
|
65
|
+
path = _auth_path(session_id)
|
|
66
|
+
if path is None:
|
|
67
|
+
return
|
|
68
|
+
try:
|
|
69
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
path.write_text(
|
|
71
|
+
json.dumps({"marker": marker, "confirmed_ts": time.time()}),
|
|
72
|
+
encoding="utf-8",
|
|
73
|
+
)
|
|
74
|
+
except OSError:
|
|
75
|
+
pass # authorization state must never block the hook
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def confirmed_marker(
|
|
79
|
+
session_id: str, ttl_seconds: int = DEFAULT_TTL_SECONDS
|
|
80
|
+
) -> str | None:
|
|
81
|
+
"""Return the persisted marker if present and within TTL, else None."""
|
|
82
|
+
data = _read(_auth_path(session_id))
|
|
83
|
+
ts = data.get("confirmed_ts")
|
|
84
|
+
if ts is None:
|
|
85
|
+
return None
|
|
86
|
+
if time.time() - float(ts) > ttl_seconds:
|
|
87
|
+
return None
|
|
88
|
+
marker = data.get("marker")
|
|
89
|
+
return marker if isinstance(marker, str) and marker else None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_confirmed(
|
|
93
|
+
session_id: str, ttl_seconds: int = DEFAULT_TTL_SECONDS
|
|
94
|
+
) -> bool:
|
|
95
|
+
return confirmed_marker(session_id, ttl_seconds) is not None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def clear(session_id: str) -> None:
|
|
99
|
+
"""Remove authorization state for a session (tests / session end)."""
|
|
100
|
+
path = _auth_path(session_id)
|
|
101
|
+
if path is None:
|
|
102
|
+
return
|
|
103
|
+
try:
|
|
104
|
+
path.unlink()
|
|
105
|
+
except (FileNotFoundError, OSError):
|
|
106
|
+
pass
|
|
@@ -40,6 +40,7 @@ from datetime import datetime, timezone
|
|
|
40
40
|
from pathlib import Path
|
|
41
41
|
|
|
42
42
|
from core.shared import safe_session_id as _safe_session_id_module
|
|
43
|
+
from core.workflow import design_authorization
|
|
43
44
|
from core.workflow.flow_enforcer import (
|
|
44
45
|
TRIVIAL_RE,
|
|
45
46
|
_load_last_assistant_messages,
|
|
@@ -186,6 +187,56 @@ def _classify_marker(messages: list[str]) -> tuple[str | None, str]:
|
|
|
186
187
|
return None, "none"
|
|
187
188
|
|
|
188
189
|
|
|
190
|
+
def _resolve_ui_scope(
|
|
191
|
+
tool_name: str, file_path: str, tool_input: dict
|
|
192
|
+
) -> str | None:
|
|
193
|
+
"""Return 'suffix' | 'heuristic' when the call is UI-scoped, else None."""
|
|
194
|
+
if tool_name not in _GATED_TOOLS:
|
|
195
|
+
return None
|
|
196
|
+
if is_ui_file(file_path):
|
|
197
|
+
return "suffix"
|
|
198
|
+
if is_heuristic_ui_file(file_path, tool_name, tool_input):
|
|
199
|
+
return "heuristic"
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _marker_decision(
|
|
204
|
+
session_id: str, marker: str | None, kind: str,
|
|
205
|
+
mode: str, file_path: str, ui_scope: str,
|
|
206
|
+
) -> Decision:
|
|
207
|
+
"""Decide allow/deny from the classified marker + persisted auth.
|
|
208
|
+
|
|
209
|
+
A structured marker is persisted (persist-on-observe) so later
|
|
210
|
+
evaluations survive transcript window-rolling — the deadlock fix,
|
|
211
|
+
mirroring flow_authorization. Trivial markers do NOT persist (they
|
|
212
|
+
authorize one small edit, not a session). With nothing in the
|
|
213
|
+
window, a valid persisted confirmation still allows.
|
|
214
|
+
"""
|
|
215
|
+
if kind == "structured" and marker:
|
|
216
|
+
design_authorization.confirm(session_id, marker)
|
|
217
|
+
if kind in ("structured", "trivial"):
|
|
218
|
+
return Decision(allow=True, reason="design-evidence", mode=mode,
|
|
219
|
+
target_file=file_path, marker_found=marker,
|
|
220
|
+
marker_kind=kind, ui_scope=ui_scope)
|
|
221
|
+
persisted = design_authorization.confirmed_marker(session_id)
|
|
222
|
+
if persisted is not None:
|
|
223
|
+
return Decision(allow=True, reason="design-evidence-persisted",
|
|
224
|
+
mode=mode, target_file=file_path,
|
|
225
|
+
marker_found=persisted, marker_kind="persisted",
|
|
226
|
+
ui_scope=ui_scope)
|
|
227
|
+
# Heuristic scope is WARN-only by design: it never denies, even in
|
|
228
|
+
# hard mode — its telemetry (ui_scope=heuristic) informs whether it
|
|
229
|
+
# ever graduates to denying scope.
|
|
230
|
+
deniable = mode == "hard" and ui_scope == "suffix"
|
|
231
|
+
if kind == "legacy":
|
|
232
|
+
return Decision(allow=not deniable, reason="legacy-marker", mode=mode,
|
|
233
|
+
target_file=file_path, marker_found=marker,
|
|
234
|
+
marker_kind=kind, ui_scope=ui_scope)
|
|
235
|
+
return Decision(allow=not deniable, reason="no-design-marker",
|
|
236
|
+
mode=mode, target_file=file_path, marker_kind=kind,
|
|
237
|
+
ui_scope=ui_scope)
|
|
238
|
+
|
|
239
|
+
|
|
189
240
|
def evaluate(
|
|
190
241
|
tool_name: str,
|
|
191
242
|
transcript_path: str,
|
|
@@ -196,13 +247,8 @@ def evaluate(
|
|
|
196
247
|
) -> Decision:
|
|
197
248
|
"""Evaluate one tool call against the frontend excellence gate."""
|
|
198
249
|
file_path = str(tool_input.get("file_path", ""))
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if is_ui_file(file_path):
|
|
202
|
-
ui_scope = "suffix"
|
|
203
|
-
elif is_heuristic_ui_file(file_path, tool_name, tool_input):
|
|
204
|
-
ui_scope = "heuristic"
|
|
205
|
-
else:
|
|
250
|
+
ui_scope = _resolve_ui_scope(tool_name, file_path, tool_input)
|
|
251
|
+
if ui_scope is None:
|
|
206
252
|
return Decision(allow=True, reason="not-ui-scope", target_file=file_path)
|
|
207
253
|
mode = _mode()
|
|
208
254
|
if mode == "off":
|
|
@@ -216,21 +262,7 @@ def evaluate(
|
|
|
216
262
|
transcript_path, ASSISTANT_WINDOW
|
|
217
263
|
)
|
|
218
264
|
marker, kind = _classify_marker(messages)
|
|
219
|
-
|
|
220
|
-
return Decision(allow=True, reason="design-evidence", mode=mode,
|
|
221
|
-
target_file=file_path, marker_found=marker,
|
|
222
|
-
marker_kind=kind, ui_scope=ui_scope)
|
|
223
|
-
# Heuristic scope is WARN-only by design: it never denies, even in
|
|
224
|
-
# hard mode — its telemetry (ui_scope=heuristic) informs whether it
|
|
225
|
-
# ever graduates to denying scope.
|
|
226
|
-
deniable = mode == "hard" and ui_scope == "suffix"
|
|
227
|
-
if kind == "legacy":
|
|
228
|
-
return Decision(allow=not deniable, reason="legacy-marker", mode=mode,
|
|
229
|
-
target_file=file_path, marker_found=marker,
|
|
230
|
-
marker_kind=kind, ui_scope=ui_scope)
|
|
231
|
-
return Decision(allow=not deniable, reason="no-design-marker",
|
|
232
|
-
mode=mode, target_file=file_path, marker_kind=kind,
|
|
233
|
-
ui_scope=ui_scope)
|
|
265
|
+
return _marker_decision(session_id, marker, kind, mode, file_path, ui_scope)
|
|
234
266
|
|
|
235
267
|
|
|
236
268
|
def record_telemetry(session_id: str, tool: str, decision: Decision) -> None:
|
|
@@ -1,18 +1,290 @@
|
|
|
1
1
|
@import "tailwindcss" theme(static);
|
|
2
2
|
@import "@nuxt/ui";
|
|
3
3
|
|
|
4
|
+
/* ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
ArkaOS Pulse — design tokens
|
|
6
|
+
Brand anchors: #00FF88 (signal green) · #0A0A0A (ink) · #FAFAFA (paper)
|
|
7
|
+
Doctrine: dark-first; green means "alive" (live data, primary action) and
|
|
8
|
+
is never decorative; surfaces are carbon with a green undertone, not flat
|
|
9
|
+
black. Full spec: dashboard/DESIGN-SYSTEM.md
|
|
10
|
+
───────────────────────────────────────────────────────────────────────── */
|
|
11
|
+
|
|
4
12
|
@theme static {
|
|
5
|
-
--font-sans: '
|
|
6
|
-
|
|
7
|
-
--
|
|
8
|
-
--
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
--color-
|
|
12
|
-
--color-
|
|
13
|
-
--color-
|
|
14
|
-
--color-
|
|
15
|
-
--color-
|
|
16
|
-
--color-
|
|
17
|
-
--color-
|
|
13
|
+
--font-sans: 'Inter', sans-serif;
|
|
14
|
+
--font-display: 'Space Grotesk', sans-serif;
|
|
15
|
+
--font-serif: 'Instrument Serif', serif;
|
|
16
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
17
|
+
|
|
18
|
+
/* arka — brand signal green, anchored at 400 = #00FF88 */
|
|
19
|
+
--color-arka-50: #E8FFF4;
|
|
20
|
+
--color-arka-100: #C6FFE3;
|
|
21
|
+
--color-arka-200: #8FFFC9;
|
|
22
|
+
--color-arka-300: #4DFFAB;
|
|
23
|
+
--color-arka-400: #00FF88;
|
|
24
|
+
--color-arka-500: #00E67A;
|
|
25
|
+
--color-arka-600: #00BF66;
|
|
26
|
+
--color-arka-700: #009951;
|
|
27
|
+
--color-arka-800: #00733D;
|
|
28
|
+
--color-arka-900: #0A5C34;
|
|
29
|
+
--color-arka-950: #04331C;
|
|
30
|
+
|
|
31
|
+
/* carbon — neutral scale with a green undertone (never flat gray) */
|
|
32
|
+
--color-carbon-50: #F6F8F7;
|
|
33
|
+
--color-carbon-100: #E9EDEB;
|
|
34
|
+
--color-carbon-200: #D4DBD7;
|
|
35
|
+
--color-carbon-300: #B3BDB8;
|
|
36
|
+
--color-carbon-400: #8A958F;
|
|
37
|
+
--color-carbon-500: #6B756F;
|
|
38
|
+
--color-carbon-600: #525B56;
|
|
39
|
+
--color-carbon-700: #3E4642;
|
|
40
|
+
--color-carbon-800: #262C29;
|
|
41
|
+
--color-carbon-900: #161B18;
|
|
42
|
+
--color-carbon-950: #0A0D0B;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── Motion tokens ──────────────────────────────────────────────────────
|
|
46
|
+
One rhythm for the whole product. Enter uses --arka-ease-out (expo-like
|
|
47
|
+
deceleration), exit is faster (~65%) with --arka-ease-in. The pulse is
|
|
48
|
+
the signature: only elements that are genuinely live may breathe. */
|
|
49
|
+
:root {
|
|
50
|
+
--arka-motion-fast: 150ms;
|
|
51
|
+
--arka-motion-base: 240ms;
|
|
52
|
+
--arka-motion-slow: 400ms;
|
|
53
|
+
--arka-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
54
|
+
--arka-ease-in: cubic-bezier(0.7, 0, 0.84, 0);
|
|
55
|
+
--arka-pulse-period: 2.4s;
|
|
56
|
+
--arka-stagger-step: 40ms;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── Semantic surfaces ──────────────────────────────────────────────────
|
|
60
|
+
Light: paper + ink. Primary darkened to arka-800: 5.72:1 on paper —
|
|
61
|
+
arka-700 reads 3.55:1 and fails AA for text-primary usage. */
|
|
62
|
+
:root {
|
|
63
|
+
--ui-primary: var(--ui-color-primary-800);
|
|
64
|
+
--ui-bg: #FAFAFA;
|
|
65
|
+
--ui-bg-muted: #F1F4F2;
|
|
66
|
+
--ui-bg-elevated: #FFFFFF;
|
|
67
|
+
--ui-bg-accented: var(--color-carbon-100);
|
|
68
|
+
--ui-border: var(--color-carbon-200);
|
|
69
|
+
--ui-border-muted: var(--color-carbon-100);
|
|
70
|
+
--ui-text: #0A0A0A;
|
|
71
|
+
--ui-text-muted: var(--color-carbon-500);
|
|
72
|
+
--arka-glow: 0 0 0 transparent; /* glow is a dark-mode-only voice */
|
|
73
|
+
--arka-grid-line: color-mix(in srgb, var(--color-carbon-200) 60%, transparent);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Dark (flagship): void → muted → elevated → accented, all carbon-tinted.
|
|
77
|
+
Primary lifts to raw brand green; glow becomes available. */
|
|
78
|
+
.dark {
|
|
79
|
+
--ui-primary: var(--ui-color-primary-400);
|
|
80
|
+
--ui-bg: #070908;
|
|
81
|
+
--ui-bg-muted: #0C100E;
|
|
82
|
+
--ui-bg-elevated: var(--color-carbon-950);
|
|
83
|
+
--ui-bg-accented: var(--color-carbon-900);
|
|
84
|
+
--ui-border: #1E2521;
|
|
85
|
+
--ui-border-muted: #151A17;
|
|
86
|
+
--ui-text: var(--color-carbon-100);
|
|
87
|
+
--ui-text-muted: var(--color-carbon-400);
|
|
88
|
+
--arka-glow: 0 0 24px color-mix(in srgb, var(--color-arka-400) 25%, transparent);
|
|
89
|
+
--arka-grid-line: color-mix(in srgb, var(--color-arka-400) 7%, transparent);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ── HUD vernacular ─────────────────────────────────────────────────────
|
|
93
|
+
Eyebrow labels: uppercase mono with wide tracking and a signal tick.
|
|
94
|
+
This — not ornament — is what carries the Jarvis DNA. */
|
|
95
|
+
.arka-eyebrow {
|
|
96
|
+
font-family: var(--font-mono);
|
|
97
|
+
font-size: 0.6875rem;
|
|
98
|
+
font-weight: 500;
|
|
99
|
+
letter-spacing: 0.08em;
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
color: var(--ui-text-muted);
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 0.5rem;
|
|
105
|
+
}
|
|
106
|
+
.arka-eyebrow::before {
|
|
107
|
+
content: "";
|
|
108
|
+
width: 0.375rem;
|
|
109
|
+
height: 0.375rem;
|
|
110
|
+
background: var(--ui-primary);
|
|
111
|
+
clip-path: polygon(0 0, 100% 0, 100% 100%);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Live indicator: a dot that breathes. Reserved for genuinely live data. */
|
|
115
|
+
.arka-live-dot {
|
|
116
|
+
width: 0.5rem;
|
|
117
|
+
height: 0.5rem;
|
|
118
|
+
border-radius: 9999px;
|
|
119
|
+
background: var(--ui-primary);
|
|
120
|
+
box-shadow: var(--arka-glow);
|
|
121
|
+
animation: arka-pulse var(--arka-pulse-period) var(--arka-ease-out) infinite;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Numbers are data: mono, tabular, never shifting layout as they tick. */
|
|
125
|
+
.arka-data {
|
|
126
|
+
font-family: var(--font-mono);
|
|
127
|
+
font-variant-numeric: tabular-nums;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@keyframes arka-pulse {
|
|
131
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
132
|
+
50% { opacity: 0.55; transform: scale(0.88); }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Stream-in: how live content arrives. Pair with a per-item
|
|
136
|
+
animation-delay of calc(var(--arka-stagger-step) * i). */
|
|
137
|
+
@keyframes arka-stream-in {
|
|
138
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
139
|
+
to { opacity: 1; transform: translateY(0); }
|
|
140
|
+
}
|
|
141
|
+
.arka-stream-in {
|
|
142
|
+
animation: arka-stream-in var(--arka-motion-base) var(--arka-ease-out) both;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Pulse line: the signature EKG. One per view, under live sections. */
|
|
146
|
+
.arka-pulse-line {
|
|
147
|
+
height: 2px;
|
|
148
|
+
background: linear-gradient(
|
|
149
|
+
90deg,
|
|
150
|
+
transparent,
|
|
151
|
+
var(--ui-primary) 20%,
|
|
152
|
+
var(--ui-primary) 80%,
|
|
153
|
+
transparent
|
|
154
|
+
);
|
|
155
|
+
background-size: 200% 100%;
|
|
156
|
+
animation: arka-pulse-sweep 3.2s linear infinite;
|
|
157
|
+
opacity: 0.7;
|
|
158
|
+
}
|
|
159
|
+
@keyframes arka-pulse-sweep {
|
|
160
|
+
from { background-position: 100% 0; }
|
|
161
|
+
to { background-position: -100% 0; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* ── Editorial layer (Pulse v2) ─────────────────────────────────────────
|
|
165
|
+
The serif voice: cinematic page titles, Roman-numeral section markers,
|
|
166
|
+
italic brand accents. Serif is display-only — body copy never uses it. */
|
|
167
|
+
.arka-serif-title {
|
|
168
|
+
font-family: var(--font-serif);
|
|
169
|
+
font-weight: 400;
|
|
170
|
+
line-height: 1.05;
|
|
171
|
+
letter-spacing: -0.01em;
|
|
172
|
+
color: var(--ui-text);
|
|
173
|
+
}
|
|
174
|
+
.arka-serif-accent {
|
|
175
|
+
font-family: var(--font-serif);
|
|
176
|
+
font-style: italic;
|
|
177
|
+
font-weight: 400;
|
|
178
|
+
}
|
|
179
|
+
.arka-numeral {
|
|
180
|
+
font-family: var(--font-serif);
|
|
181
|
+
font-style: italic;
|
|
182
|
+
font-variant-numeric: tabular-nums;
|
|
183
|
+
color: color-mix(in srgb, var(--ui-primary) 55%, var(--ui-text-muted));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Gradient-border card. Live cards raise the primary tint; glow stays a
|
|
187
|
+
dark-mode-only voice. Padding-box mask keeps the 1px gradient ring. */
|
|
188
|
+
.arka-glow-card {
|
|
189
|
+
position: relative;
|
|
190
|
+
border-radius: calc(var(--ui-radius, 0.25rem) * 3);
|
|
191
|
+
background: var(--ui-bg-elevated);
|
|
192
|
+
}
|
|
193
|
+
.arka-glow-card::before {
|
|
194
|
+
content: "";
|
|
195
|
+
position: absolute;
|
|
196
|
+
inset: 0;
|
|
197
|
+
border-radius: inherit;
|
|
198
|
+
padding: 1px;
|
|
199
|
+
background: linear-gradient(
|
|
200
|
+
135deg,
|
|
201
|
+
color-mix(in srgb, var(--ui-primary) 22%, transparent),
|
|
202
|
+
color-mix(in srgb, var(--color-carbon-700) 45%, transparent) 55%,
|
|
203
|
+
color-mix(in srgb, var(--color-carbon-700) 25%, transparent)
|
|
204
|
+
);
|
|
205
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
206
|
+
-webkit-mask-composite: xor;
|
|
207
|
+
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
208
|
+
mask-composite: exclude;
|
|
209
|
+
pointer-events: none;
|
|
210
|
+
}
|
|
211
|
+
.arka-glow-card[data-live="true"]::before {
|
|
212
|
+
background: linear-gradient(
|
|
213
|
+
135deg,
|
|
214
|
+
color-mix(in srgb, var(--ui-primary) 45%, transparent),
|
|
215
|
+
color-mix(in srgb, var(--ui-primary) 10%, transparent)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
.dark .arka-glow-card[data-live="true"] {
|
|
219
|
+
box-shadow: var(--arka-glow);
|
|
220
|
+
}
|
|
221
|
+
.arka-glow-card[data-interactive="true"] {
|
|
222
|
+
transition:
|
|
223
|
+
transform var(--arka-motion-fast) var(--arka-ease-out),
|
|
224
|
+
box-shadow var(--arka-motion-fast) var(--arka-ease-out);
|
|
225
|
+
}
|
|
226
|
+
.arka-glow-card[data-interactive="true"]:hover {
|
|
227
|
+
transform: translateY(-2px);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Ambient starfield: layered radial dots in slow drift. Effective star
|
|
231
|
+
opacity stays under ~0.06 so it never competes with text contrast. */
|
|
232
|
+
.arka-starfield {
|
|
233
|
+
position: fixed;
|
|
234
|
+
inset: -10%;
|
|
235
|
+
z-index: 0;
|
|
236
|
+
pointer-events: none;
|
|
237
|
+
opacity: 0.5;
|
|
238
|
+
background-image:
|
|
239
|
+
radial-gradient(1px 1px at 12% 24%, color-mix(in srgb, var(--ui-primary) 12%, transparent), transparent 60%),
|
|
240
|
+
radial-gradient(1px 1px at 34% 68%, color-mix(in srgb, var(--ui-text-muted) 10%, transparent), transparent 60%),
|
|
241
|
+
radial-gradient(1.5px 1.5px at 58% 15%, color-mix(in srgb, var(--ui-primary) 9%, transparent), transparent 60%),
|
|
242
|
+
radial-gradient(1px 1px at 71% 47%, color-mix(in srgb, var(--ui-text-muted) 8%, transparent), transparent 60%),
|
|
243
|
+
radial-gradient(1px 1px at 86% 79%, color-mix(in srgb, var(--ui-primary) 10%, transparent), transparent 60%),
|
|
244
|
+
radial-gradient(1.5px 1.5px at 23% 89%, color-mix(in srgb, var(--ui-text-muted) 7%, transparent), transparent 60%),
|
|
245
|
+
radial-gradient(1px 1px at 47% 38%, color-mix(in srgb, var(--ui-text-muted) 9%, transparent), transparent 60%),
|
|
246
|
+
radial-gradient(1px 1px at 92% 8%, color-mix(in srgb, var(--ui-primary) 8%, transparent), transparent 60%);
|
|
247
|
+
background-size: 55% 55%;
|
|
248
|
+
animation: arka-starfield-drift 140s linear infinite alternate;
|
|
249
|
+
}
|
|
250
|
+
@keyframes arka-starfield-drift {
|
|
251
|
+
to { transform: translate3d(-2.5%, -1.5%, 0); }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Route transitions: one rhythm, upward continuity (enter rises, exit
|
|
255
|
+
recedes). Wired via app.pageTransition in nuxt.config. */
|
|
256
|
+
.arka-page-enter-active {
|
|
257
|
+
transition:
|
|
258
|
+
opacity var(--arka-motion-base) var(--arka-ease-out),
|
|
259
|
+
transform var(--arka-motion-base) var(--arka-ease-out);
|
|
260
|
+
}
|
|
261
|
+
.arka-page-leave-active {
|
|
262
|
+
transition:
|
|
263
|
+
opacity var(--arka-motion-fast) var(--arka-ease-in),
|
|
264
|
+
transform var(--arka-motion-fast) var(--arka-ease-in);
|
|
265
|
+
}
|
|
266
|
+
.arka-page-enter-from {
|
|
267
|
+
opacity: 0;
|
|
268
|
+
transform: translateY(8px);
|
|
269
|
+
}
|
|
270
|
+
.arka-page-leave-to {
|
|
271
|
+
opacity: 0;
|
|
272
|
+
transform: translateY(-4px);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@media (prefers-reduced-motion: reduce) {
|
|
276
|
+
.arka-live-dot,
|
|
277
|
+
.arka-pulse-line,
|
|
278
|
+
.arka-stream-in,
|
|
279
|
+
.arka-starfield {
|
|
280
|
+
animation: none;
|
|
281
|
+
}
|
|
282
|
+
.arka-page-enter-active,
|
|
283
|
+
.arka-page-leave-active {
|
|
284
|
+
transition: none;
|
|
285
|
+
}
|
|
286
|
+
.arka-page-enter-from,
|
|
287
|
+
.arka-page-leave-to {
|
|
288
|
+
transform: none;
|
|
289
|
+
}
|
|
18
290
|
}
|