| Custom CSS |
/* ============================================================
HEIMDALL — COMMAND CENTER THEME
Drop in: storage/app/public/custom.css
============================================================ */
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;800;900&family=Rajdhani:wght@300;400;500;600;700&family=Share+Tech+Mono&display=swap');
/* ─── CSS VARIABLES ─────────────────────────────────────────── */
:root {
--teal: #00f5c8;
--teal-dim: #00c49e;
--teal-glow: rgba(0, 245, 200, 0.18);
--amber: #ffb800;
--amber-dim: #cc9200;
--amber-glow: rgba(255, 184, 0, 0.18);
--red-alert: #ff3a5c;
--bg-void: #030508;
--bg-deep: #060c14;
--bg-surface: #0a1520;
--bg-elevated: #0f1e2d;
--border: rgba(0, 245, 200, 0.12);
--border-hot: rgba(0, 245, 200, 0.45);
--text-primary: #e8f4f8;
--text-secondary: #7aaabb;
--text-dim: #3d6070;
--font-display: 'Orbitron', monospace;
--font-body: 'Rajdhani', sans-serif;
--font-mono: 'Share Tech Mono', monospace;
--radius: 4px;
--radius-lg: 8px;
}
/* ─── RESET + BASE ───────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html {
scroll-behavior: smooth;
cursor: none;
}
body {
font-family: var(--font-body);
font-size: 15px;
background: var(--bg-void);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
letter-spacing: 0.02em;
}
/* ─── CUSTOM CURSOR ──────────────────────────────────────────── */
#cursor-dot {
position: fixed;
width: 6px; height: 6px;
background: var(--teal);
border-radius: 50%;
pointer-events: none;
z-index: 99999;
transition: transform 0.1s ease;
box-shadow: 0 0 10px var(--teal), 0 0 20px var(--teal);
}
#cursor-ring {
position: fixed;
width: 28px; height: 28px;
border: 1px solid var(--teal);
border-radius: 50%;
pointer-events: none;
z-index: 99998;
transition: transform 0.18s ease, width 0.2s, height 0.2s, border-color 0.2s;
opacity: 0.6;
}
body:hover #cursor-ring { opacity: 1; }
/* ─── PARTICLE CANVAS ────────────────────────────────────────── */
#heimdall-canvas {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
}
/* ─── HEX GRID OVERLAY ───────────────────────────────────────── */
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 1;
background-image:
repeating-linear-gradient(
60deg,
transparent,
transparent 28px,
rgba(0, 245, 200, 0.022) 28px,
rgba(0, 245, 200, 0.022) 29px
),
repeating-linear-gradient(
-60deg,
transparent,
transparent 28px,
rgba(0, 245, 200, 0.022) 28px,
rgba(0, 245, 200, 0.022) 29px
),
repeating-linear-gradient(
0deg,
transparent,
transparent 48px,
rgba(0, 245, 200, 0.015) 48px,
rgba(0, 245, 200, 0.015) 49px
);
pointer-events: none;
}
/* ─── SCANLINES ──────────────────────────────────────────────── */
body::after {
content: '';
position: fixed;
inset: 0;
z-index: 2;
background: repeating-linear-gradient(
to bottom,
transparent,
transparent 2px,
rgba(0, 0, 0, 0.08) 2px,
rgba(0, 0, 0, 0.08) 4px
);
pointer-events: none;
animation: scanPulse 8s ease-in-out infinite;
}
@keyframes scanPulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.7; }
}
/* ─── MAIN WRAPPER ───────────────────────────────────────────── */
.wrapper,
#wrapper,
.container-fluid,
main {
position: relative;
z-index: 10;
}
/* ─── HEADER / NAV ───────────────────────────────────────────── */
header,
.navbar,
nav.navbar {
background: rgba(3, 5, 8, 0.92) !important;
backdrop-filter: blur(24px) saturate(1.5);
border-bottom: 1px solid var(--border-hot) !important;
box-shadow: 0 1px 0 rgba(0, 245, 200, 0.08), 0 4px 40px rgba(0, 0, 0, 0.6);
position: sticky;
top: 0;
z-index: 1000;
}
/* NAV brand / title */
.navbar-brand,
.navbar-brand span,
header h1,
header .title {
font-family: var(--font-display) !important;
font-weight: 800 !important;
font-size: 1.1rem !important;
letter-spacing: 0.22em !important;
text-transform: uppercase !important;
color: var(--teal) !important;
text-shadow: 0 0 12px var(--teal), 0 0 30px rgba(0, 245, 200, 0.4);
animation: glitch 6s infinite;
}
@keyframes glitch {
0%, 94%, 100% { text-shadow: 0 0 12px var(--teal), 0 0 30px rgba(0,245,200,0.4); transform: none; }
95% { text-shadow: -2px 0 var(--amber), 2px 0 var(--red-alert); transform: skewX(-1deg); }
96% { text-shadow: 2px 0 var(--teal), -2px 0 var(--amber); transform: skewX(1deg); }
97% { text-shadow: 0 0 12px var(--teal); transform: none; }
}
/* NAV links */
.navbar a,
nav a {
font-family: var(--font-mono) !important;
font-size: 0.75rem !important;
letter-spacing: 0.12em !important;
color: var(--text-secondary) !important;
text-transform: uppercase;
transition: color 0.2s, text-shadow 0.2s;
}
.navbar a:hover,
nav a:hover {
color: var(--teal) !important;
text-shadow: 0 0 8px var(--teal);
}
/* ─── SEARCH BAR ─────────────────────────────────────────────── */
.search-wrapper,
#search,
input[type="search"],
input[type="text"].search,
.search-input {
background: rgba(10, 21, 32, 0.8) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius) !important;
color: var(--text-primary) !important;
font-family: var(--font-mono) !important;
font-size: 0.85rem !important;
letter-spacing: 0.08em !important;
padding: 10px 18px 10px 42px !important;
outline: none !important;
box-shadow: inset 0 0 0 1px transparent;
transition: all 0.3s ease;
}
.search-wrapper:focus-within,
input[type="search"]:focus,
input[type="text"].search:focus {
border-color: var(--teal) !important;
box-shadow: 0 0 0 1px var(--teal), 0 0 24px var(--teal-glow), inset 0 0 12px rgba(0,245,200,0.04) !important;
background: rgba(0, 245, 200, 0.03) !important;
}
input::placeholder { color: var(--text-dim) !important; }
/* ═══════════════════════════════════════════════════════════════
HEIMDALL APP CARDS — real selectors from Heimdall's DOM
═══════════════════════════════════════════════════════════════ */
/* ─── Main grid ──────────────────────────────────────────────── */
#sortable {
display: flex !important;
flex-wrap: wrap !important;
align-items: flex-start !important;
gap: 18px !important;
padding: 24px !important;
}
/* ─── Tag group container ────────────────────────────────────── */
.tags-container-parent {
background: rgba(6, 12, 20, 0.7) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius-lg) !important;
backdrop-filter: blur(16px) !important;
box-shadow:
0 4px 24px rgba(0,0,0,0.4),
inset 0 1px 0 rgba(0,245,200,0.04) !important;
overflow: hidden;
position: relative;
}
/* teal top bar on tag group */
.tags-container-parent::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--teal), transparent);
opacity: 0.5;
}
/* ─── Tag group title ────────────────────────────────────────── */
.tags-title {
font-family: var(--font-display) !important;
font-size: 0.7rem !important;
font-weight: 700 !important;
letter-spacing: 0.28em !important;
text-transform: uppercase !important;
color: var(--teal) !important;
text-shadow: 0 0 10px rgba(0,245,200,0.35) !important;
padding: 14px 18px 10px !important;
margin: 0 !important;
border-bottom: 1px solid var(--border) !important;
display: flex !important;
align-items: center !important;
gap: 10px;
}
.tags-title::before {
content: '//';
color: rgba(0,245,200,0.35);
font-size: 0.65rem;
letter-spacing: 0.1em;
}
/* ─── Tag grid inside group ──────────────────────────────────── */
.tags-container {
display: flex !important;
flex-wrap: wrap !important;
gap: 0 !important;
padding: 12px !important;
}
/* ─── Individual app card (.item) ───────────────────────────── */
.item {
position: relative !important;
display: flex !important;
align-items: center !important;
width: 280px !important;
height: 88px !important;
padding: 0 70px 0 0 !important;
margin: 6px !important;
border-radius: var(--radius-lg) !important;
background: var(--bg-surface) !important;
border: 1px solid var(--border) !important;
box-shadow:
0 2px 8px rgba(0,0,0,0.4),
inset 0 1px 0 rgba(255,255,255,0.02) !important;
overflow: hidden !important;
transition:
transform 0.28s cubic-bezier(0.23,1,0.32,1),
box-shadow 0.28s cubic-bezier(0.23,1,0.32,1),
border-color 0.28s ease,
background 0.28s ease !important;
cursor: pointer;
backdrop-filter: blur(8px) !important;
}
/* Top shimmer bar */
.item::before {
content: '' !important;
position: absolute !important;
top: 0; left: 0; right: 0 !important;
height: 1px !important;
background: linear-gradient(90deg, transparent, var(--teal), transparent) !important;
opacity: 0 !important;
transition: opacity 0.3s ease !important;
z-index: 2 !important;
}
/* Right-side color swatch — override with teal gradient panel */
.item:after {
content: '' !important;
position: absolute !important;
top: 0 !important;
right: 0 !important;
width: 64px !important;
height: 100% !important;
background: linear-gradient(
135deg,
rgba(0,245,200,0.08) 0%,
rgba(0,245,200,0.14) 100%
) !important;
border-left: 1px solid rgba(0,245,200,0.1) !important;
opacity: 1 !important;
transition: background 0.28s ease !important;
z-index: 0 !important;
}
.item:hover {
transform: translateY(-4px) scale(1.02) !important;
background: var(--bg-elevated) !important;
border-color: var(--border-hot) !important;
box-shadow:
0 0 0 1px rgba(0,245,200,0.2),
0 12px 40px rgba(0,0,0,0.6),
0 0 28px var(--teal-glow) !important;
}
.item:hover::before { opacity: 1 !important; }
.item:hover:after {
background: linear-gradient(
135deg,
rgba(0,245,200,0.14) 0%,
rgba(0,245,200,0.22) 100%
) !important;
}
/* ─── App icon wrapper ───────────────────────────────────────── */
.app-icon {
position: relative !important;
z-index: 1 !important;
flex-shrink: 0 !important;
width: 52px !important;
height: 52px !important;
margin-left: 14px !important;
margin-right: 0 !important;
filter:
saturate(0.9)
brightness(1.0)
drop-shadow(0 0 6px rgba(0,245,200,0.15)) !important;
transition:
filter 0.3s ease,
transform 0.3s cubic-bezier(0.23,1,0.32,1) !important;
border-radius: var(--radius) !important;
}
.item:hover .app-icon {
filter:
saturate(1.1)
brightness(1.1)
drop-shadow(0 0 12px rgba(0,245,200,0.35)) !important;
transform: scale(1.1) !important;
}
/* Decorative FA icon on right panel */
.item .svg-inline--fa {
position: absolute !important;
right: 0 !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: 52px !important;
height: 52px !important;
color: rgba(0,245,200,0.22) !important;
z-index: 1 !important;
transition: color 0.3s ease !important;
}
.item:hover .svg-inline--fa {
color: rgba(0,245,200,0.38) !important;
}
/* ─── App text details ───────────────────────────────────────── */
.details {
position: relative !important;
z-index: 1 !important;
padding: 0 10px 0 14px !important;
flex: 1 !important;
min-width: 0 !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
gap: 4px !important;
}
.details * { color: var(--text-primary) !important; }
.details > .title {
font-family: var(--font-display) !important;
font-size: 0.72rem !important;
font-weight: 700 !important;
letter-spacing: 0.16em !important;
text-transform: uppercase !important;
color: var(--text-primary) !important;
text-shadow: none !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
transition: color 0.2s ease, text-shadow 0.2s ease !important;
margin: 0 !important;
line-height: 1.2 !important;
}
.item:hover .details > .title {
color: var(--teal) !important;
text-shadow: 0 0 10px rgba(0,245,200,0.45) !important;
}
/* App description / URL subtitle */
.details .description,
.details .url,
.details p {
font-family: var(--font-mono) !important;
font-size: 0.6rem !important;
letter-spacing: 0.06em !important;
color: var(--text-dim) !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
margin: 0 !important;
}
/* ─── Live stats (enhanced apps) ────────────────────────────── */
.livestats-container {
margin-top: 3px !important;
}
.livestats-container .livestats {
display: flex !important;
flex-wrap: wrap !important;
gap: 4px 10px !important;
list-style: none !important;
padding: 0 !important;
margin: 0 !important;
}
.livestats-container .livestats li {
font-family: var(--font-mono) !important;
font-size: 0.58rem !important;
letter-spacing: 0.06em !important;
color: var(--text-dim) !important;
padding: 0 !important;
}
.livestats-container .livestats strong {
color: var(--amber) !important;
font-weight: 500 !important;
}
.livestats-container .livestats .title {
font-family: var(--font-mono) !important;
font-size: 0.55rem !important;
letter-spacing: 0.1em !important;
text-transform: uppercase !important;
color: var(--text-dim) !important;
margin-bottom: 1px !important;
}
/* ─── Config / settings sidebar buttons ─────────────────────── */
#config-buttons {
position: fixed !important;
right: 0 !important;
bottom: 50% !important;
transform: translateY(50%) !important;
display: flex !important;
flex-direction: column !important;
background: rgba(6, 12, 20, 0.92) !important;
border: 1px solid var(--border) !important;
border-right: none !important;
border-radius: 8px 0 0 8px !important;
backdrop-filter: blur(20px) !important;
box-shadow:
-4px 0 24px rgba(0,0,0,0.4),
inset 1px 0 0 rgba(0,245,200,0.04) !important;
z-index: 500 !important;
overflow: hidden !important;
}
#config-buttons a {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 46px !important;
height: 46px !important;
background: transparent !important;
border-bottom: 1px solid var(--border) !important;
transition: background 0.2s ease !important;
}
#config-buttons a:last-child { border-bottom: none !important; }
#config-buttons a:hover {
background: rgba(0,245,200,0.08) !important;
}
#config-buttons a svg {
color: rgba(0,245,200,0.45) !important;
width: 16px !important;
height: 16px !important;
transition: color 0.2s ease, filter 0.2s ease !important;
}
#config-buttons a:hover svg {
color: var(--teal) !important;
filter: drop-shadow(0 0 6px rgba(0,245,200,0.5)) !important;
transform: none !important;
}
/* ─── Search bar ─────────────────────────────────────────────── */
#search-wrapper,
.search-container,
#search {
background: rgba(6,12,20,0.85) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius) !important;
backdrop-filter: blur(16px) !important;
transition: all 0.3s ease !important;
}
#search-wrapper:focus-within,
.search-container:focus-within {
border-color: var(--teal) !important;
box-shadow: 0 0 0 1px var(--teal), 0 0 20px var(--teal-glow) !important;
}
/* ─── Item container tooltip ─────────────────────────────────── */
.item-container .tooltip {
font-family: var(--font-mono) !important;
font-size: 0.65rem !important;
letter-spacing: 0.1em !important;
background: var(--bg-elevated) !important;
border: 1px solid var(--border-hot) !important;
color: var(--text-primary) !important;
border-radius: var(--radius) !important;
box-shadow: 0 4px 20px rgba(0,0,0,0.5), 0 0 12px var(--teal-glow) !important;
}
/* ─── Card stagger entry animation ──────────────────────────── */
.item {
animation: cardEntry 0.45s cubic-bezier(0.23,1,0.32,1) both;
}
.item:nth-child(1) { animation-delay: 0.04s; }
.item:nth-child(2) { animation-delay: 0.08s; }
.item:nth-child(3) { animation-delay: 0.12s; }
.item:nth-child(4) { animation-delay: 0.16s; }
.item:nth-child(5) { animation-delay: 0.20s; }
.item:nth-child(6) { animation-delay: 0.24s; }
.item:nth-child(7) { animation-delay: 0.28s; }
.item:nth-child(8) { animation-delay: 0.32s; }
.item:nth-child(n+9){ animation-delay: 0.36s; }
@keyframes cardEntry {
0% { opacity: 0; transform: translateY(14px) scale(0.97); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
/* ─── .black text override for Heimdall color classes ───────── */
.black { color: var(--text-primary) !important; }
.white { color: var(--text-primary) !important; }
/* ─── TAGS / STATUS PILLS ────────────────────────────────────── */
.tag,
.badge,
.status-badge,
.label {
font-family: var(--font-mono) !important;
font-size: 0.62rem !important;
letter-spacing: 0.14em !important;
text-transform: uppercase !important;
border-radius: 2px !important;
padding: 2px 8px !important;
background: rgba(0, 245, 200, 0.08) !important;
border: 1px solid rgba(0, 245, 200, 0.2) !important;
color: var(--teal) !important;
}
/* ─── BUTTONS ────────────────────────────────────────────────── */
.btn,
button:not([class*="navbar"]) {
font-family: var(--font-display) !important;
font-size: 0.7rem !important;
font-weight: 600 !important;
letter-spacing: 0.2em !important;
text-transform: uppercase !important;
border-radius: var(--radius) !important;
border: 1px solid var(--border) !important;
background: rgba(0,245,200,0.06) !important;
color: var(--teal) !important;
padding: 8px 20px !important;
cursor: none;
transition: all 0.2s ease !important;
position: relative;
overflow: hidden;
}
.btn::before,
button::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(0,245,200,0.1), transparent);
transform: translateX(-100%);
transition: transform 0.4s ease;
}
.btn:hover,
button:hover {
background: rgba(0,245,200,0.12) !important;
border-color: var(--teal) !important;
box-shadow: 0 0 16px var(--teal-glow) !important;
}
.btn:hover::before,
button:hover::before { transform: translateX(100%); }
.btn-primary {
background: linear-gradient(135deg, rgba(0,245,200,0.15), rgba(0,196,158,0.08)) !important;
border-color: var(--teal) !important;
box-shadow: 0 0 10px var(--teal-glow) !important;
}
/* ─── SIDEBAR ────────────────────────────────────────────────── */
.sidebar,
aside,
#sidebar {
background: rgba(3, 5, 8, 0.95) !important;
border-right: 1px solid var(--border) !important;
backdrop-filter: blur(20px);
}
.sidebar a,
aside a {
font-family: var(--font-body) !important;
font-weight: 500;
letter-spacing: 0.06em;
color: var(--text-secondary) !important;
padding: 9px 16px !important;
border-left: 2px solid transparent !important;
transition: all 0.2s ease !important;
display: block;
font-size: 0.88rem;
}
.sidebar a:hover,
aside a:hover,
.sidebar a.active,
aside a.active {
color: var(--teal) !important;
border-left-color: var(--teal) !important;
background: rgba(0,245,200,0.05) !important;
text-shadow: 0 0 8px rgba(0,245,200,0.3);
padding-left: 22px !important;
}
/* ─── PAGE TITLE / SECTION HEADERS ──────────────────────────── */
h1, h2, h3, h4, h5, h6,
.page-title,
.section-title {
font-family: var(--font-display) !important;
color: var(--text-primary) !important;
letter-spacing: 0.12em !important;
text-transform: uppercase !important;
}
h1, .page-title {
font-size: 1.6rem !important;
font-weight: 900 !important;
color: var(--teal) !important;
text-shadow: 0 0 20px rgba(0,245,200,0.3);
}
h2 {
font-size: 1.1rem !important;
font-weight: 600 !important;
color: var(--amber) !important;
text-shadow: 0 0 12px rgba(255,184,0,0.25);
}
h3 { font-size: 0.9rem !important; color: var(--text-secondary) !important; }
/* ─── MODALS / DIALOGS ───────────────────────────────────────── */
.modal-content,
.dialog,
[role="dialog"] {
background: var(--bg-deep) !important;
border: 1px solid var(--border-hot) !important;
border-radius: var(--radius-lg) !important;
box-shadow:
0 0 0 1px rgba(0,245,200,0.1),
0 24px 80px rgba(0,0,0,0.9),
0 0 60px var(--teal-glow) !important;
color: var(--text-primary) !important;
}
.modal-header,
.dialog-header {
border-bottom: 1px solid var(--border) !important;
background: rgba(0,245,200,0.03) !important;
}
.modal-title {
font-family: var(--font-display) !important;
font-size: 0.85rem !important;
letter-spacing: 0.2em !important;
text-transform: uppercase !important;
color: var(--teal) !important;
}
.modal-footer,
.dialog-footer {
border-top: 1px solid var(--border) !important;
background: rgba(0,0,0,0.2) !important;
}
/* ─── FORM ELEMENTS ──────────────────────────────────────────── */
input:not([type="submit"]):not([type="button"]):not([type="checkbox"]):not([type="radio"]),
textarea,
select {
background: rgba(6, 12, 20, 0.8) !important;
border: 1px solid var(--border) !important;
border-radius: var(--radius) !important;
color: var(--text-primary) !important;
font-family: var(--font-body) !important;
font-size: 0.9rem !important;
padding: 9px 14px !important;
transition: all 0.25s ease !important;
outline: none !important;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--teal) !important;
box-shadow: 0 0 0 2px var(--teal-glow), 0 0 14px var(--teal-glow) !important;
}
label {
font-family: var(--font-mono) !important;
font-size: 0.72rem !important;
letter-spacing: 0.14em !important;
text-transform: uppercase !important;
color: var(--text-secondary) !important;
margin-bottom: 4px !important;
display: block;
}
/* ─── TABLES ─────────────────────────────────────────────────── */
table { border-collapse: collapse; width: 100%; }
thead th {
font-family: var(--font-mono) !important;
font-size: 0.68rem !important;
letter-spacing: 0.18em !important;
text-transform: uppercase !important;
color: var(--teal) !important;
border-bottom: 1px solid var(--border-hot) !important;
padding: 10px 14px !important;
background: rgba(0,245,200,0.04) !important;
}
tbody tr {
border-bottom: 1px solid rgba(0,245,200,0.06) !important;
transition: background 0.15s ease !important;
}
tbody tr:hover { background: rgba(0,245,200,0.04) !important; }
tbody td {
padding: 9px 14px !important;
color: var(--text-secondary) !important;
font-family: var(--font-body) !important;
font-size: 0.9rem;
}
/* ─── SCROLLBAR ──────────────────────────────────────────────── */
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: var(--bg-void); }
::-webkit-scrollbar-thumb {
background: rgba(0,245,200,0.3);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover { background: var(--teal); }
/* ─── NOTIFICATIONS / ALERTS ─────────────────────────────────── */
.alert,
.notification,
.toast {
background: var(--bg-elevated) !important;
border-radius: var(--radius) !important;
border-left: 3px solid var(--teal) !important;
font-family: var(--font-body) !important;
color: var(--text-primary) !important;
box-shadow: 0 4px 20px rgba(0,0,0,0.5), 4px 0 0 var(--teal-glow) !important;
}
.alert-warning { border-left-color: var(--amber) !important; }
.alert-danger { border-left-color: var(--red-alert) !important; }
.alert-success { border-left-color: var(--teal) !important; }
/* ─── LINKS ──────────────────────────────────────────────────── */
a {
color: var(--teal-dim) !important;
text-decoration: none !important;
transition: color 0.2s ease, text-shadow 0.2s ease !important;
}
a:hover {
color: var(--teal) !important;
text-shadow: 0 0 8px rgba(0,245,200,0.4);
}
/* ─── SECTION DIVIDERS ───────────────────────────────────────── */
hr {
border: none !important;
height: 1px !important;
background: linear-gradient(90deg, transparent, var(--teal-dim), transparent) !important;
opacity: 0.3 !important;
margin: 28px 0 !important;
}
/* ─── LOADING / SPINNER ──────────────────────────────────────── */
.loading,
.spinner,
[class*="loader"] {
border-color: rgba(0,245,200,0.15) !important;
border-top-color: var(--teal) !important;
filter: drop-shadow(0 0 6px var(--teal));
}
/* ─── STATUS INDICATORS ──────────────────────────────────────── */
.status-online,
.online,
[class*="status-up"] {
color: var(--teal) !important;
text-shadow: 0 0 6px var(--teal);
}
.status-offline,
.offline,
[class*="status-down"] {
color: var(--red-alert) !important;
text-shadow: 0 0 6px var(--red-alert);
}
/* ─── CARD GRID STAGGER ANIMATION ────────────────────────────── */
.app-card,
.card,
.application-card {
animation: cardEntry 0.5s cubic-bezier(0.23, 1, 0.32, 1) both;
}
.app-card:nth-child(1), .card:nth-child(1) { animation-delay: 0.04s; }
.app-card:nth-child(2), .card:nth-child(2) { animation-delay: 0.08s; }
.app-card:nth-child(3), .card:nth-child(3) { animation-delay: 0.12s; }
.app-card:nth-child(4), .card:nth-child(4) { animation-delay: 0.16s; }
.app-card:nth-child(5), .card:nth-child(5) { animation-delay: 0.20s; }
.app-card:nth-child(6), .card:nth-child(6) { animation-delay: 0.24s; }
.app-card:nth-child(7), .card:nth-child(7) { animation-delay: 0.28s; }
.app-card:nth-child(8), .card:nth-child(8) { animation-delay: 0.32s; }
.app-card:nth-child(n+9), .card:nth-child(n+9) { animation-delay: 0.36s; }
@keyframes cardEntry {
0% { opacity: 0; transform: translateY(18px) scale(0.96); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
/* ─── HUD CORNER BRACKETS ────────────────────────────────────── */
.app-card,
.card,
.application-card {
--b: 12px;
}
.app-card .corner-bracket,
.card .corner-bracket {
position: absolute;
width: var(--b); height: var(--b);
border-color: var(--teal);
border-style: solid;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.app-card:hover .corner-bracket,
.card:hover .corner-bracket { opacity: 1; }
/* ─── HUD STATUS BAR — compact single row ────────────────────── */
.heimdall-hud-bar {
display: flex;
align-items: center;
gap: 0;
padding: 0 20px;
height: 36px;
background: rgba(3, 8, 14, 0.95);
border-bottom: 1px solid var(--border-hot);
box-shadow: 0 2px 16px rgba(0,0,0,0.5);
position: relative;
z-index: 999;
overflow: hidden;
}
/* Left: uptime block */
.hud-left {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.hud-uptime-label {
font-family: var(--font-mono);
font-size: 0.58rem;
letter-spacing: 0.18em;
color: var(--text-dim);
text-transform: uppercase;
}
.hud-uptime-value {
font-family: var(--font-display);
font-size: 0.82rem;
font-weight: 700;
color: var(--teal);
letter-spacing: 0.06em;
line-height: 1;
text-shadow: 0 0 10px rgba(0,245,200,0.4);
}
.hud-session-row {
display: flex;
align-items: center;
gap: 8px;
padding-left: 12px;
border-left: 1px solid var(--border);
}
.hud-session-label {
font-family: var(--font-mono);
font-size: 0.58rem;
letter-spacing: 0.12em;
color: var(--text-dim);
text-transform: uppercase;
}
.hud-session-id {
font-family: var(--font-mono);
font-size: 0.65rem;
color: rgba(0,245,200,0.55);
letter-spacing: 0.08em;
}
.hud-ping-dot {
width: 5px; height: 5px;
background: var(--teal);
border-radius: 50%;
animation: pingPulse 1.4s ease-in-out infinite;
box-shadow: 0 0 5px var(--teal);
flex-shrink: 0;
}
@keyframes pingPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.3; transform: scale(0.55); }
}
/* Center: clock — takes remaining space, centered */
.hud-center {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.hud-clock {
font-family: var(--font-display);
font-size: 1.05rem;
font-weight: 900;
letter-spacing: 0.1em;
color: var(--teal);
line-height: 1;
text-shadow: 0 0 12px rgba(0,245,200,0.45);
}
.hud-date {
font-family: var(--font-mono);
font-size: 0.62rem;
letter-spacing: 0.16em;
color: var(--text-secondary);
text-transform: uppercase;
}
.hud-sys-tag {
font-family: var(--font-mono);
font-size: 0.55rem;
letter-spacing: 0.2em;
color: rgba(0,245,200,0.28);
text-transform: uppercase;
}
/* Right: stats + terminal button */
.hud-right {
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.hud-stat-row {
display: flex;
align-items: center;
gap: 6px;
}
.hud-stat-label {
font-family: var(--font-mono);
font-size: 0.58rem;
letter-spacing: 0.12em;
color: var(--text-dim);
text-transform: uppercase;
}
.hud-stat-value {
font-family: var(--font-display);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.06em;
color: var(--teal);
}
.hud-stat-value.amber {
color: var(--amber);
}
.hud-terminal-btn {
font-family: var(--font-display);
font-size: 0.55rem;
letter-spacing: 0.18em;
text-transform: uppercase;
padding: 3px 12px;
border: 1px solid rgba(0,245,200,0.3);
border-radius: 2px;
background: rgba(0,245,200,0.05);
color: var(--teal);
cursor: none;
transition: all 0.2s ease;
white-space: nowrap;
}
.hud-terminal-btn:hover {
background: rgba(0,245,200,0.12);
border-color: var(--teal);
box-shadow: 0 0 8px rgba(0,245,200,0.15);
}
/* ─── FOOTER ─────────────────────────────────────────────────── */
footer {
background: transparent !important;
border-top: 1px solid var(--border) !important;
font-family: var(--font-mono) !important;
font-size: 0.65rem !important;
letter-spacing: 0.14em !important;
color: var(--text-dim) !important;
text-transform: uppercase;
padding: 16px 24px !important;
}
/* ─── MOBILE ADAPTIVE ────────────────────────────────────────── */
@media (max-width: 768px) {
:root { font-size: 13px; }
body::before { display: none; }
.navbar-brand { font-size: 0.85rem !important; }
}
/* ─── SPECIAL: AMBER ACCENT FOR PINNED ITEMS ─────────────────── */
.app-card.pinned,
.card.pinned,
.application-card.pinned {
border-color: rgba(255, 184, 0, 0.22) !important;
}
.app-card.pinned::before,
.card.pinned::before {
background: linear-gradient(90deg, transparent, var(--amber), transparent) !important;
opacity: 1;
}
.app-card.pinned:hover {
box-shadow:
0 0 0 1px rgba(255,184,0,0.3),
0 12px 40px rgba(0,0,0,0.7),
0 0 30px var(--amber-glow) !important;
}
/* ─── SELECTION ──────────────────────────────────────────────── */
::selection {
background: rgba(0,245,200,0.2);
color: var(--teal);
}
/* ═══════════════════════════════════════════════════════════════
WAWTOR TERMINAL WIDGET
═══════════════════════════════════════════════════════════════ */
/* ─── Floating window ───────────────────────────────────────────── */
#wt-window {
position: fixed;
bottom: 28px;
right: 28px;
width: 820px;
height: 480px;
min-width: 420px;
min-height: 240px;
display: flex;
flex-direction: column;
background: #0b0d0f;
border: 1px solid rgba(0,245,200,0.35);
border-radius: 8px;
box-shadow:
0 0 0 1px rgba(0,245,200,0.1),
0 24px 80px rgba(0,0,0,0.9),
0 0 40px rgba(0,245,200,0.08);
z-index: 8000;
overflow: hidden;
resize: both;
font-family: 'Share Tech Mono', monospace;
transition: box-shadow 0.2s ease;
}
#wt-window.minimized { height: 36px !important; resize: none; }
#wt-window.maximized {
inset: 0 !important;
width: 100vw !important;
height: 100vh !important;
border-radius: 0;
resize: none;
}
#wt-window.hidden { display: none !important; }
/* ─── Title bar ─────────────────────────────────────────────────── */
#wt-titlebar {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0;
height: 36px;
background: #141820;
border-bottom: 1px solid rgba(0,245,200,0.15);
user-select: none;
cursor: move;
padding: 0 10px;
}
.wt-traffic {
display: flex;
align-items: center;
gap: 6px;
margin-right: 12px;
}
.wt-dot {
width: 11px; height: 11px;
border-radius: 50%;
cursor: none;
transition: filter 0.15s ease;
}
.wt-dot:hover { filter: brightness(1.4); }
.wt-dot.close { background: #ff5f57; }
.wt-dot.min { background: #ffbd2e; }
.wt-dot.max { background: #28c840; }
.wt-title {
font-family: 'Share Tech Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.14em;
color: rgba(0,245,200,0.6);
text-transform: uppercase;
flex: 1;
}
#wt-conn-status {
font-family: 'Share Tech Mono', monospace;
font-size: 0.62rem;
letter-spacing: 0.1em;
padding: 2px 10px;
border-radius: 2px;
margin-right: 8px;
}
#wt-conn-status.connected { color: #00f5c8; background: rgba(0,245,200,0.08); border: 1px solid rgba(0,245,200,0.25); }
#wt-conn-status.disconnected { color: #ff3a5c; background: rgba(255,58,92,0.08); border: 1px solid rgba(255,58,92,0.25); }
#wt-conn-status.connecting { color: #ffb800; background: rgba(255,184,0,0.08); border: 1px solid rgba(255,184,0,0.25); }
.wt-btn {
font-family: 'Share Tech Mono', monospace;
font-size: 0.62rem;
letter-spacing: 0.14em;
text-transform: uppercase;
padding: 3px 12px;
border-radius: 2px;
border: 1px solid rgba(0,245,200,0.2);
background: rgba(0,245,200,0.04);
color: rgba(0,245,200,0.7);
cursor: none;
margin-left: 6px;
transition: all 0.15s ease;
white-space: nowrap;
}
.wt-btn:hover {
background: rgba(0,245,200,0.12);
border-color: rgba(0,245,200,0.5);
color: #00f5c8;
}
.wt-btn.active {
background: rgba(0,245,200,0.15);
border-color: #00f5c8;
color: #00f5c8;
box-shadow: 0 0 8px rgba(0,245,200,0.2);
}
/* ─── Body (terminal + macro panel) ────────────────────────────── */
#wt-body {
flex: 1;
display: flex;
overflow: hidden;
min-height: 0;
}
/* ─── xterm container ───────────────────────────────────────────── */
#wt-xterm {
flex: 1;
min-width: 0;
padding: 6px 4px;
background: #0b0d0f;
/* CRITICAL: xterm must receive pointer events for focus/selection */
pointer-events: auto !important;
cursor: text !important;
}
#wt-xterm * {
/* Override the global cursor:none from custom cursor module */
pointer-events: auto !important;
}
#wt-xterm .xterm {
height: 100%;
cursor: text !important;
}
#wt-xterm .xterm-screen {
cursor: text !important;
}
#wt-xterm .xterm canvas {
cursor: text !important;
pointer-events: auto !important;
}
#wt-xterm .xterm-viewport {
scrollbar-width: thin;
scrollbar-color: rgba(0,245,200,0.25) transparent;
}
/* ─── Connect screen (shown when no WS) ────────────────────────── */
#wt-connect-screen {
display: none;
flex: 1;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 18px;
padding: 32px;
background: #0b0d0f;
}
#wt-connect-screen.visible { display: flex; }
.wt-connect-title {
font-family: 'Orbitron', monospace;
font-size: 0.8rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #00f5c8;
text-shadow: 0 0 12px rgba(0,245,200,0.4);
}
.wt-connect-desc {
font-family: 'Share Tech Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.08em;
color: #7aaabb;
text-align: center;
max-width: 420px;
line-height: 1.8;
}
.wt-connect-desc code {
background: rgba(0,245,200,0.08);
border: 1px solid rgba(0,245,200,0.2);
border-radius: 3px;
padding: 2px 8px;
color: #00f5c8;
font-size: 0.68rem;
}
.wt-connect-row {
display: flex;
gap: 8px;
width: 100%;
max-width: 440px;
}
.wt-connect-row input {
flex: 1;
background: rgba(10,21,32,0.9) !important;
border: 1px solid rgba(0,245,200,0.2) !important;
border-radius: 3px !important;
color: #e8f4f8 !important;
font-family: 'Share Tech Mono', monospace !important;
font-size: 0.78rem !important;
padding: 8px 14px !important;
}
.wt-connect-row input:focus {
border-color: #00f5c8 !important;
box-shadow: 0 0 0 1px rgba(0,245,200,0.3) !important;
outline: none !important;
}
.wt-connect-btn {
padding: 8px 20px;
font-family: 'Orbitron', monospace;
font-size: 0.62rem;
letter-spacing: 0.18em;
text-transform: uppercase;
border: 1px solid rgba(0,245,200,0.4);
border-radius: 3px;
background: rgba(0,245,200,0.08);
color: #00f5c8;
cursor: none;
transition: all 0.2s ease;
white-space: nowrap;
}
.wt-connect-btn:hover {
background: rgba(0,245,200,0.16);
box-shadow: 0 0 12px rgba(0,245,200,0.2);
}
/* ─── Macro panel ───────────────────────────────────────────────── */
#wt-macro-panel {
width: 0;
overflow: hidden;
background: #0e1318;
border-left: 0px solid rgba(0,245,200,0.15);
display: flex;
flex-direction: column;
transition: width 0.28s cubic-bezier(0.23,1,0.32,1), border-width 0.28s ease;
flex-shrink: 0;
}
#wt-macro-panel.open {
width: 260px;
border-left-width: 1px;
}
.wt-macro-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid rgba(0,245,200,0.12);
flex-shrink: 0;
}
.wt-macro-title {
font-family: 'Orbitron', monospace;
font-size: 0.62rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #00f5c8;
white-space: nowrap;
}
.wt-macro-edit-btn {
font-family: 'Share Tech Mono', monospace;
font-size: 0.6rem;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 2px 9px;
border: 1px solid rgba(0,245,200,0.2);
border-radius: 2px;
background: transparent;
color: rgba(0,245,200,0.5);
cursor: none;
transition: all 0.15s ease;
white-space: nowrap;
}
.wt-macro-edit-btn:hover, .wt-macro-edit-btn.active {
background: rgba(0,245,200,0.1);
border-color: rgba(0,245,200,0.5);
color: #00f5c8;
}
.wt-macro-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
scrollbar-width: thin;
scrollbar-color: rgba(0,245,200,0.2) transparent;
}
/* Macro item — display mode */
.wt-macro-item {
position: relative;
display: flex;
align-items: center;
gap: 0;
margin: 2px 8px;
border-radius: 4px;
border: 1px solid transparent;
overflow: hidden;
transition: all 0.15s ease;
min-height: 46px;
}
.wt-macro-item:hover {
background: rgba(0,245,200,0.04);
border-color: rgba(0,245,200,0.12);
}
.wt-macro-run {
flex-shrink: 0;
width: 34px; height: 100%;
min-height: 46px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,245,200,0.06);
border-right: 1px solid rgba(0,245,200,0.1);
cursor: none;
transition: background 0.15s ease;
font-size: 0.75rem;
color: #00f5c8;
}
.wt-macro-run:hover {
background: rgba(0,245,200,0.18);
color: #fff;
}
.wt-macro-run.running { color: #ffb800; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.wt-macro-info {
flex: 1;
padding: 6px 10px;
min-width: 0;
}
.wt-macro-name {
font-family: 'Orbitron', monospace;
font-size: 0.62rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #e8f4f8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.wt-macro-cmd {
font-family: 'Share Tech Mono', monospace;
font-size: 0.58rem;
color: rgba(0,245,200,0.45);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}
.wt-macro-del {
flex-shrink: 0;
width: 28px; height: 100%;
min-height: 46px;
display: none;
align-items: center;
justify-content: center;
background: transparent;
border-left: 1px solid rgba(255,58,92,0.1);
cursor: none;
color: rgba(255,58,92,0.4);
font-size: 0.72rem;
transition: all 0.15s ease;
}
.wt-macro-del:hover { background: rgba(255,58,92,0.1); color: #ff3a5c; }
.edit-mode .wt-macro-del { display: flex; }
/* Edit inputs inside macro items */
.wt-macro-item.editing {
flex-direction: column;
align-items: stretch;
padding: 8px 10px;
background: rgba(0,245,200,0.04);
border-color: rgba(0,245,200,0.2);
gap: 5px;
}
.wt-macro-input {
width: 100%;
background: rgba(6,12,20,0.8) !important;
border: 1px solid rgba(0,245,200,0.2) !important;
border-radius: 2px !important;
color: #e8f4f8 !important;
font-family: 'Share Tech Mono', monospace !important;
font-size: 0.7rem !important;
padding: 4px 8px !important;
outline: none !important;
}
.wt-macro-input:focus { border-color: rgba(0,245,200,0.5) !important; }
.wt-macro-input.name-input {
font-family: 'Orbitron', monospace !important;
font-size: 0.62rem !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
color: #e8f4f8 !important;
}
.wt-macro-save-row {
display: flex;
gap: 5px;
justify-content: flex-end;
margin-top: 2px;
}
.wt-macro-save, .wt-macro-cancel {
font-family: 'Orbitron', monospace;
font-size: 0.55rem;
letter-spacing: 0.14em;
text-transform: uppercase;
padding: 3px 10px;
border-radius: 2px;
cursor: none;
transition: all 0.15s ease;
}
.wt-macro-save {
border: 1px solid rgba(0,245,200,0.35);
background: rgba(0,245,200,0.08);
color: #00f5c8;
}
.wt-macro-save:hover { background: rgba(0,245,200,0.18); }
.wt-macro-cancel {
border: 1px solid rgba(255,255,255,0.08);
background: transparent;
color: #7aaabb;
}
.wt-macro-cancel:hover { background: rgba(255,255,255,0.05); }
/* Add macro button */
.wt-macro-add-btn {
margin: 8px;
padding: 8px;
font-family: 'Share Tech Mono', monospace;
font-size: 0.65rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(0,245,200,0.5);
border: 1px dashed rgba(0,245,200,0.2);
border-radius: 4px;
background: transparent;
cursor: none;
transition: all 0.2s ease;
text-align: center;
display: none;
}
.wt-macro-add-btn:hover {
color: #00f5c8;
border-color: rgba(0,245,200,0.5);
background: rgba(0,245,200,0.04);
}
.edit-mode .wt-macro-add-btn { display: block; }
/* ─── Settings panel (inside macro panel bottom) ────────────────── */
.wt-macro-settings {
flex-shrink: 0;
border-top: 1px solid rgba(0,245,200,0.1);
padding: 10px 12px;
}
.wt-settings-label {
font-family: 'Share Tech Mono', monospace;
font-size: 0.58rem;
letter-spacing: 0.14em;
color: #3d6070;
text-transform: uppercase;
margin-bottom: 5px;
}
.wt-settings-input {
width: 100%;
background: rgba(6,12,20,0.8) !important;
border: 1px solid rgba(0,245,200,0.15) !important;
border-radius: 2px !important;
color: rgba(0,245,200,0.7) !important;
font-family: 'Share Tech Mono', monospace !important;
font-size: 0.65rem !important;
padding: 5px 8px !important;
outline: none !important;
}
.wt-settings-input:focus { border-color: rgba(0,245,200,0.4) !important; }
/* ─── Resize handle indicator ────────────────────────────────────── */
#wt-window::after {
content: '';
position: absolute;
bottom: 3px; right: 4px;
width: 10px; height: 10px;
border-right: 2px solid rgba(0,245,200,0.25);
border-bottom: 2px solid rgba(0,245,200,0.25);
pointer-events: none;
}
/* ─── Toggle button (always visible) ────────────────────────────── */
#wt-toggle {
position: fixed;
bottom: 28px;
right: 28px;
z-index: 7999;
font-family: 'Orbitron', monospace;
font-size: 0.62rem;
letter-spacing: 0.2em;
text-transform: uppercase;
padding: 10px 18px;
border: 1px solid rgba(0,245,200,0.4);
border-radius: 4px;
background: rgba(3,8,14,0.95);
color: #00f5c8;
cursor: none;
transition: all 0.2s ease;
box-shadow: 0 4px 20px rgba(0,0,0,0.5), 0 0 12px rgba(0,245,200,0.1);
display: none;
}
#wt-toggle:hover {
background: rgba(0,245,200,0.12);
box-shadow: 0 4px 20px rgba(0,0,0,0.5), 0 0 20px rgba(0,245,200,0.2);
}
|
|
| Custom JavaScript |
/* ============================================================
HEIMDALL — COMMAND CENTER THEME | custom.js
Drop in: storage/app/public/custom.js
============================================================ */
(function () {
'use strict';
/* ─── UTILITY ──────────────────────────────────────────────── */
const qs = (sel, ctx = document) => ctx.querySelector(sel);
const qsa = (sel, ctx = document) => [...ctx.querySelectorAll(sel)];
const on = (el, ev, fn, opts) => el && el.addEventListener(ev, fn, opts);
/* =====================================================
1. PARTICLE CANVAS — deep-space starfield with
teal constellation lines
===================================================== */
function initParticles() {
const canvas = document.createElement('canvas');
canvas.id = 'heimdall-canvas';
document.body.prepend(canvas);
const ctx = canvas.getContext('2d');
const CONFIG = {
count: 120,
maxDist: 140,
speed: 0.22,
teal: '0, 245, 200',
amber: '255, 184, 0',
mouseDist: 130,
mouseRepel: 0.012,
};
let W, H, mouse = { x: -9999, y: -9999 };
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
resize();
on(window, 'resize', resize);
on(window, 'mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
class Particle {
constructor() { this.reset(true); }
reset(init = false) {
this.x = Math.random() * W;
this.y = init ? Math.random() * H : -4;
this.vx = (Math.random() - 0.5) * CONFIG.speed;
this.vy = (Math.random() - 0.5) * CONFIG.speed;
this.r = Math.random() * 1.4 + 0.3;
this.alpha = Math.random() * 0.6 + 0.2;
// ~5% amber stars
this.color = Math.random() < 0.05 ? CONFIG.amber : CONFIG.teal;
this.pulse = Math.random() * Math.PI * 2;
this.pulseSpeed = 0.015 + Math.random() * 0.02;
}
update() {
// mouse repel
const dx = this.x - mouse.x;
const dy = this.y - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < CONFIG.mouseDist) {
const force = (CONFIG.mouseDist - dist) / CONFIG.mouseDist;
this.vx += dx / dist * force * CONFIG.mouseRepel;
this.vy += dy / dist * force * CONFIG.mouseRepel;
}
// dampen velocity
this.vx *= 0.994;
this.vy *= 0.994;
this.x += this.vx;
this.y += this.vy;
this.pulse += this.pulseSpeed;
// wrap edges
if (this.x < -10) this.x = W + 10;
if (this.x > W + 10) this.x = -10;
if (this.y < -10) this.y = H + 10;
if (this.y > H + 10) this.y = -10;
}
draw() {
const a = this.alpha * (0.75 + 0.25 * Math.sin(this.pulse));
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${this.color}, ${a})`;
ctx.fill();
}
}
const particles = Array.from({ length: CONFIG.count }, () => new Particle());
function drawLines() {
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const a = particles[i], b = particles[j];
const dx = a.x - b.x, dy = a.y - b.y;
const d2 = dx * dx + dy * dy;
if (d2 < CONFIG.maxDist * CONFIG.maxDist) {
const alpha = (1 - Math.sqrt(d2) / CONFIG.maxDist) * 0.18;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.strokeStyle = `rgba(${CONFIG.teal}, ${alpha})`;
ctx.lineWidth = 0.6;
ctx.stroke();
}
}
}
}
function loop() {
ctx.clearRect(0, 0, W, H);
particles.forEach(p => { p.update(); p.draw(); });
drawLines();
requestAnimationFrame(loop);
}
loop();
}
/* =====================================================
2. CUSTOM CURSOR
===================================================== */
function initCursor() {
const dot = document.createElement('div');
const ring = document.createElement('div');
dot.id = 'cursor-dot';
ring.id = 'cursor-ring';
document.body.append(dot, ring);
let rx = 0, ry = 0, mx = 0, my = 0;
let isHovering = false;
on(document, 'mousemove', e => {
mx = e.clientX; my = e.clientY;
dot.style.left = mx + 'px';
dot.style.top = my + 'px';
// Hide custom cursor inside xterm so it doesn't interfere
const inXterm = e.target.closest('#wt-xterm');
dot.style.opacity = inXterm ? '0' : '1';
ring.style.opacity = inXterm ? '0' : '1';
});
// Lag ring for smooth trail
(function ringLoop() {
rx += (mx - rx) * 0.14;
ry += (my - ry) * 0.14;
ring.style.left = (rx - 14) + 'px';
ring.style.top = (ry - 14) + 'px';
requestAnimationFrame(ringLoop);
})();
// Expand ring on interactive elements
const hotSelectors = 'a, button, .btn, .app-card, .card, .application-card, input, select, textarea, [role="button"]';
on(document, 'mouseover', e => {
if (e.target.closest(hotSelectors)) {
ring.style.width = '44px';
ring.style.height = '44px';
ring.style.borderColor = 'rgba(0, 245, 200, 0.9)';
dot.style.transform = 'scale(2)';
isHovering = true;
}
});
on(document, 'mouseout', e => {
if (isHovering && !e.target.closest(hotSelectors)) {
ring.style.width = '28px';
ring.style.height = '28px';
ring.style.borderColor = 'rgba(0, 245, 200, 0.6)';
dot.style.transform = 'scale(1)';
isHovering = false;
}
});
// Click burst
on(document, 'click', e => {
const burst = document.createElement('div');
burst.style.cssText = `
position:fixed; left:${e.clientX - 20}px; top:${e.clientY - 20}px;
width:40px; height:40px; border-radius:50%;
border:2px solid rgba(0,245,200,0.8);
pointer-events:none; z-index:99997;
animation:cursorBurst 0.5s ease-out forwards;
`;
document.body.append(burst);
setTimeout(() => burst.remove(), 520);
});
}
/* =====================================================
3. HUD STATUS BAR — 3-column large metrics
===================================================== */
function initHudBar() {
const SESSION_ID = Math.random().toString(36).slice(2,9).toUpperCase();
const bar = document.createElement('div');
bar.className = 'heimdall-hud-bar';
bar.innerHTML = `
<!-- LEFT: uptime + session -->
<div class="hud-left">
<span class="hud-uptime-label">Uptime</span>
<span class="hud-uptime-value" id="hud-uptime">00:00:00</span>
<div class="hud-session-row">
<div class="hud-ping-dot"></div>
<span class="hud-session-label">Online</span>
<span class="hud-session-id">#${SESSION_ID}</span>
</div>
</div>
<!-- CENTER: clock + date -->
<div class="hud-center">
<span class="hud-clock" id="hud-clock">--:--:--</span>
<span class="hud-date" id="hud-date">--- -- ----</span>
<span class="hud-sys-tag">WAWTOR</span>
</div>
<!-- RIGHT: stats + terminal button -->
<div class="hud-right">
<div class="hud-stat-row">
<span class="hud-stat-label">Containers</span>
<span class="hud-stat-value">6</span>
</div>
<div class="hud-stat-row">
<span class="hud-stat-label">Tailscale</span>
<span class="hud-stat-value">UP</span>
</div>
<div class="hud-stat-row">
<span class="hud-stat-label">Temp</span>
<span class="hud-stat-value amber">38°C</span>
</div>
<button class="hud-terminal-btn" id="hud-term-btn">⌨ Terminal</button>
</div>
`;
const nav = qs('header, nav.navbar, .navbar, #topnav');
if (nav && nav.parentNode) {
nav.parentNode.insertBefore(bar, nav.nextSibling);
} else {
document.body.prepend(bar);
}
// Live clock
function tick() {
const now = new Date();
const hh = String(now.getHours()).padStart(2,'0');
const mm = String(now.getMinutes()).padStart(2,'0');
const ss = String(now.getSeconds()).padStart(2,'0');
const clockEl = qs('#hud-clock');
if (clockEl) clockEl.textContent = `${hh}:${mm}:${ss}`;
const dateEl = qs('#hud-date');
if (dateEl) dateEl.textContent = now.toLocaleDateString('en-US', {
weekday:'short', month:'short', day:'numeric', year:'numeric'
}).toUpperCase();
}
tick();
setInterval(tick, 1000);
// Session uptime counter
const start = Date.now();
setInterval(() => {
const el = qs('#hud-uptime');
if (!el) return;
const s = Math.floor((Date.now() - start) / 1000);
const H = String(Math.floor(s / 3600)).padStart(2,'0');
const M = String(Math.floor((s % 3600) / 60)).padStart(2,'0');
const S = String(s % 60).padStart(2,'0');
el.textContent = `${H}:${M}:${S}`;
}, 1000);
// Terminal button wires to the terminal widget
on(qs('#hud-term-btn'), 'click', () => {
const win = qs('#wt-window');
const tog = qs('#wt-toggle');
if (!win) return;
if (win.classList.contains('hidden')) {
win.classList.remove('hidden');
if (tog) tog.style.display = 'none';
} else {
win.classList.add('hidden');
if (tog) tog.style.display = 'block';
}
});
}
/* =====================================================
3b. TERMINAL WIDGET
===================================================== */
function initTerminalWidget() {
/* ── Default macros ────────────────────────────────── */
const DEFAULT_MACROS = [
{ id: 'm1', name: 'Restart Nginx', cmd: 'docker restart nginx-proxy' },
{ id: 'm2', name: 'Restart Portainer', cmd: 'docker restart portainer' },
{ id: 'm3', name: 'Restart Nextcloud', cmd: 'docker restart nextcloud' },
{ id: 'm4', name: 'Restart MariaDB', cmd: 'docker restart mariadb' },
{ id: 'm5', name: 'Restart Redis', cmd: 'docker restart redis' },
{ id: 'm6', name: 'Docker PS', cmd: 'docker ps --format "table {{.Names}}\\t{{.Status}}"' },
{ id: 'm7', name: 'System Uptime', cmd: 'uptime' },
{ id: 'm8', name: 'Disk Usage', cmd: 'df -h /' },
];
function loadMacros() {
try {
const raw = localStorage.getItem('wt_macros');
return raw ? JSON.parse(raw) : DEFAULT_MACROS;
} catch { return DEFAULT_MACROS; }
}
function saveMacros(list) {
localStorage.setItem('wt_macros', JSON.stringify(list));
}
function loadWsUrl() {
return localStorage.getItem('wt_ws_url') || 'ws://localhost:7682/ws';
}
function saveWsUrl(url) {
localStorage.setItem('wt_ws_url', url);
}
/* ── Load xterm.js + fit addon from CDN ─────────────── */
function loadScript(src) {
return new Promise((res, rej) => {
if (document.querySelector(`script[src="${src}"]`)) { res(); return; }
const s = document.createElement('script');
s.src = src; s.onload = res; s.onerror = rej;
document.head.appendChild(s);
});
}
function loadLink(href) {
if (document.querySelector(`link[href="${href}"]`)) return;
const l = document.createElement('link');
l.rel = 'stylesheet'; l.href = href;
document.head.appendChild(l);
}
/* ── Build DOM ─────────────────────────────────────── */
const win = document.createElement('div');
win.id = 'wt-window';
win.innerHTML = `
<div id="wt-titlebar">
<div class="wt-traffic">
<div class="wt-dot close" id="wt-close"></div>
<div class="wt-dot min" id="wt-min"></div>
<div class="wt-dot max" id="wt-max"></div>
</div>
<span class="wt-title">dmw@wawtor — terminal</span>
<span id="wt-conn-status" class="disconnected">DISCONNECTED</span>
<button class="wt-btn" id="wt-macro-toggle">⚡ Macros</button>
<button class="wt-btn" id="wt-reconnect-btn">Reconnect</button>
</div>
<div id="wt-body">
<!-- xterm mount -->
<div id="wt-xterm"></div>
<!-- connect screen -->
<div id="wt-connect-screen">
<div class="wt-connect-title">Connect to Backend Terminal</div>
<div class="wt-connect-desc">
This widget connects to a WebSocket terminal server.<br>
Recommended: <code>ttyd</code> running on your wawtor server.<br><br>
Install: <code>apt install ttyd</code><br>
Run: <code>ttyd -p 7681 bash</code>
</div>
<div class="wt-connect-row">
<input type="text" id="wt-ws-input" placeholder="ws://wawtor:7682/ws" />
<button class="wt-connect-btn" id="wt-do-connect">Connect</button>
</div>
</div>
<!-- macro panel -->
<div id="wt-macro-panel">
<div class="wt-macro-header">
<span class="wt-macro-title">⚡ Service Macros</span>
<button class="wt-macro-edit-btn" id="wt-edit-mode-btn">Edit</button>
</div>
<div class="wt-macro-list" id="wt-macro-list"></div>
<button class="wt-macro-add-btn" id="wt-add-macro">+ Add Macro</button>
<div class="wt-macro-settings">
<div class="wt-settings-label">WebSocket URL</div>
<input class="wt-settings-input" id="wt-ws-setting" type="text"
placeholder="ws://wawtor:7681/ws" />
</div>
</div>
</div>
`;
document.body.appendChild(win);
// Toggle button (shows when window is hidden)
const toggleBtn = document.createElement('button');
toggleBtn.id = 'wt-toggle';
toggleBtn.textContent = '⌨ Terminal';
document.body.appendChild(toggleBtn);
on(toggleBtn, 'click', () => {
win.classList.remove('hidden');
toggleBtn.style.display = 'none';
});
/* ── State ──────────────────────────────────────────── */
let term = null;
let fitAddon = null;
let ws = null;
let macros = loadMacros();
let editMode = false;
let isMinimized = false;
const connScreen = qs('#wt-connect-screen');
const xtermMount = qs('#wt-xterm');
const macroPanel = qs('#wt-macro-panel');
const macroList = qs('#wt-macro-list');
const connStatus = qs('#wt-conn-status');
const wsInput = qs('#wt-ws-input');
const wsSetting = qs('#wt-ws-setting');
// Pre-fill stored URL
const stored = loadWsUrl();
if (wsInput) wsInput.value = stored;
if (wsSetting) wsSetting.value = stored;
/* ── Load xterm then init ──────────────────────────── */
loadLink('https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css');
Promise.all([
loadScript('https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js'),
loadScript('https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js'),
]).then(() => {
term = new Terminal({
theme: {
background: '#0b0d0f',
foreground: '#e8f4f8',
cursor: '#00f5c8',
cursorAccent: '#0b0d0f',
selectionBackground: 'rgba(0,245,200,0.25)',
black: '#0b0d0f', brightBlack: '#3d6070',
red: '#ff3a5c', brightRed: '#ff6b84',
green: '#00f5c8', brightGreen: '#00f5c8',
yellow: '#ffb800', brightYellow: '#ffca44',
blue: '#4a9eff', brightBlue: '#7ab8ff',
magenta: '#cc44ff', brightMagenta: '#dd88ff',
cyan: '#00c49e', brightCyan: '#00f5c8',
white: '#7aaabb', brightWhite: '#e8f4f8',
},
fontFamily: "'Share Tech Mono', 'Courier New', monospace",
fontSize: 13,
lineHeight: 1.4,
cursorBlink: true,
cursorStyle: 'block',
scrollback: 2000,
allowTransparency: true,
});
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(xtermMount);
// Register input handler ONCE — references outer ws variable
term.onData(data => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send('0' + data);
}
});
// Register resize handler here where term is defined
term.onResize(() => sendResize());
// Click anywhere in terminal to focus it
on(xtermMount, 'click', () => term.focus());
// Try auto-connect
attemptConnect(loadWsUrl());
// Resize observer
new ResizeObserver(() => {
if (fitAddon && !isMinimized) {
try { fitAddon.fit(); } catch(e){}
}
}).observe(win);
});
/* ── WebSocket connection ───────────────────────────── */
function setStatus(state) {
connStatus.className = state;
const labels = { connected:'CONNECTED', disconnected:'DISCONNECTED', connecting:'CONNECTING…' };
connStatus.textContent = labels[state] || state.toUpperCase();
}
function attemptConnect(url) {
if (!url || !url.startsWith('ws')) {
showConnectScreen(true);
return;
}
if (ws) { try { ws.close(); } catch(e){} }
setStatus('connecting');
ws = new WebSocket(url, ['tty']); // 'tty' subprotocol is REQUIRED by ttyd
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
setStatus('connected');
showConnectScreen(false);
saveWsUrl(url);
if (wsSetting) wsSetting.value = url;
if (wsInput) wsInput.value = url;
// ttyd auth — must include cols/rows in initial message
const cols = (term && term.cols) || 80;
const rows = (term && term.rows) || 24;
ws.send(JSON.stringify({ AuthToken: '', columns: cols, rows: rows }));
setTimeout(() => {
if (fitAddon) { try { fitAddon.fit(); } catch(e){} }
sendResize();
term && term.focus();
}, 200);
};
ws.onmessage = (e) => {
if (!term) return;
if (typeof e.data === 'string') {
const type = e.data[0];
const payload = e.data.slice(1);
if (type === '0') {
// OUTPUT — ttyd uses '0' for server→client output
term.write(payload);
} else if (type === '1') {
// Also handle '1' just in case build differs
term.write(payload);
} else if (type === '2') {
try {
const t = JSON.parse(payload);
qs('.wt-title').textContent = t.title || 'dmw@wawtor — terminal';
} catch(e) {}
}
} else if (e.data instanceof ArrayBuffer) {
// Binary frame — first byte is opcode, rest is data
const bytes = new Uint8Array(e.data);
if (bytes.length > 1) {
term.write(bytes.slice(1)); // strip the opcode byte
}
}
};
ws.onerror = () => { setStatus('disconnected'); showConnectScreen(true); };
ws.onclose = () => { setStatus('disconnected'); };
}
function sendResize() {
if (!ws || ws.readyState !== WebSocket.OPEN || !term) return;
// ttyd resize opcode is '4', NOT '1'
ws.send('4' + JSON.stringify({ columns: term.cols, rows: term.rows }));
}
function showConnectScreen(show) {
connScreen.classList.toggle('visible', show);
xtermMount.style.display = show ? 'none' : 'block';
}
/* ── Macro panel ────────────────────────────────────── */
function renderMacros() {
macroList.innerHTML = '';
macros.forEach((m, idx) => {
const item = document.createElement('div');
item.className = 'wt-macro-item';
item.dataset.id = m.id;
item.innerHTML = `
<div class="wt-macro-run" title="Run macro">▶</div>
<div class="wt-macro-info">
<div class="wt-macro-name">${escHtml(m.name)}</div>
<div class="wt-macro-cmd">${escHtml(m.cmd)}</div>
</div>
<div class="wt-macro-del" title="Delete">✕</div>
`;
// Run
qs('.wt-macro-run', item).onclick = () => runMacro(m, qs('.wt-macro-run', item));
// Double-click name/cmd to edit
qs('.wt-macro-info', item).ondblclick = () => enterEditItem(item, m);
// Delete
qs('.wt-macro-del', item).onclick = () => {
macros = macros.filter(x => x.id !== m.id);
saveMacros(macros);
renderMacros();
};
macroList.appendChild(item);
});
}
function enterEditItem(item, m) {
item.classList.add('editing');
item.innerHTML = `
<input class="wt-macro-input name-input" value="${escHtml(m.name)}" placeholder="Macro name" />
<input class="wt-macro-input cmd-input" value="${escHtml(m.cmd)}" placeholder="Command…" />
<div class="wt-macro-save-row">
<button class="wt-macro-cancel">Cancel</button>
<button class="wt-macro-save">Save</button>
</div>
`;
qs('.wt-macro-save', item).onclick = () => {
const newName = qs('.name-input', item).value.trim();
const newCmd = qs('.cmd-input', item).value.trim();
if (!newName || !newCmd) return;
const idx = macros.findIndex(x => x.id === m.id);
if (idx !== -1) { macros[idx].name = newName; macros[idx].cmd = newCmd; }
saveMacros(macros);
renderMacros();
};
qs('.wt-macro-cancel', item).onclick = () => renderMacros();
qs('.name-input', item).focus();
}
function runMacro(m, btn) {
if (ws && ws.readyState === WebSocket.OPEN && term) {
// ttyd text protocol: '0' + data
ws.send('0' + m.cmd + '\r');
term.focus();
// Visual feedback
btn.classList.add('running');
btn.textContent = '⟳';
setTimeout(() => { btn.classList.remove('running'); btn.textContent = '▶'; }, 800);
} else {
// Fallback: copy to clipboard
navigator.clipboard.writeText(m.cmd).catch(() => {});
btn.textContent = '✓';
setTimeout(() => { btn.textContent = '▶'; }, 1200);
}
}
// Edit mode toggle
on(qs('#wt-edit-mode-btn'), 'click', () => {
editMode = !editMode;
macroPanel.classList.toggle('edit-mode', editMode);
qs('#wt-edit-mode-btn').classList.toggle('active', editMode);
qs('#wt-edit-mode-btn').textContent = editMode ? 'Done' : 'Edit';
});
// Add new macro
on(qs('#wt-add-macro'), 'click', () => {
const newM = { id: 'm' + Date.now(), name: 'New Macro', cmd: 'echo hello' };
macros.push(newM);
saveMacros(macros);
renderMacros();
// Auto-enter edit on the new item
const lastItem = macroList.lastElementChild;
if (lastItem) enterEditItem(lastItem, newM);
});
// WS URL setting
on(wsSetting, 'change', () => {
const url = wsSetting.value.trim();
saveWsUrl(url);
if (wsInput) wsInput.value = url;
});
// Connect screen button
on(qs('#wt-do-connect'), 'click', () => {
const url = wsInput.value.trim();
if (url) attemptConnect(url);
});
on(wsInput, 'keydown', e => { if (e.key === 'Enter') qs('#wt-do-connect').click(); });
// Reconnect button
on(qs('#wt-reconnect-btn'), 'click', () => attemptConnect(loadWsUrl()));
// Macro panel toggle
on(qs('#wt-macro-toggle'), 'click', () => {
macroPanel.classList.toggle('open');
qs('#wt-macro-toggle').classList.toggle('active', macroPanel.classList.contains('open'));
setTimeout(() => { if (fitAddon) try { fitAddon.fit(); } catch(e){} }, 300);
});
renderMacros();
/* ── Window controls ────────────────────────────────── */
on(qs('#wt-close'), 'click', () => {
win.classList.add('hidden');
toggleBtn.style.display = 'block';
});
on(qs('#wt-min'), 'click', () => {
isMinimized = !isMinimized;
win.classList.toggle('minimized', isMinimized);
});
on(qs('#wt-max'), 'click', () => {
win.classList.toggle('maximized');
setTimeout(() => { if (fitAddon) try { fitAddon.fit(); } catch(e){} }, 100);
});
/* ── Drag ───────────────────────────────────────────── */
const titlebar = qs('#wt-titlebar');
let dragOffX = 0, dragOffY = 0, dragging = false;
function savePos() {
const rect = win.getBoundingClientRect();
localStorage.setItem('wt_pos', JSON.stringify({ left: rect.left, top: rect.top }));
}
function restorePos() {
try {
const saved = JSON.parse(localStorage.getItem('wt_pos'));
if (saved) {
// Clamp to viewport in case screen size changed
const maxL = window.innerWidth - 200;
const maxT = window.innerHeight - 60;
const left = Math.max(0, Math.min(saved.left, maxL));
const top = Math.max(0, Math.min(saved.top, maxT));
win.style.right = 'auto';
win.style.bottom = 'auto';
win.style.left = left + 'px';
win.style.top = top + 'px';
}
} catch(e) {}
}
on(titlebar, 'mousedown', e => {
if (e.target.closest('.wt-dot, .wt-btn')) return;
if (win.classList.contains('maximized')) return;
dragging = true;
const rect = win.getBoundingClientRect();
dragOffX = e.clientX - rect.left;
dragOffY = e.clientY - rect.top;
win.style.transition = 'none';
win.style.right = 'auto';
win.style.bottom = 'auto';
e.preventDefault();
});
on(document, 'mousemove', e => {
if (!dragging) return;
win.style.left = (e.clientX - dragOffX) + 'px';
win.style.top = (e.clientY - dragOffY) + 'px';
});
on(document, 'mouseup', () => {
if (dragging) { dragging = false; savePos(); }
});
// Restore saved position, start hidden
restorePos();
win.classList.add('hidden');
toggleBtn.style.display = 'block';
}
function escHtml(s) {
return String(s)
.replace(/&/g,'&')
.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/"/g,'"');
}
/* =====================================================
4. HUD CORNER BRACKETS ON CARDS
===================================================== */
function initCornerBrackets() {
const cards = qsa('.item');
cards.forEach(card => {
if (!card.querySelector('.corner-bracket')) {
const positions = [
{ top:'5px', left:'5px', borderWidth:'2px 0 0 2px' },
{ top:'5px', right:'5px', borderWidth:'2px 2px 0 0' },
{ bottom:'5px', left:'5px', borderWidth:'0 0 2px 2px' },
{ bottom:'5px', right:'68px', borderWidth:'0 2px 2px 0' },
];
positions.forEach(pos => {
const b = document.createElement('div');
b.className = 'corner-bracket';
b.style.cssText = Object.entries({
...pos,
position:'absolute',
width:'10px', height:'10px',
borderColor:'rgba(0,245,200,0.7)',
borderStyle:'solid',
opacity:'0',
transition:'opacity 0.3s ease',
pointerEvents:'none',
zIndex:'3',
}).map(([k,v]) => `${k.replace(/[A-Z]/g,m=>'-'+m.toLowerCase())}:${v}`).join(';');
card.appendChild(b);
});
card.addEventListener('mouseenter', () => {
card.querySelectorAll('.corner-bracket').forEach(b => b.style.opacity = '1');
});
card.addEventListener('mouseleave', () => {
card.querySelectorAll('.corner-bracket').forEach(b => b.style.opacity = '0');
});
}
});
}
function initCardTilt() {
const cards = qsa('.item');
cards.forEach(card => {
on(card, 'mousemove', e => {
const rect = card.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const rx = ((e.clientY - cy) / (rect.height / 2)) * -5;
const ry = ((e.clientX - cx) / (rect.width / 2)) * 5;
card.style.transform = `perspective(600px) rotateX(${rx}deg) rotateY(${ry}deg) translateY(-4px) scale(1.02)`;
});
on(card, 'mouseleave', () => {
card.style.transform = '';
card.style.transition = 'transform 0.45s cubic-bezier(0.23,1,0.32,1)';
setTimeout(() => { card.style.transition = ''; }, 460);
});
});
}
/* =====================================================
6. RIPPLE EFFECT on interactive elements
===================================================== */
function initRipple() {
on(document, 'click', e => {
const target = e.target.closest('.btn, button, .item');
if (!target) return;
const rect = target.getBoundingClientRect();
const ripple = document.createElement('span');
const size = Math.max(rect.width, rect.height) * 2;
ripple.style.cssText = `
position:absolute;
width:${size}px; height:${size}px;
left:${e.clientX - rect.left - size/2}px;
top:${e.clientY - rect.top - size/2}px;
background:rgba(0,245,200,0.12);
border-radius:50%;
transform:scale(0);
animation:rippleAnim 0.6s ease-out forwards;
pointer-events:none; z-index:10;
`;
target.style.overflow = 'hidden';
target.appendChild(ripple);
setTimeout(() => ripple.remove(), 620);
});
}
/* =====================================================
7. BOOT SEQUENCE — wawtor server terminal shell
===================================================== */
function initBootSequence() {
if (sessionStorage.getItem('hd_boot_v2')) return;
sessionStorage.setItem('hd_boot_v2', '1');
const HOST = 'wawtor';
const USER = 'dmw';
const KERNEL = '6.8.0-wawtor-lts';
const now = new Date();
const bootTs = now.toISOString().replace('T', ' ').slice(0, 19);
const upSeed = (Math.random() * 4 + 0.5).toFixed(2);
/* Each entry:
{ cmd: string | null, out: [ { text, color } ] }
cmd=null → output-only block (kernel banner, etc.)
*/
const SCRIPT = [
// ── kernel boot banner (no prompt) ──────────────────────
{ cmd: null, out: [
{ text: `[ 0.000000] Linux version ${KERNEL} (gcc 13.2.0)`, color: '#4a7c6e' },
{ text: `[ 0.000000] BIOS-provided memory map:`, color: '#4a7c6e' },
{ text: `[ 0.000000] ACPI: IRQ0 used by override.`, color: '#4a7c6e' },
{ text: `[ 0.418312] pci 0000:00:01.0: [8086:1237] type 00 class 0x060000`, color: '#4a7c6e' },
{ text: `[ 1.203441] EXT4-fs (nvme0n1p2): mounted filesystem with ordered data mode`, color: '#4a7c6e' },
{ text: `[ 1.887604] systemd[1]: Detected architecture x86-64.`, color: '#4a7c6e' },
{ text: `[ 1.887660] systemd[1]: Hostname set to <${HOST}>.`, color: '#00f5c8' },
{ text: ``, color: '' },
{ text: `Ubuntu 24.04.2 LTS ${HOST} ${KERNEL} #1 SMP`, color: '#7aaabb' },
{ text: ``, color: '' },
]},
// ── systemctl check ─────────────────────────────────────
{ cmd: `systemctl is-active --all 2>/dev/null | head -12`, out: [
{ text: ` docker active`, color: '#00f5c8' },
{ text: ` portainer active`, color: '#00f5c8' },
{ text: ` nextcloud active`, color: '#00f5c8' },
{ text: ` tailscaled active`, color: '#00f5c8' },
{ text: ` nginx active`, color: '#00f5c8' },
{ text: ` samba active`, color: '#00f5c8' },
{ text: ` ssh active`, color: '#00f5c8' },
{ text: ` cron active`, color: '#00f5c8' },
{ text: ` ufw active`, color: '#00f5c8' },
]},
// ── disk usage ──────────────────────────────────────────
{ cmd: `df -h --output=target,used,avail,pcent | column -t`, out: [
{ text: `Mounted on Used Avail Use%`, color: '#7aaabb' },
{ text: `/ 14G 198G 7%`, color: '#e8f4f8' },
{ text: `/mnt/storage 2.1T 5.7T 27%`, color: '#e8f4f8' },
{ text: `/mnt/media 890G 3.1T 22%`, color: '#e8f4f8' },
{ text: `/boot/efi 11M 511M 3%`, color: '#e8f4f8' },
]},
// ── docker containers ───────────────────────────────────
{ cmd: `docker ps --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}" 2>/dev/null`, out: [
{ text: `NAMES STATUS PORTS`, color: '#7aaabb' },
{ text: `portainer Up 14 days 0.0.0.0:9443->9443/tcp`, color: '#e8f4f8' },
{ text: `nextcloud Up 14 days 0.0.0.0:8080->80/tcp`, color: '#e8f4f8' },
{ text: `watchtower Up 14 days 8080/tcp`, color: '#e8f4f8' },
{ text: `nginx-proxy Up 14 days 0.0.0.0:80->80/tcp, 443->443/tcp`, color: '#e8f4f8' },
{ text: `mariadb Up 14 days 3306/tcp`, color: '#e8f4f8' },
{ text: `redis Up 14 days 6379/tcp`, color: '#e8f4f8' },
]},
// ── tailscale status ─────────────────────────────────────
{ cmd: `tailscale status 2>/dev/null`, out: [
{ text: `100.x.x.x ${HOST} tagged-devices active; relay "mia", tx 1.4MiB rx 892KiB`, color: '#00f5c8' },
]},
// ── temp / uptime ────────────────────────────────────────
{ cmd: `uptime && sensors 2>/dev/null | grep -E 'Core|temp'`, out: [
{ text: ` ${bootTs} up ${upSeed} days, 1 user, load average: 0.12, 0.18, 0.21`, color: '#e8f4f8' },
{ text: `Core 0: +38.0°C (high = +80.0°C, crit = +100.0°C)`, color: '#ffb800' },
{ text: `Core 1: +37.0°C (high = +80.0°C, crit = +100.0°C)`, color: '#ffb800' },
{ text: `Core 2: +39.0°C (high = +80.0°C, crit = +100.0°C)`, color: '#ffb800' },
{ text: `Core 3: +38.0°C (high = +80.0°C, crit = +100.0°C)`, color: '#ffb800' },
]},
// ── final ────────────────────────────────────────────────
{ cmd: `echo "all systems nominal — welcome back, ${USER}"`, out: [
{ text: ``, color: '' },
{ text: ` ██╗ ██╗ █████╗ ██╗ ██╗████████╗ ██████╗ ██████╗ `, color: '#00f5c8' },
{ text: ` ██║ ██║██╔══██╗██║ ██║╚══██╔══╝██╔═══██╗██╔══██╗`, color: '#00f5c8' },
{ text: ` ██║ █╗ ██║███████║██║ █╗ ██║ ██║ ██║ ██║██████╔╝`, color: '#00c49e' },
{ text: ` ██║███╗██║██╔══██║██║███╗██║ ██║ ██║ ██║██╔══██╗`, color: '#00c49e' },
{ text: ` ╚███╔███╔╝██║ ██║╚███╔███╔╝ ██║ ╚██████╔╝██║ ██║`, color: '#009e7e' },
{ text: ` ╚══╝╚══╝ ╚═╝ ╚═╝ ╚══╝╚══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝`, color: '#009e7e' },
{ text: ``, color: '' },
{ text: ` all systems nominal — welcome back, ${USER}`, color: '#ffb800' },
{ text: ``, color: '' },
]},
];
/* ── Build overlay ──────────────────────────────────────── */
const overlay = document.createElement('div');
overlay.id = 'wawtor-boot';
overlay.style.cssText = `
position:fixed; inset:0; z-index:99999;
background:#0b0d0f;
display:flex; flex-direction:column;
font-family:'Share Tech Mono',monospace;
font-size:0.8rem; line-height:1.7;
overflow:hidden;
`;
/* terminal title bar */
const titleBar = document.createElement('div');
titleBar.style.cssText = `
flex-shrink:0; display:flex; align-items:center; gap:8px;
padding:8px 16px;
background:#161a1e;
border-bottom:1px solid rgba(0,245,200,0.12);
`;
titleBar.innerHTML = `
<span style="width:10px;height:10px;border-radius:50%;background:#ff5f57;display:inline-block"></span>
<span style="width:10px;height:10px;border-radius:50%;background:#ffbd2e;display:inline-block"></span>
<span style="width:10px;height:10px;border-radius:50%;background:#28c840;display:inline-block"></span>
<span style="margin-left:12px;color:#3d6070;font-size:0.72rem;letter-spacing:0.14em">
${USER}@${HOST}: ~
</span>
`;
overlay.appendChild(titleBar);
/* scrollable terminal body */
const body = document.createElement('div');
body.style.cssText = `
flex:1; overflow-y:auto; padding:18px 24px 24px;
scroll-behavior:smooth;
`;
/* scanlines inside terminal */
body.style.backgroundImage = `repeating-linear-gradient(
to bottom, transparent, transparent 2px,
rgba(0,0,0,0.15) 2px, rgba(0,0,0,0.15) 4px
)`;
overlay.appendChild(body);
document.body.appendChild(overlay);
/* ── Rendering helpers ──────────────────────────────────── */
function appendLine(text, color, extraStyle = '') {
const el = document.createElement('div');
el.style.cssText = `color:${color || '#e8f4f8'};white-space:pre;${extraStyle}`;
el.textContent = text;
body.appendChild(el);
body.scrollTop = body.scrollHeight;
return el;
}
function appendPrompt(cmd) {
const row = document.createElement('div');
row.style.cssText = `display:flex; align-items:baseline; white-space:pre;`;
row.innerHTML = `
<span style="color:#00f5c8">${USER}@${HOST}</span>
<span style="color:#7aaabb">:</span>
<span style="color:#4a9eff">~</span>
<span style="color:#7aaabb">$ </span>
<span id="cmd-target" style="color:#e8f4f8"></span>
<span class="blink-cursor" style="
display:inline-block;width:8px;height:1em;
background:#00f5c8;margin-left:1px;
animation:blinkCursor 0.75s step-end infinite;
vertical-align:text-bottom;
"></span>
`;
body.appendChild(row);
body.scrollTop = body.scrollHeight;
return { target: row.querySelector('#cmd-target'), cursor: row.querySelector('.blink-cursor') };
}
/* ── Sequencer ──────────────────────────────────────────── */
let scriptIdx = 0;
// Generate a plausible-looking MAC address seeded to this browser
function genMac() {
const seed = navigator.userAgent + screen.width + screen.height + navigator.language;
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = ((hash << 5) - hash) + seed.charCodeAt(i);
hash |= 0;
}
const hex = Math.abs(hash).toString(16).padStart(12, '0');
// Set locally administered bit on first octet for realism
const octs = [];
for (let i = 0; i < 12; i += 2) octs.push(hex.slice(i, i + 2));
octs[0] = (parseInt(octs[0], 16) | 0x02 & ~0x01).toString(16).padStart(2,'0');
return octs.join(':').toUpperCase();
}
const CLIENT_MAC = genMac();
function runNext() {
if (scriptIdx >= SCRIPT.length) {
// Show MAC warning prompt instead of auto-fading
showMacPrompt();
return;
}
const block = SCRIPT[scriptIdx++];
if (block.cmd === null) {
typeOutputLines(block.out, 0, runNext);
} else {
const { target, cursor } = appendPrompt('');
typeCommand(block.cmd, target, cursor, () => {
cursor.remove();
setTimeout(() => typeOutputLines(block.out, 0, runNext), 40);
});
}
}
function showMacPrompt() {
appendLine('', '');
// Warning box
const warn = document.createElement('div');
warn.style.cssText = `
margin:12px 0;
border:1px solid rgba(255,184,0,0.5);
border-left:3px solid #ffb800;
border-radius:4px;
padding:14px 18px;
background:rgba(255,184,0,0.05);
color:#ffb800;
font-family:'Share Tech Mono',monospace;
font-size:0.78rem;
line-height:1.9;
`;
warn.innerHTML = `
<div style="color:#ffb800;font-size:0.7rem;letter-spacing:0.22em;margin-bottom:8px;">
⚠ SECURITY NOTICE — WAWTOR ACCESS CONTROL
</div>
<div style="color:#e8f4f8;">This terminal session is monitored and logged.</div>
<div style="color:#e8f4f8;">By continuing you acknowledge that the following</div>
<div style="color:#e8f4f8;">client identifier has been recorded:</div>
<div style="margin-top:10px;color:#00f5c8;letter-spacing:0.12em;">
MAC ADDRESS <span style="color:#ffb800;font-size:0.9rem;">${CLIENT_MAC}</span>
</div>
<div style="margin-top:2px;color:#3d6070;font-size:0.65rem;">
Timestamp: ${new Date().toISOString()}
</div>
`;
body.appendChild(warn);
body.scrollTop = body.scrollHeight;
// Prompt row
appendLine('', '');
const promptRow = document.createElement('div');
promptRow.style.cssText = `
display:flex; align-items:center; gap:14px;
font-family:'Share Tech Mono',monospace;
font-size:0.78rem;
`;
promptRow.innerHTML = `
<span style="color:#7aaabb;">Continue to dashboard?</span>
<button id="boot-accept" style="
font-family:'Orbitron',monospace; font-size:0.62rem;
letter-spacing:0.18em; text-transform:uppercase;
padding:6px 20px; border:1px solid rgba(0,245,200,0.5);
border-radius:3px; background:rgba(0,245,200,0.08);
color:#00f5c8; cursor:pointer; transition:all 0.2s ease;
">[ ACCEPT & CONTINUE ]</button>
<button id="boot-decline" style="
font-family:'Orbitron',monospace; font-size:0.62rem;
letter-spacing:0.18em; text-transform:uppercase;
padding:6px 20px; border:1px solid rgba(255,58,92,0.3);
border-radius:3px; background:rgba(255,58,92,0.05);
color:#ff3a5c; cursor:pointer; transition:all 0.2s ease;
">[ DISCONNECT ]</button>
`;
body.appendChild(promptRow);
body.scrollTop = body.scrollHeight;
const acceptBtn = qs('#boot-accept');
const declineBtn = qs('#boot-decline');
acceptBtn.onmouseenter = () => { acceptBtn.style.background = 'rgba(0,245,200,0.18)'; acceptBtn.style.boxShadow = '0 0 16px rgba(0,245,200,0.2)'; };
acceptBtn.onmouseleave = () => { acceptBtn.style.background = 'rgba(0,245,200,0.08)'; acceptBtn.style.boxShadow = 'none'; };
declineBtn.onmouseenter = () => { declineBtn.style.background = 'rgba(255,58,92,0.12)'; };
declineBtn.onmouseleave = () => { declineBtn.style.background = 'rgba(255,58,92,0.05)'; };
acceptBtn.onclick = () => {
promptRow.innerHTML = `<span style="color:#00f5c8;">Access granted. Loading dashboard...</span>`;
body.scrollTop = body.scrollHeight;
setTimeout(() => {
overlay.style.transition = 'opacity 0.8s ease';
overlay.style.opacity = '0';
setTimeout(() => overlay.remove(), 820);
}, 600);
};
declineBtn.onclick = () => {
promptRow.innerHTML = `<span style="color:#ff3a5c;">Session terminated. Goodbye.</span>`;
body.scrollTop = body.scrollHeight;
setTimeout(() => {
document.body.style.transition = 'opacity 0.5s ease';
document.body.style.opacity = '0';
}, 800);
};
}
function typeCommand(cmd, target, cursor, done) {
let i = 0;
function step() {
if (i >= cmd.length) { done(); return; }
target.textContent += cmd[i++];
body.scrollTop = body.scrollHeight;
// Faster: 8-18ms per character instead of 28-66ms
setTimeout(step, 8 + Math.random() * 10);
}
// Shorter initial pause: 60-120ms instead of 180-300ms
setTimeout(step, 60 + Math.random() * 60);
}
function typeOutputLines(lines, idx, done) {
if (idx >= lines.length) { done(); return; }
const { text, color } = lines[idx];
appendLine(text, color);
// Faster: 8-14ms per line instead of 22-40ms
const delay = text.trim() === '' ? 8 : 8 + Math.random() * 6;
setTimeout(() => typeOutputLines(lines, idx + 1, done), delay);
}
/* inject blinkCursor keyframe if not present */
if (!document.querySelector('#wawtor-boot-kf')) {
const s = document.createElement('style');
s.id = 'wawtor-boot-kf';
s.textContent = `
@keyframes blinkCursor {
0%,100% { opacity:1; }
50% { opacity:0; }
}
`;
document.head.appendChild(s);
}
/* kick it off */
setTimeout(runNext, 120);
}
/* =====================================================
8. AMBIENT GLOW TRAIL
===================================================== */
function initGlowTrail() {
const trail = [];
const N = 10;
for (let i = 0; i < N; i++) {
const el = document.createElement('div');
el.style.cssText = `
position:fixed; width:${6 - i*0.4}px; height:${6 - i*0.4}px;
background:rgba(0,245,200,${0.28 - i*0.024});
border-radius:50%;
pointer-events:none; z-index:99990;
transition:left ${0.04 + i*0.04}s ease, top ${0.04 + i*0.04}s ease;
transform:translate(-50%,-50%);
`;
document.body.appendChild(el);
trail.push(el);
}
on(document, 'mousemove', e => {
trail.forEach(el => {
el.style.left = e.clientX + 'px';
el.style.top = e.clientY + 'px';
});
});
}
/* =====================================================
9. INJECT REQUIRED KEYFRAMES
===================================================== */
function injectKeyframes() {
const style = document.createElement('style');
style.textContent = `
@keyframes rippleAnim {
to { transform:scale(1); opacity:0; }
}
@keyframes cursorBurst {
0% { transform:scale(0.5); opacity:1; }
100% { transform:scale(2.5); opacity:0; }
}
`;
document.head.appendChild(style);
}
/* =====================================================
10. OBSERVER: re-init on dynamic card injection
===================================================== */
function initObserver() {
const ob = new MutationObserver(() => {
initCornerBrackets();
initCardTilt();
});
ob.observe(document.body, { childList: true, subtree: true });
}
/* =====================================================
11. KEYBOARD SHORTCUT OVERLAY (press ?)
===================================================== */
function initShortcutHelp() {
const shortcuts = [
['?', 'Toggle this overlay'],
['Ctrl+K', 'Focus search'],
['Escape', 'Close modal / search'],
];
const overlay = document.createElement('div');
overlay.id = 'hd-shortcuts';
overlay.style.cssText = `
display:none; position:fixed; inset:0;
background:rgba(3,5,8,0.92); z-index:99000;
justify-content:center; align-items:center;
backdrop-filter:blur(12px);
`;
const box = document.createElement('div');
box.style.cssText = `
background:#0a1520; border:1px solid rgba(0,245,200,0.3);
border-radius:8px; padding:36px 48px; min-width:380px;
box-shadow:0 0 60px rgba(0,245,200,0.15);
`;
box.innerHTML = `
<div style="font-family:'Orbitron',monospace;font-size:0.7rem;
letter-spacing:0.24em;color:#00f5c8;margin-bottom:24px;
text-transform:uppercase;text-shadow:0 0 10px #00f5c8">
⌨ KEY BINDINGS
</div>
${shortcuts.map(([k,v]) => `
<div style="display:flex;justify-content:space-between;
margin-bottom:14px;font-family:'Share Tech Mono',monospace;
font-size:0.78rem;">
<span style="background:rgba(0,245,200,0.1);border:1px solid rgba(0,245,200,0.25);
border-radius:3px;padding:3px 10px;color:#00f5c8">${k}</span>
<span style="color:#7aaabb;align-self:center">${v}</span>
</div>
`).join('')}
<div style="margin-top:22px;font-family:'Share Tech Mono',monospace;
font-size:0.65rem;color:#3d6070;text-transform:uppercase;
letter-spacing:0.1em">Press ESC or ? to close</div>
`;
overlay.appendChild(box);
document.body.appendChild(overlay);
let open = false;
function toggle() {
open = !open;
overlay.style.display = open ? 'flex' : 'none';
}
on(document, 'keydown', e => {
if (e.key === '?' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName)) toggle();
if (e.key === 'Escape' && open) toggle();
});
on(overlay, 'click', e => { if (e.target === overlay) toggle(); });
}
/* ─── BOOT ORDER ─────────────────────────────────────────────── */
function init() {
injectKeyframes();
initBootSequence();
initParticles();
initCursor();
initHudBar();
initTerminalWidget();
initCornerBrackets();
initCardTilt();
initRipple();
initGlowTrail();
initObserver();
initShortcutHelp();
}
if (document.readyState === 'loading') {
on(document, 'DOMContentLoaded', init);
} else {
init();
}
})();
|
|