with_gvisor. Mips: mihomo's own userspace stack (metacubex/mipstack), always available — pick it when gVisor is not in the core.");
o = s.option(form.Flag, 'disable_quic_gso', _("Disable QUIC GSO"));
o = s.option(form.Flag, 'block_quic', _("Block QUIC"));
o.description = _("Reject proxied UDP 443 so some apps fall back to TCP. Try only when downloads or video stall.");
@@ -1577,7 +1577,9 @@ return view.extend({
o = s.option(form.DynamicList, 'fake_ip_filter', _("Fake-IP Filter Domains"));
o.placeholder = '+.lan / geosite:cn / RULE-SET,cn_domain,real-ip';
- o.description = _("Blocklist mode: enter domains or shorthand like geosite:cn. Rule mode: entries are converted to rule syntax automatically, such as RULE-SET,xxx,real-ip / DOMAIN-SUFFIX,xxx,real-ip; usually add MATCH,fake-ip at the end as fallback. geosite:cn becomes RULE-SET,cn_domain,real-ip via built-in cn.mrs acceleration (other geosite:xxx becomes GEOSITE,xxx,real-ip). Allowlist mode drops the shipped defaults (they mean the opposite there), so enter your own domains.");
+ o.description = _(`Blocklist (default): listed domains get a real IP — +.lan, rule-set:cn_domain, geosite:cn.fake-ip / real-ip action; Clashoo adds real-ip for you.`);
o.depends('enhanced_mode', 'fake-ip');
o.remove = function () {};
o = s.option(form.DynamicList, 'default_nameserver', 'Bootstrap DNS');
@@ -1613,8 +1615,15 @@ return view.extend({
o.value('nameserver', _("Domestic Upstream DNS"));
o.value('direct-nameserver', _("Direct Domain Resolver"));
o.value('proxy-server-nameserver', _("Proxy Node Domain Resolver"));
- o.value('fallback', _("Overseas Encrypted DNS (anti-pollution)"));
+ /* fake-ip 先返回合成地址,fallback 这一角色会被 mixin 跳过,界面上要说清 */
+ var dnsFakeIpMode = (uci.get('clashoo', 'config', 'enhanced_mode') || 'fake-ip') === 'fake-ip';
+ o.value('fallback', dnsFakeIpMode
+ ? _("Overseas Encrypted DNS (no effect in fake-ip mode)")
+ : _("Overseas Encrypted DNS"));
o.default = 'nameserver';
+ o.description = dnsFakeIpMode
+ ? _("Currently fake-ip: this role is skipped, the entry is saved but not used. Switch to redir-host to make it a secondary resolver.")
+ : _("redir-host mode: used as a secondary encrypted resolver.");
o = s.option(form.Value, 'ser_address', _("DNS Address"));
o.placeholder = 'https://dns.alidns.com/dns-query / https://doh.pub/dns-query';
o = s.option(form.ListValue, 'protocol', _("Protocol"));
diff --git a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js
index 056f719a8..0598a7ce8 100644
--- a/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js
+++ b/luci-app-clashoo/htdocs/luci-static/resources/view/clashoo/overview.js
@@ -876,11 +876,23 @@ return view.extend({
_stackLabel: function (stack) {
var s = String(stack || '').toLowerCase();
- var map = { gvisor: 'gVisor', system: 'System', mixed: 'Mixed' };
+ var map = { gvisor: 'gVisor', system: 'System', mixed: 'Mixed', mips: 'Mips' };
return map[s] || (stack ? String(stack) : 'System');
},
_tpModeRows: function (st) {
+ /* eBPF 内核自己接管(cgroup + packet_rewrite),不跑 TUN/Redirect,也不建 tun 设备 */
+ if (st && st.core_is_ebpf) {
+ return {
+ ebpf: true,
+ rows: [
+ { proto: 'TCP', mode: 'eBPF', suffix: _("Mode") },
+ { proto: 'UDP', mode: 'eBPF', suffix: _("Mode") },
+ { proto: _("Network Stack"), mode: _("Core-managed"), suffix: '' }
+ ]
+ };
+ }
+
var tcpRaw = st.tcp_node || st.effective_tcp_mode || st.tcp_mode || uci.get('clashoo', 'config', 'tcp_mode') || 'off';
var udpRaw = st.udp_node || st.effective_udp_mode || st.udp_mode || uci.get('clashoo', 'config', 'udp_mode') || tcpRaw;
var stackRaw = st.stack_type || st.stack || uci.get('clashoo', 'config', 'stack') || 'system';
@@ -898,6 +910,8 @@ return view.extend({
_isTpModeDegraded: function (st) {
if (!st)
return false;
+ if (st.core_is_ebpf)
+ return false;
if (st.runtime_degraded)
return true;
@@ -919,6 +933,8 @@ return view.extend({
});
if (this._isTpModeDegraded(st))
rows.push(E('div', { 'class': 'cl-check-updated cl-mode-degraded' }, _("Degraded")));
+ if (data.ebpf)
+ rows.push(E('div', { 'class': 'cl-check-updated cl-mode-degraded' }, _("eBPF core captures traffic itself; turn off Clashoo forwarding rules")));
return E('div', { id: 'cl-tpmode', 'class': 'cl-mode-stack' }, rows);
},
@@ -1565,10 +1581,14 @@ return view.extend({
var enhancedMode = String(st.enhanced_mode || 'fake-ip').toLowerCase();
var tpMode = 'custom';
- if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'mixed')
+ if (st.core_is_ebpf)
+ tpMode = 'ebpf';
+ else if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'mixed')
tpMode = 'tun-mixed';
else if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'gvisor')
tpMode = 'tun-gvisor';
+ else if (tcpMode === 'tun' && udpMode === 'tun' && stackMode === 'mips')
+ tpMode = 'tun-mips';
else if (tcpMode === 'tun' && udpMode === 'tun')
tpMode = 'tun-system';
else if (tcpMode === 'redirect' && udpMode === 'tproxy')
@@ -1583,7 +1603,9 @@ return view.extend({
return E('option', { value: o[0], selected: o[0] === val ? '' : null, disabled: o[2] ? '' : null }, o[1]);
}));
};
- var tpModeOptions = [['fake-ip','Fake-IP'],['redir-host','Redir-Host'],['tun-system','System'],['tun-gvisor','gVisor'],['tun-mixed','Mixed']];
+ var tpModeOptions = [['fake-ip','Fake-IP'],['redir-host','Redir-Host'],['tun-system','System'],['tun-gvisor','gVisor'],['tun-mixed','Mixed'],['tun-mips','Mips']];
+ if (tpMode === 'ebpf')
+ tpModeOptions.unshift(['ebpf', 'eBPF (' + _("core-managed") + ')', true]);
if (tpMode === 'custom')
tpModeOptions.unshift(['custom',_("Custom")]);
@@ -1649,6 +1671,10 @@ return view.extend({
self._lastSt.tcp_mode = 'tun';
self._lastSt.udp_mode = 'tun';
self._lastSt.stack = 'gvisor';
+ } else if (mode === 'tun-mips') {
+ self._lastSt.tcp_mode = 'tun';
+ self._lastSt.udp_mode = 'tun';
+ self._lastSt.stack = 'mips';
} else if (mode === 'tun-system') {
self._lastSt.tcp_mode = 'tun';
self._lastSt.udp_mode = 'tun';
diff --git a/luci-app-clashoo/po/zh-cn/clashoo.po b/luci-app-clashoo/po/zh-cn/clashoo.po
index e3717393c..44ed21677 100644
--- a/luci-app-clashoo/po/zh-cn/clashoo.po
+++ b/luci-app-clashoo/po/zh-cn/clashoo.po
@@ -213,8 +213,8 @@ msgstr "黑名单"
msgid "Blocklist (listed items use real IP, default)"
msgstr "黑名单(列入走真 IP,默认)"
-msgid "Blocklist mode: enter domains or shorthand like geosite:cn. Rule mode: entries are converted to rule syntax automatically, such as RULE-SET,xxx,real-ip / DOMAIN-SUFFIX,xxx,real-ip; usually add MATCH,fake-ip at the end as fallback. geosite:cn becomes RULE-SET,cn_domain,real-ip via built-in cn.mrs acceleration (other geosite:xxx becomes GEOSITE,xxx,real-ip). Allowlist mode drops the shipped defaults (they mean the opposite there), so enter your own domains."
-msgstr "黑名单模式:填域名或 geosite:cn 简写。规则模式:条目会自动转成 RULE-SET,xxx,real-ip / DOMAIN-SUFFIX,xxx,real-ip 这种与 rules 同语法的写法,末尾通常加一条 MATCH,fake-ip 兜底。geosite:cn 会转成 RULE-SET,cn_domain,real-ip,走内置 cn.mrs 加速(其它 geosite:xxx 则转成 GEOSITE,xxx,real-ip)。白名单模式下随包自带的默认项含义相反,会被跳过,请自行填写需要 Fake-IP 的域名。"
+msgid "Blocklist (default): listed domains get a real IP — +.lan, rule-set:cn_domain, geosite:cn.fake-ip / real-ip action; Clashoo adds real-ip for you."
+msgstr "黑名单(默认):列出的域名返回真实 IP,可写 +.lan、rule-set:cn_domain、geosite:cn。with_gvisor. Mips: mihomo's own userspace stack (metacubex/mipstack), always available — pick it when gVisor is not in the core."
+msgstr "TCP 或 UDP 模式为 TUN 时生效。System:系统协议栈。gVisor / Mixed:用户态栈,内核需要带 with_gvisor 编译。Mips:mihomo 自带用户态栈(metacubex/mipstack),始终可用,内核没有 gVisor 时选它。"
+
+msgid "core-managed"
+msgstr "内核接管"
+
+msgid "Core-managed"
+msgstr "内核接管"
+
+msgid "eBPF core captures traffic itself; turn off Clashoo forwarding rules"
+msgstr "eBPF 内核自行接管流量,请关闭 Clashoo 的转发规则"
diff --git a/luci-app-clashoo/root/usr/share/rpcd/ucode/luci.clashoo b/luci-app-clashoo/root/usr/share/rpcd/ucode/luci.clashoo
index 890504123..e5e158f63 100755
--- a/luci-app-clashoo/root/usr/share/rpcd/ucode/luci.clashoo
+++ b/luci-app-clashoo/root/usr/share/rpcd/ucode/luci.clashoo
@@ -361,6 +361,13 @@ p.close();
return v;
}
+/* eBPF 入站只存在于第三方 mihomo 分支(如 alpha-smart-xxx-ebpf),
+ * 官方 mihomo / smart / sing-box 都没有,这里只做识别。 */
+function core_is_ebpf_build(binary) {
+ let v = get_core_version(binary);
+ return !!match(v, /ebpf/i);
+}
+
function sync_legacy_core_fields(c) {
let family = c.get('clashoo', 'config', 'core_type') || 'mihomo';
let dcore = c.get('clashoo', 'config', 'dcore') || '2';
@@ -2169,6 +2176,7 @@ return { running, pid, dash_port, dash_pass, proxy_mode, conf_path: conf_name, c
runtime_degraded, runtime_degrade_reason,
health_status, health_detail, has_tun_device,
core_type: configured_core, dcore, local_ip,
+ core_is_ebpf: core_is_ebpf_build(find_binary()),
has_smart: !!find_smart_binary() };
} catch (e) {
return { success: false, error: 'status_failed', message: '' + e };
@@ -2259,14 +2267,14 @@ set_mode: {
args: { mode: 'mode' },
call: function(req) {
let mode = req.args?.mode || 'fake-ip';
- if (mode != 'fake-ip' && mode != 'redir-host' && mode != 'tun-system' && mode != 'tun-gvisor' && mode != 'tun-mixed')
+ if (mode != 'fake-ip' && mode != 'redir-host' && mode != 'tun-system' && mode != 'tun-gvisor' && mode != 'tun-mixed' && mode != 'tun-mips')
return { error: 'invalid mode' };
let c = cursor(); c.load('clashoo');
- if (mode == 'tun-system' || mode == 'tun-gvisor' || mode == 'tun-mixed') {
+ if (mode == 'tun-system' || mode == 'tun-gvisor' || mode == 'tun-mixed' || mode == 'tun-mips') {
c.set('clashoo', 'config', 'tun_mode', '1');
c.set('clashoo', 'config', 'tcp_mode', 'tun');
c.set('clashoo', 'config', 'udp_mode', 'tun');
- c.set('clashoo', 'config', 'stack', mode == 'tun-mixed' ? 'mixed' : (mode == 'tun-gvisor' ? 'gvisor' : 'system'));
+ c.set('clashoo', 'config', 'stack', mode == 'tun-mixed' ? 'mixed' : (mode == 'tun-gvisor' ? 'gvisor' : (mode == 'tun-mips' ? 'mips' : 'system')));
c.set('clashoo', 'config', 'enable_udp', '1');
c.set('clashoo', 'config', 'enhanced_mode', 'fake-ip');
} else {
diff --git a/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp.js b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp.js
index 1e98855b8..3da25fb2d 100644
--- a/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp.js
+++ b/luci-app-mptcp/htdocs/luci-static/resources/view/mptcp/mptcp.js
@@ -63,6 +63,10 @@ return L.view.extend({
return kernelMajor > major || (kernelMajor == major && kernelMinor >= minor);
}
+ // x86_64 routers aggregate up to 16 WANs, the others 8: the most the
+ // mptcp init passes to "ip mptcp limits set"
+ var mptcpLimitMax = (boardinfo.release && boardinfo.release.target == 'x86/64') ? 16 : 8;
+
function normalizeSchedulerValue(value) {
if (value == null)
return value;
@@ -320,10 +324,10 @@ return L.view.extend({
o.depends("mptcp_pm_type","1");
o.retain = true;
- // The kernel takes 8 at most for both limits, and one value above
- // it made "ip mptcp limits set" fail for both
+ // One value above the kernel's maximum made "ip mptcp limits set"
+ // fail for both limits
o = s.option(form.Value, "mptcp_subflows", _("Max subflows"),_("specifies the maximum number of additional subflows allowed for each MPTCP connection"));
- o.datatype = "range(0,8)";
+ o.datatype = "range(0," + mptcpLimitMax + ")";
o.rmempty = false;
o.default = 3;
@@ -333,7 +337,7 @@ return L.view.extend({
o.default = 4;
o = s.option(form.Value, "mptcp_add_addr_accepted", _("Max add address"),_("specifies the maximum number of ADD_ADDR (add address) suboptions accepted for each MPTCP connection"));
- o.datatype = "range(0,8)";
+ o.datatype = "range(0," + mptcpLimitMax + ")";
o.rmempty = false;
o.default = 1;
diff --git a/luci-app-mptcp/root/usr/libexec/rpcd/luci.mptcp b/luci-app-mptcp/root/usr/libexec/rpcd/luci.mptcp
index 6b9a944c1..d7110f4a3 100755
--- a/luci-app-mptcp/root/usr/libexec/rpcd/luci.mptcp
+++ b/luci-app-mptcp/root/usr/libexec/rpcd/luci.mptcp
@@ -595,13 +595,20 @@ end
-- What set_settings / set_interface_settings accept for each field. They
-- wrote any string: the init script builds a file path from mptcp_scheduler
-- ("../" in it registered an arbitrary .o as the scheduler), passes the
--- limits to "ip mptcp limits set" (a value above 8 made it fail for both)
--- and the rest to sysctl.
+-- limits to "ip mptcp limits set" (a value above the kernel's maximum made
+-- it fail for both) and the rest to sysctl.
local function valid_uint(v, max)
local n = tonumber(v)
return v:match("^%d+$") ~= nil and n ~= nil and (max == nil or n <= max)
end
+-- x86_64 routers aggregate up to 16 WANs, the others 8: the most the mptcp
+-- init passes to "ip mptcp limits set"
+local function valid_mptcp_limit(v)
+ local max = exec_output("uname -m 2>/dev/null") == "x86_64" and 16 or 8
+ return valid_uint(v, max)
+end
+
-- 1 to 256, as "ip route" takes it and the forms ask: the BPF schedulers
-- read 0 as no weight (neutral 100) and add the weight to a signed counter
local function valid_weight(v)
@@ -671,9 +678,9 @@ local setting_checks = {
mptcp_disable_initial_config = function(v) return v == "0" or v == "1" end,
mptcp_force_multipath = function(v) return v == "0" or v == "1" end,
mptcpd_enable = function(v) return v == "enable" or v == "disable" end,
- mptcp_subflows = function(v) return valid_uint(v, 8) end,
+ mptcp_subflows = valid_mptcp_limit,
mptcp_stale_loss_cnt = valid_uint,
- mptcp_add_addr_accepted = function(v) return valid_uint(v, 8) end,
+ mptcp_add_addr_accepted = valid_mptcp_limit,
mptcp_add_addr_timeout = valid_uint,
mptcp_blackhole_timeout = valid_uint,
mptcp_close_timeout = valid_uint,
diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile
index 146c80e71..523c28df9 100644
--- a/luci-app-open-box/Makefile
+++ b/luci-app-open-box/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
-PKG_VERSION:=0.1.397
-PKG_RELEASE:=62
+PKG_VERSION:=0.1.400
+PKG_RELEASE:=63
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
diff --git a/luci-theme-aurora/.dev/src/media/login.css b/luci-theme-aurora/.dev/src/media/login.css
index df9148199..9765c9c00 100644
--- a/luci-theme-aurora/.dev/src/media/login.css
+++ b/luci-theme-aurora/.dev/src/media/login.css
@@ -159,6 +159,10 @@ body[data-bg] .login-card {
.login-error {
@apply bg-danger-surface text-danger border-hairline rounded-2xl border px-3 py-2 text-center text-sm;
+
+ &.warning {
+ @apply bg-warning-surface text-warning;
+ }
}
.login-actions {
diff --git a/luci-theme-aurora/.dev/tests/build-performance.test.js b/luci-theme-aurora/.dev/tests/build-performance.test.js
index b254a50e7..210ea62e2 100644
--- a/luci-theme-aurora/.dev/tests/build-performance.test.js
+++ b/luci-theme-aurora/.dev/tests/build-performance.test.js
@@ -19,7 +19,8 @@ test("production assets stay within raw-transfer budgets", () => {
// 187K: pseudo-elements are nested rules, one content declaration each.
assert.ok(main <= 187_000, "main.css exceeds 187 KB");
- assert.ok(login <= 12_000, "login.css exceeds 12 KB");
+ // 12.5K: auth_plugin warning tokens added ~0.35 KB.
+ assert.ok(login <= 12_500, "login.css exceeds 12.5 KB");
// 22K: palette recents (record on pick, pure-LRU browse order, storage
// validation) added ~0.8 KB and the ">" logout command ~0.2 KB. The total
// budget below moved by the same amount.
diff --git a/luci-theme-aurora/Makefile b/luci-theme-aurora/Makefile
index 68f42bfef..92854e9a6 100644
--- a/luci-theme-aurora/Makefile
+++ b/luci-theme-aurora/Makefile
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
LUCI_DEPENDS:=+luci-base
-PKG_VERSION:=1.4.2
-PKG_RELEASE:=86
+PKG_VERSION:=1.4.3
+PKG_RELEASE:=87
PKG_LICENSE:=Apache-2.0
LUCI_MINIFY_CSS:=
diff --git a/luci-theme-aurora/htdocs/luci-static/aurora/login.css b/luci-theme-aurora/htdocs/luci-static/aurora/login.css
index b8a2a3b3d..e5ade23c7 100644
--- a/luci-theme-aurora/htdocs/luci-static/aurora/login.css
+++ b/luci-theme-aurora/htdocs/luci-static/aurora/login.css
@@ -1,2 +1,2 @@
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
-@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content:"";--tw-shadow:0 0 #0000;--tw-inset-shadow:0 0 #0000;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base) * 2);--animate-spin:spin 1s linear infinite}}@layer base{*,:before,:after{box-sizing:border-box}body,hr,p,button,input{margin:0}button,input{font:inherit}a{text-decoration-line:none}[hidden]{display:none!important}}@layer components,utilities;:root{--bg:#f7fafc;--surface:#fff;--text:#121a22;--brand:#0085b7;--on-brand:#fff;--danger:#8d1925;--text-muted:#5f666d;--text-subtle:#7f858b;--hairline:#121a2221;--brand-subtle:#ddecf5;--danger-surface:#ffe8e6;--control-bg:#fff;--app-shadow-md:0 4px 16px #00000014, 0 1px 3px #0000000a;--font-sans:"Lato", ui-sans-serif, system-ui, sans-serif;--spacing:.25rem;--radius-base:.5rem}@supports (color:lab(0% 0 0)){:root{--bg:lab(98.1553% -.760227 -1.25223);--surface:lab(100% 0 0);--text:lab(8.89727% -1.68239 -6.93074);--brand:lab(51.3503% -19.237 -40.2116);--on-brand:lab(100% 0 0);--danger:lab(31.1495% 47.9256 25.8661);--text-muted:lab(42.8254% -1.57365 -4.8763);--text-subtle:lab(55.3248% -1.4126 -4.053);--hairline:lab(8.89727% -1.68239 -6.93074/.13);--brand-subtle:lab(92.5794% -3.9458 -6.17858);--danger-surface:lab(93.849% 11.2841 5.29771);--control-bg:lab(100.011% -.760317 -1.25225);--app-shadow-md:0 4px 16px lab(0% 0 0/.08), 0 1px 3px lab(0% 0 0/.04)}}[data-darkmode=true]{--bg:#05070e;--surface:#141822;--text:#f9fafb;--brand:#00958e;--on-brand:#fff;--danger:#f17070;--text-muted:#909297;--text-subtle:#5d6067;--hairline:#f9fafb1a;--brand-subtle:#061a20;--danger-surface:#541f20;--control-bg:#0a0e17;--app-shadow-md:0 10px 28px #0000006b}@supports (color:lab(0% 0 0)){[data-darkmode=true]{--bg:lab(1.93894% .0855997 -3.22397);--surface:lab(8.24713% -.0405461 -7.31381);--text:lab(98.252% -.0618994 -.756907);--brand:lab(55.0978% -44.5567 -7.62978);--on-brand:lab(100% 0 0);--danger:lab(63.5705% 50.8105 25.3639);--text-muted:lab(60.5379% -.225931 -3.05105);--text-subtle:lab(40.6865% -.278354 -4.23647);--hairline:lab(98.252% -.0618994 -.756907/.1);--brand-subtle:lab(7.95734% -6.34954 -6.72844);--danger-surface:lab(20.3014% 25.4342 12.827);--control-bg:lab(3.9802% .0812709 -5.64623);--app-shadow-md:0 10px 28px lab(0% 0 0/.42)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-property:none!important;animation:none!important}.spinning:before{animation:var(--animate-spin)!important}}html{background-color:var(--bg);min-height:100dvh;font-family:var(--font-sans);--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}html[data-darkmode=true]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}html body{background-color:var(--bg);min-height:100dvh}.page-bg{pointer-events:none;background-image:var(--page-bg-lqip,var(--page-bg));background-position:50%;background-repeat:no-repeat;background-size:cover;position:fixed;inset:0;transform:translateZ(0)}.page-bg:after{pointer-events:none;background-image:var(--page-bg);opacity:0;--tw-content:"";content:var(--tw-content);background-position:50%;background-repeat:no-repeat;background-size:cover;transition:opacity .5s;position:absolute;inset:0}.page-bg.full-loaded:after{opacity:1}@keyframes divider-in{0%{opacity:0;transform:scaleX(.15)}}.login-screen{min-height:100dvh;padding-inline:calc(var(--spacing) * 5);padding-bottom:calc(var(--spacing) * 14);flex-direction:column;justify-content:center;align-items:center;display:flex;position:relative}.login-shell{width:100%;max-width:var(--container-md)}.page-bg{--page-bg:var(--login-bg);--page-bg-lqip:var(--login-bg-lqip)}body[data-bg] .page-bg:before{pointer-events:none;z-index:1;--tw-content:"";content:var(--tw-content);background:var(--bg);position:absolute;inset:0}@supports (color:color-mix(in lab, red, red)){body[data-bg] .page-bg:before{background:color-mix(in srgb,var(--bg) var(--login-bg-scrim,0%),transparent)}}body[data-bg] .login-card{-webkit-backdrop-filter:blur(var(--login-bg-blur,0px)) saturate(150%);backdrop-filter:blur(var(--login-bg-blur,0px)) saturate(150%);background:var(--surface)}@supports (color:color-mix(in lab, red, red)){body[data-bg] .login-card{background:color-mix(in srgb,var(--surface) var(--login-bg-alpha,100%),transparent)}}@media (prefers-reduced-transparency:reduce){body[data-bg] .login-card{--login-bg-alpha:100%;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}}.login-card{gap:calc(var(--spacing) * 7);border-radius:calc(var(--radius-base) * 4);background-color:var(--surface);padding:calc(var(--spacing) * 14);--tw-shadow:var(--app-shadow-md);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);flex-direction:column;display:flex}@media not all and (width>=48rem){.login-card{border-radius:calc(var(--radius-base) * 3);padding:calc(var(--spacing) * 9)}}.login-brand{align-items:center;gap:calc(var(--spacing) * 3.5);display:flex}.login-divider{border-style:var(--tw-border-style);--tw-gradient-position:to right;border-width:0;width:100%;height:1px;animation:.7s cubic-bezier(.16,1,.3,1) .1s both divider-in}@supports (background-image:linear-gradient(in lab, red, red)){.login-divider{--tw-gradient-position:to right in oklab}}.login-divider{background-image:linear-gradient(var(--tw-gradient-stops));--tw-gradient-from:transparent;--tw-gradient-via:var(--hairline);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));--tw-gradient-to:transparent}.login-logo{height:calc(var(--spacing) * 11);width:calc(var(--spacing) * 11);border-radius:calc(var(--radius-base) * 2);background-color:var(--brand-subtle);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--brand-subtle);flex-shrink:0;justify-content:center;align-items:center;display:flex}.login-logo img{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6)}.login-title{gap:var(--spacing);flex-direction:column;display:flex}.login-hostname{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-tight);color:var(--text)}.login-subtitle{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--text)}.login-form{gap:calc(var(--spacing) * 5);flex-direction:column;display:flex}.login-fields{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.login-field{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.login-label{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));font-weight:var(--font-weight-semibold);letter-spacing:var(--tracking-widest);color:var(--text);text-transform:uppercase}.input-wrap{border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline);background-color:var(--control-bg);contain:layout style;position:relative}.input-wrap:after{pointer-events:none;border-radius:calc(var(--radius-2xl) + 1px);border-style:var(--tw-border-style);border-width:2px;border-color:var(--brand);opacity:0;--tw-content:"";content:var(--tw-content);transition:opacity .15s;position:absolute;inset:-1px}.input-wrap:focus-within:after{opacity:1}.login-input{appearance:none;border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));color:var(--text);background-color:#0000;border-width:0;outline-style:none}.login-input::placeholder{color:var(--text-subtle)}.login-error{border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:var(--hairline);background-color:var(--danger-surface);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--danger)}.login-actions{padding-top:calc(var(--spacing) * 4)}.login-submit{cursor:pointer;border-radius:calc(var(--radius-base) * 2);border-style:var(--tw-border-style);background-color:var(--brand);width:100%;padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));font-weight:var(--font-weight-medium);color:var(--on-brand);border-width:0}@media (hover:hover){.login-submit:hover{opacity:.9}}.login-submit:active{opacity:.8}.login-footer{right:0;bottom:calc(var(--spacing) * 5);padding-inline:calc(var(--spacing) * 5);text-align:center;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--text-muted);position:absolute;left:0}.login-footer p{white-space:normal}.login-footer a{color:var(--brand)}@media (hover:hover){.login-footer a:hover{text-decoration-line:underline}}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"