<!DOCTYPE html>
<html lang="{{ lang }}" dir="{{ dir }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<style>
:root {
--bg: #0e0e1e;
--fg: #d4d4e8;
--card-bg: rgba(255, 255, 255, 0.04);
--card-border: rgba(255, 255, 255, 0.08);
--heading: #ffffff;
--tagline: rgba(212, 212, 232, 0.6);
--label: rgba(212, 212, 232, 0.5);
--value: #d4d4e8;
--divider: rgba(255, 255, 255, 0.05);
--retry: rgba(212, 212, 232, 0.4);
--footer: rgba(212, 212, 232, 0.25);
--footer-hover: rgba(212, 212, 232, 0.55);
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f5f5f0;
--fg: #333340;
--card-bg: rgba(255, 255, 255, 0.8);
--card-border: rgba(0, 0, 0, 0.08);
--heading: #1a1a2e;
--tagline: rgba(30, 30, 50, 0.5);
--label: rgba(30, 30, 50, 0.45);
--value: #333340;
--divider: rgba(0, 0, 0, 0.05);
--retry: rgba(30, 30, 50, 0.35);
--footer: rgba(30, 30, 50, 0.2);
--footer-hover: rgba(30, 30, 50, 0.5);
}
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
line-height: 1.6;
padding: 2rem;
}
.card {
max-width: 480px;
width: 100%;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 2.5rem 2rem;
text-align: center;
backdrop-filter: blur(12px);
animation: accordionOpen 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes accordionOpen {
0% {
clip-path: inset(32% 0 42% 0);
opacity: 0.6;
}
100% {
clip-path: inset(0 0 0 0);
opacity: 1;
}
}
.logo {
width: 64px;
height: 64px;
margin: 0 auto 1rem;
opacity: 0.8;
display: block;
}
h1 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--heading);
}
.tagline {
font-size: 0.875rem;
color: var(--tagline);
margin-bottom: 1.5rem;
}
.status {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 20px;
border-radius: 9999px;
font-size: 0.8125rem;
font-weight: 600;
margin-bottom: 1.25rem;
text-align: center;
}
.status--ready {
background: rgba(46, 213, 115, 0.12);
color: #2ed573;
}
.status--working {
background: rgba(255, 165, 0, 0.12);
color: #ffa500;
animation: pulse 2s ease-in-out infinite;
}
.status--landing {
background: rgba(255, 165, 0, 0.12);
color: #ffa500;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.info-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--divider);
font-size: 0.8125rem;
gap: 12px;
}
.info-row:last-of-type { border-bottom: none; }
.info-label {
color: var(--label);
flex-shrink: 0;
min-width: 70px;
text-align: left;
}
.info-value {
color: var(--value);
font-weight: 500;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
text-align: right;
word-break: break-all;
}
.watch-list {
display: flex;
flex-direction: column;
gap: 4px;
text-align: right;
}
.watch-item {
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.75rem;
color: var(--value);
max-width: 260px;
position: relative;
cursor: pointer;
}
.watch-text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.watch-item:hover .tooltip {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(-4px);
}
.tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(0);
background: var(--heading);
color: var(--bg);
padding: 8px 14px;
border-radius: 8px;
font-size: 0.7rem;
white-space: normal;
word-break: break-all;
min-width: 280px;
max-width: 420px;
z-index: 10;
pointer-events: auto;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
line-height: 1.6;
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease, transform 0.15s ease;
}
.tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: var(--heading);
}
.tooltip-path {
display: block;
word-break: break-all;
line-height: 1.5;
}
.tooltip-hint {
display: block;
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid rgba(255,255,255,0.15);
font-style: italic;
font-size: 0.65rem;
opacity: 0.7;
cursor: pointer;
}
.toast {
position: fixed;
top: -50px;
left: 50%;
transform: translateX(-50%);
background: var(--heading);
color: var(--bg);
padding: 10px 20px;
border-radius: 8px;
font-size: 0.8rem;
font-weight: 500;
z-index: 99999;
transition: top 0.3s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
pointer-events: none;
white-space: nowrap;
}
.toast.show {
top: 20px;
}
.portal-tooltip {
position: fixed;
background: var(--heading, #fff);
color: var(--bg, #000);
padding: 8px 14px;
border-radius: 8px;
font-size: 0.7rem;
line-height: 1.5;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
text-align: center;
max-width: min(420px, 80vw);
word-break: break-all;
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
pointer-events: auto;
z-index: 999999;
}
.portal-tooltip .tooltip-hint {
display: block;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid rgba(255,255,255,0.15);
font-style: italic;
font-size: 0.65rem;
opacity: 0.8;
cursor: pointer;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.retry-hint {
margin-top: 1.25rem;
font-size: 0.75rem;
color: var(--retry);
line-height: 1.8;
}
.cancel-row {
margin-top: 0.5rem;
text-align: center;
}
.countdown {
font-weight: 700;
font-size: 1.5em;
color: var(--countdown-color, #2ed573);
margin: 0 8px;
}
.countdown-unit {
font-size: 0.75rem;
font-weight: 400;
color: var(--retry);
margin-left: 2px;
margin-right: 2px;
}
.cancel-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border: 1px solid var(--card-border);
border-radius: 6px;
background: var(--card-bg);
color: var(--label);
font-size: 0.6875rem;
cursor: pointer;
transition: border-color 0.2s ease, color 0.2s ease;
font-family: inherit;
vertical-align: middle;
}
.cancel-btn:hover {
border-color: var(--divider);
color: var(--fg);
}
.cancel-btn--refresh {
color: var(--retry);
border-color: rgba(255, 165, 0, 0.25);
}
.cancel-btn--refresh:hover {
background: rgba(255, 165, 0, 0.08);
border-color: rgba(255, 165, 0, 0.45);
color: #ffa500;
}
.footer {
margin-top: 1.5rem;
font-size: 0.6875rem;
color: var(--footer);
}
.footer a {
color: inherit;
text-decoration: underline;
text-decoration-color: var(--divider);
text-underline-offset: 3px;
transition: color 0.2s ease, text-decoration-color 0.2s ease;
}
.footer a:hover {
color: var(--footer-hover);
text-decoration-color: var(--footer-hover);
}
.version-line {
margin-top: 0.5rem;
font-size: 0.6875rem;
color: var(--footer);
text-align: center;
}
.binaries {
margin-top: 1rem;
text-align: left;
}
.binaries-title {
font-size: 0.75rem;
color: var(--label);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.binary-row {
display: grid;
grid-template-columns: minmax(70px, auto) 1fr;
gap: 8px 12px;
padding: 6px 0;
border-bottom: 1px solid var(--divider);
font-size: 0.75rem;
align-items: baseline;
}
.binary-row:last-child { border-bottom: none; }
.binary-name {
font-weight: 600;
color: var(--value);
text-align: left;
cursor: pointer;
position: relative;
}
.binary-name-full,
.binary-time-full,
.binary-hash-full {
display: none !important;
}
.watch-item > .tooltip {
display: none !important;
}
.binary-detail {
color: var(--label);
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
font-size: 0.6875rem;
text-align: right;
}
.binary-time {
cursor: pointer;
}
.binary-hash {
color: rgba(46, 213, 115, 0.7);
cursor: pointer;
position: relative;
}
.binary-hash-short {
display: inline;
}
</style>
</head>
<body>
<div class="card">
{% if logo_base64 %}
<img class="logo" src="data:image/webp;base64,{{ logo_base64 }}" alt="Malkuth" />
{% endif %}
<h1>{{ heading }}</h1>
<p class="tagline">{{ tagline }}</p>
<div class="status {% if landing %}status--landing{% elif ready %}status--ready{% else %}status--working{% endif %}">
{{ status_text }}
</div>
<div class="info-row">
<span class="info-label">{{ task_label }}</span>
<span class="info-value">{{ task }}</span>
</div>
{% if proxy_endpoint %}
<div class="info-row">
<span class="info-label">{{ proxy_label }}</span>
<span class="info-value">{{ proxy_endpoint }}</span>
</div>
{% endif %}
{% if watch_paths %}
<div class="info-row">
<span class="info-label">{{ watch_label }}</span>
<div class="watch-list">
{% for p in watch_paths %}
<span class="watch-item" data-path="{{ p }}"><span class="watch-text">{{ p }}</span><span class="tooltip"><span class="tooltip-path">{{ p }}</span><span class="tooltip-hint" data-copy-hint="{{ copy_hint }}"></span></span></span>
{% endfor %}
</div>
</div>
{% endif %}
{% if landing and binaries %}
<div class="binaries">
<div class="binaries-title">{{ binaries_title }}</div>
{% for b in binaries %}
<div class="binary-row">
<span class="binary-name" data-copy="{{ b.name }}"><span>{{ b.name }}</span><span class="binary-name-full">{{ b.name }}<br><span class="tooltip-hint" data-copy-hint="{{ copy_hint }}"></span></span></span>
<span class="binary-detail"><span class="binary-time" data-copy="{{ b.compile_time }}">{{ b.compile_time }}<span class="binary-time-full">{{ b.compile_time }}<br><span class="tooltip-hint" data-copy-hint="{{ copy_hint }}"></span></span></span> · <span class="binary-hash" data-copy="{{ b.hash }}"><span class="binary-hash-short">{{ b.hash_short }}</span><span class="binary-hash-full">{{ b.hash }}<br><span class="tooltip-hint" data-copy-hint="{{ copy_hint }}"></span></span></span></span>
</div>
{% endfor %}
</div>
{% endif %}
<p class="retry-hint" id="retryHint">{% if landing %}{{ redirect_before }}{% else %}{{ retry_before }}{% endif %}<span class="countdown" id="countdown" style="--countdown-color: {% if ready %}#2ed573{% else %}#ffa500{% endif %}">{% if landing %}3{% else %}10{% endif %}</span><span class="countdown-unit" id="countdownUnit">{% if landing %}{{ redirect_after }}{% else %} {{ retry_unit }}{{ retry_after }}{% endif %}</span></p>
<div class="cancel-row"><button class="cancel-btn" id="cancelBtn">{{ cancel_label }}</button></div>
<p class="footer">{{ footer_prefix }}<a href="https://github.com/celestia-island/malkuth" target="_blank" rel="noopener">Malkuth</a>{{ footer_suffix }}</p>
{% if install_method %}
<p class="version-line">v{{ version }} ({{ install_label }} {{ install_method }})</p>
{% else %}
<p class="version-line">v{{ version }}</p>
{% endif %}
</div>
<script>
(function(){
var start = {% if landing %}3{% else %}10{% endif %};
var s = start;
var e = document.getElementById("countdown");
var u = document.getElementById("countdownUnit");
var h = document.getElementById("retryHint");
var btn = document.getElementById("cancelBtn");
var timer = null;
var cancelled = false;
var card = document.querySelector(".card");
if (card) {
card.addEventListener("animationend", function() {
card.style.clipPath = "none";
card.style.overflow = "visible";
}, { once: true });
}
function tick() {
if (cancelled) return;
if (s <= 0) {
location.reload();
return;
}
e.textContent = s;
s--;
timer = setTimeout(tick, 1000);
}
function cancelCountdown() {
if (cancelled) {
location.reload();
return;
}
cancelled = true;
clearTimeout(timer);
e.textContent = "\u2014";
u.textContent = "";
h.textContent = "{{ retry_manual }}";
btn.textContent = "{{ refresh_label }}";
btn.classList.add("cancel-btn--refresh");
}
if (btn) {
btn.onclick = cancelCountdown;
}
tick();
var portal = null;
function getPortal() {
if (!portal) { portal = document.createElement("div"); document.body.appendChild(portal); }
return portal;
}
function showPortalTooltip(target, contentHTML) {
hidePortalTooltip();
var p = getPortal();
var rect = target.getBoundingClientRect();
var tip = document.createElement("div");
tip.className = "portal-tooltip";
tip.innerHTML = contentHTML;
var hint = tip.querySelector(".tooltip-hint");
if (hint) {
hint.style.cursor = "pointer";
hint.addEventListener("click", function(ev) { ev.stopPropagation(); doCopy(target.getAttribute("data-copy") || ""); });
}
p.appendChild(tip);
var tw = tip.offsetWidth;
var th = tip.offsetHeight;
var left = rect.left + rect.width / 2 - tw / 2;
if (left < 8) left = 8;
if (left + tw > window.innerWidth - 8) left = window.innerWidth - tw - 8;
tip.style.left = Math.max(8, left) + "px";
tip.style.top = (rect.top + window.scrollY - th - 8) + "px";
var arr = document.createElement("span");
arr.cssText = "position:absolute;bottom:-6px;left:50%;transform:translateX(-50%);border:6px solid transparent;border-top-color:" + getComputedStyle(tip).backgroundColor;
tip.appendChild(arr);
var lt = null;
function defer() { lt = setTimeout(hidePortalTooltip, 150); }
function cancel() { clearTimeout(lt); }
target.addEventListener("mouseleave", defer, { once: true });
tip.addEventListener("mouseenter", cancel);
tip.addEventListener("mouseleave", defer);
}
function hidePortalTooltip() { var p = getPortal(); while (p.lastChild) p.removeChild(p.lastChild); }
[".binary-name", ".binary-time", ".binary-hash"].forEach(function(sel) {
document.querySelectorAll(sel).forEach(function(el) {
var full = el.querySelector("[class$='-full']");
if (!full) return;
el.addEventListener("mouseenter", function(ev) { showPortalTooltip(el, full.innerHTML); ev.stopPropagation(); });
});
});
document.querySelectorAll(".watch-item").forEach(function(el) {
var tt = el.querySelector(".tooltip");
if (!tt) return;
el.addEventListener("mouseenter", function(ev) {
showPortalTooltip(el, tt.querySelector(".tooltip-path").outerHTML + "<br>" + tt.querySelector(".tooltip-hint").outerHTML);
ev.stopPropagation();
});
});
document.querySelectorAll(".tooltip-hint").forEach(function(el) {
el.textContent = el.getAttribute("data-copy-hint") || "Click to copy";
});
function doCopy(text) {
var ok = false;
if (window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() { ok = true; toast("{{ copied_msg }}"); }, function() { fallback(); });
} else { fallback(); }
function fallback() {
var ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", "");
ta.style.cssText = "position:fixed;left:-9999px;top:-9999px";
document.body.appendChild(ta); ta.focus(); ta.select();
try { ok = document.execCommand("copy"); } catch(e) {}
document.body.removeChild(ta);
toast(ok ? "{{ copied_msg }}" : "{{ copy_fail_msg }}");
}
}
[".watch-item", ".binary-name", ".binary-time", ".binary-hash"].forEach(function(sel) {
document.querySelectorAll(sel).forEach(function(el) {
el.addEventListener("click", function(ev) { ev.preventDefault(); doCopy(el.getAttribute("data-copy") || ""); });
});
});
function toast(msg) {
var t = document.getElementById("globalToast");
if (!t) { t = document.createElement("div"); t.id = "globalToast"; t.className = "toast"; document.body.appendChild(t); }
t.textContent = msg; t.classList.remove("show"); void t.offsetWidth; t.classList.add("show");
clearTimeout(t._timer); t._timer = setTimeout(function() { t.classList.remove("show"); }, 2000);
}
})();
</script>
</body>
</html>