diff --git a/luci-app-mptcp/luasrc/model/cbi/mptcp.lua b/luci-app-mptcp/luasrc/model/cbi/mptcp.lua
index efa878923..a36c3d391 100755
--- a/luci-app-mptcp/luasrc/model/cbi/mptcp.lua
+++ b/luci-app-mptcp/luasrc/model/cbi/mptcp.lua
@@ -75,6 +75,12 @@ if uname.release:sub(1,4) == "5.15" or uname.release:sub(1,1) == "6" then
o.datatype = "uinteger"
o.rmempty = false
o.default = 4
+
+ o = s:option(Value, "mptcp_add_addr_timeout", translate("Set the timeout after which an ADD_ADDR control message will be resent to an MPTCP peer that has not acknowledged a previous ADD_ADDR message."))
+ o.datatype = "uinteger"
+ o.rmempty = false
+ o.default = 120
+
else
o = s:option(Value, "mptcp_fullmesh_num_subflows", translate("Fullmesh subflows for each pair of IP addresses"))
o.datatype = "uinteger"
diff --git a/luci-app-zerotier/luasrc/controller/zerotier.lua b/luci-app-zerotier/luasrc/controller/zerotier.lua
new file mode 100755
index 000000000..f2973c65e
--- /dev/null
+++ b/luci-app-zerotier/luasrc/controller/zerotier.lua
@@ -0,0 +1,24 @@
+module("luci.controller.zerotier", package.seeall)
+
+function index()
+ if not nixio.fs.access("/etc/config/zerotier") then
+ return
+ end
+
+ entry({"admin", "vpn"}, firstchild(), "VPN", 45).dependent = false
+
+ entry({"admin", "vpn", "zerotier"}, alias("admin", "vpn", "zerotier", "general"), _("ZeroTier"), 99)
+
+ entry({"admin", "vpn", "zerotier", "general"}, cbi("zerotier/settings"), _("Base Setting"), 1)
+ entry({"admin", "vpn", "zerotier", "log"}, form("zerotier/info"), _("Interface Info"), 2)
+ entry({"admin", "vpn", "zerotier", "manual"}, cbi("zerotier/manual"), _("Manual Config"), 3)
+
+ entry({"admin", "vpn", "zerotier", "status"}, call("act_status"))
+end
+
+function act_status()
+ local e = {}
+ e.running = luci.sys.call("pgrep /usr/bin/zerotier-one >/dev/null") == 0
+ luci.http.prepare_content("application/json")
+ luci.http.write_json(e)
+end
diff --git a/luci-app-zerotier/luasrc/model/cbi/zerotier/info.lua b/luci-app-zerotier/luasrc/model/cbi/zerotier/info.lua
new file mode 100755
index 000000000..bb8fc3769
--- /dev/null
+++ b/luci-app-zerotier/luasrc/model/cbi/zerotier/info.lua
@@ -0,0 +1,15 @@
+local fs = require "nixio.fs"
+local conffile = "/tmp/zero.info"
+
+f = SimpleForm("logview")
+
+t = f:field(TextValue, "conf")
+t.rmempty = true
+t.rows = 19
+function t.cfgvalue()
+ luci.sys.exec("for i in $(ifconfig | grep 'zt' | awk '{print $1}'); do ifconfig $i; done > /tmp/zero.info")
+ return fs.readfile(conffile) or ""
+end
+t.readonly = "readonly"
+
+return f
diff --git a/luci-app-zerotier/luasrc/model/cbi/zerotier/manual.lua b/luci-app-zerotier/luasrc/model/cbi/zerotier/manual.lua
new file mode 100755
index 000000000..71ae2bb37
--- /dev/null
+++ b/luci-app-zerotier/luasrc/model/cbi/zerotier/manual.lua
@@ -0,0 +1,26 @@
+local m, s, o
+local fs = require "nixio.fs"
+local jsonc = require "luci.jsonc" or nil
+m = Map("zerotier")
+s = m:section(NamedSection, "sample_config", "zerotier")
+s.anonymous = true
+s.addremove = false
+o = s:option(TextValue, "manualconfig")
+o.rows = 20
+o.wrap = "soft"
+o.rmempty = true
+o.cfgvalue = function(self, section)
+ return fs.readfile("/etc/config/zero/local.conf")
+end
+o.write = function(self, section, value)
+ fs.writefile("/etc/config/zero/local.conf", value:gsub("\r\n", "\n"))
+end
+o.validate = function(self, value)
+ if jsonc == nil or jsonc.parse(value) ~= nil then
+ return value
+ end
+ return nil
+end
+o.description =
+ 'https://www.zerotier.com/manual/
https://github.com/zerotier/ZeroTierOne/blob/dev/service/README.md'
+return m
diff --git a/luci-app-zerotier/luasrc/model/cbi/zerotier/settings.lua b/luci-app-zerotier/luasrc/model/cbi/zerotier/settings.lua
new file mode 100755
index 000000000..fc70a8581
--- /dev/null
+++ b/luci-app-zerotier/luasrc/model/cbi/zerotier/settings.lua
@@ -0,0 +1,37 @@
+a = Map("zerotier")
+a.title = translate("ZeroTier")
+a.description = translate("Zerotier is an open source, cross-platform and easy to use virtual LAN")
+
+a:section(SimpleSection).template = "zerotier/zerotier_status"
+
+t = a:section(NamedSection, "sample_config", "zerotier")
+t.anonymous = true
+t.addremove = false
+
+e = t:option(Flag, "enabled", translate("Enable"))
+e.default = 0
+e.rmempty = false
+
+e = t:option(DynamicList, "join", translate('ZeroTier Network ID'))
+e.password = true
+e.rmempty = false
+
+e = t:option(Flag, "nat", translate("Auto NAT Clients"))
+e.description = translate("Allow zerotier clients access your LAN network")
+e.default = 0
+e.rmempty = false
+
+e = t:option(MultiValue, "access", translate("Zerotier Access Control"))
+e.default = "lanfwzt ztfwwan ztfwlan"
+e.rmempty = false
+e:value("lanfwzt", translate("LAN Access Zerotier"))
+e:value("wanfwzt", translate("WAN Access Zerotier"))
+e:value("ztfwwan", translate("Remote Access WAN"))
+e:value("ztfwlan", translate("Remote Access LAN"))
+e.widget = "checkbox"
+
+e = t:option(DummyValue, "opennewwindow", translate(
+ ""))
+e.description = translate("Create or manage your zerotier network, and auth clients who could access")
+
+return a
diff --git a/luci-app-zerotier/luasrc/view/zerotier/zerotier_status.htm b/luci-app-zerotier/luasrc/view/zerotier/zerotier_status.htm
new file mode 100755
index 000000000..b2faf8e44
--- /dev/null
+++ b/luci-app-zerotier/luasrc/view/zerotier/zerotier_status.htm
@@ -0,0 +1,29 @@
+
+
+
\ No newline at end of file
diff --git a/luci-app-zerotier/po/zh-cn/zerotier.po b/luci-app-zerotier/po/zh-cn/zerotier.po
new file mode 100755
index 000000000..07adfeabc
--- /dev/null
+++ b/luci-app-zerotier/po/zh-cn/zerotier.po
@@ -0,0 +1,35 @@
+msgid "Zerotier is an open source, cross-platform and easy to use virtual LAN"
+msgstr "Zerotier 是一个开源,跨平台,而且适合内网穿透互联的傻瓜配置虚拟 VPN LAN"
+
+msgid "Auto NAT Clients"
+msgstr "自动允许客户端 NAT"
+
+msgid "Allow zerotier clients access your LAN network"
+msgstr "允许 Zerotier 的拨入客户端访问路由器 LAN 资源(需要在 Zerotier 管理页面设定到 LAN 网段的路由表)"
+
+msgid "Create or manage your zerotier network, and auth clients who could access"
+msgstr "点击跳转到 Zerotier 官网管理平台,新建或者管理网络,并允许客户端接入访问你私人网路(新接入的节点默认不允许访问)"
+
+msgid "Base Setting"
+msgstr "基本设置"
+
+msgid "Interface Info"
+msgstr "接口信息"
+
+msgid "Zerotier Access Control"
+msgstr "Zerotier 准入控制"
+
+msgid "LAN Access Zerotier"
+msgstr "LAN 可接入 Zerotier"
+
+msgid "WAN Access Zerotier"
+msgstr "WAN 可接入 Zerotier"
+
+msgid "Remote Access WAN"
+msgstr "外部访问可接入 WAN"
+
+msgid "Remote Access LAN"
+msgstr "外部访问可接入 LAN"
+
+msgid "Manual Config"
+msgstr "手动设置"
diff --git a/luci-app-zerotier/po/zh_Hans b/luci-app-zerotier/po/zh_Hans
new file mode 100755
index 000000000..41451e4a1
--- /dev/null
+++ b/luci-app-zerotier/po/zh_Hans
@@ -0,0 +1 @@
+zh-cn
\ No newline at end of file
diff --git a/luci-app-zerotier/root/etc/init.d/zerotier b/luci-app-zerotier/root/etc/init.d/zerotier
new file mode 100755
index 000000000..666d67533
--- /dev/null
+++ b/luci-app-zerotier/root/etc/init.d/zerotier
@@ -0,0 +1,113 @@
+#!/bin/sh /etc/rc.common
+
+START=99
+
+USE_PROCD=1
+
+PROG=/usr/bin/zerotier-one
+CONFIG_PATH=/var/lib/zerotier-one
+
+service_triggers() {
+ procd_add_reload_trigger "zerotier"
+ procd_add_interface_trigger "interface.*.up" wan /etc/init.d/zerotier restart
+}
+
+section_enabled() {
+ config_get_bool enabled "$1" 'enabled' 0
+ [ $enabled -gt 0 ]
+}
+
+start_instance() {
+ local cfg="$1"
+ local port secret config_path
+ local ARGS=""
+
+ if ! section_enabled "$cfg"; then
+ echo "disabled in config"
+ return 1
+ fi
+
+ [ -d /etc/config/zero ] || mkdir -p /etc/config/zero
+ config_path=/etc/config/zero
+
+ config_get_bool port $cfg 'port'
+ config_get secret $cfg 'secret'
+
+ # Remove existing link or folder
+ rm -rf $CONFIG_PATH
+
+ # Create link from CONFIG_PATH to config_path
+ if [ -n "$config_path" -a "$config_path" != $CONFIG_PATH ]; then
+ if [ ! -d "$config_path" ]; then
+ echo "ZeroTier config_path does not exist: $config_path"
+ return
+ fi
+
+ ln -s $config_path $CONFIG_PATH
+ fi
+
+ mkdir -p $CONFIG_PATH/networks.d
+
+ if [ -n "$port" ]; then
+ ARGS="$ARGS -p$port"
+ fi
+
+ if [ "$secret" = "generate" ]; then
+ echo "Generate secret - please wait..."
+ local sf="/tmp/zt.$cfg.secret"
+
+ zerotier-idtool generate "$sf" > /dev/null
+ [ $? -ne 0 ] && return 1
+
+ secret="$(cat $sf)"
+ rm "$sf"
+
+ uci set zerotier.$cfg.secret="$secret"
+ uci commit zerotier
+ fi
+
+ if [ -n "$secret" ]; then
+ echo "$secret" > $CONFIG_PATH/identity.secret
+ # make sure there is not previous identity.public
+ rm -f $CONFIG_PATH/identity.public
+ fi
+
+ add_join() {
+ # an (empty) config file will cause ZT to join a network
+ touch $CONFIG_PATH/networks.d/$1.conf
+ }
+
+ config_list_foreach $cfg 'join' add_join
+
+ procd_open_instance
+ procd_set_param command $PROG $ARGS $CONFIG_PATH
+ procd_set_param stderr 1
+ procd_close_instance
+}
+
+start_service() {
+ config_load 'zerotier'
+ config_foreach start_instance 'zerotier'
+ touch /tmp/zero.log && /etc/zerotier.start > /tmp/zero.log 2>&1 &
+}
+
+stop_instance() {
+ rm -f /tmp/zero.log
+ local cfg="$1"
+
+ /etc/zerotier.stop > /tmp/zero.log 2>&1 &
+
+ # Remove existing link or folder
+ rm -f $CONFIG_PATH/networks.d/*.conf
+ rm -rf $CONFIG_PATH
+}
+
+stop_service() {
+ config_load 'zerotier'
+ config_foreach stop_instance 'zerotier'
+}
+
+reload_service() {
+ stop
+ start
+}
diff --git a/luci-app-zerotier/root/etc/zerotier.start b/luci-app-zerotier/root/etc/zerotier.start
new file mode 100755
index 000000000..b43e5f974
--- /dev/null
+++ b/luci-app-zerotier/root/etc/zerotier.start
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+zero_enable="$(uci get zerotier.sample_config.enabled)"
+
+[ "${zero_enable}" -ne "1" ] && exit 0
+
+[ -f "/tmp/zero.log" ] && {
+ while [ "$(ifconfig | grep 'zt' | awk '{print $1}')" = "" ]
+ do
+ sleep 1
+ done
+}
+
+nat_enable="$(uci get zerotier.sample_config.nat)"
+zt0="$(ifconfig | grep 'zt' | awk '{print $1}')"
+echo "${zt0}" > "/tmp/zt.nif"
+
+[ "${nat_enable}" -eq "1" ] && {
+ for i in ${zt0}
+ do
+ ip_segment=""
+ iptables -I FORWARD -i "$i" -j ACCEPT
+ iptables -I FORWARD -o "$i" -j ACCEPT
+ iptables -t nat -I POSTROUTING -o "$i" -j MASQUERADE
+ ip_segment="$(ip route | grep "dev $i proto kernel" | awk '{print $1}')"
+ iptables -t nat -I POSTROUTING -s "${ip_segment}" -j MASQUERADE
+ done
+}
diff --git a/luci-app-zerotier/root/etc/zerotier.stop b/luci-app-zerotier/root/etc/zerotier.stop
new file mode 100755
index 000000000..cbe7ec4b6
--- /dev/null
+++ b/luci-app-zerotier/root/etc/zerotier.stop
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+zt0="$(ifconfig | grep 'zt' | awk '{print $1}')"
+[ -z "${zt0}" ] && zt0="$(cat "/tmp/zt.nif")"
+
+for i in ${zt0}
+do
+ ip_segment=""
+ iptables -D FORWARD -i "$i" -j ACCEPT 2>/dev/null
+ iptables -D FORWARD -o "$i" -j ACCEPT 2>/dev/null
+ iptables -t nat -D POSTROUTING -o "$i" -j MASQUERADE 2>/dev/null
+ ip_segment="$(ip route | grep "dev $i proto" | awk '{print $1}')"
+ iptables -t nat -D POSTROUTING -s "${ip_segment}" -j MASQUERADE 2>/dev/null
+ echo "zt interface $i is stopped!"
+done
diff --git a/luci-app-zerotier/root/etc/zerotier/zerotier.log b/luci-app-zerotier/root/etc/zerotier/zerotier.log
new file mode 100755
index 000000000..e69de29bb
diff --git a/luci-theme-openwrt-2020/Makefile b/luci-theme-openwrt-2020/Makefile
new file mode 100755
index 000000000..e5ef95832
--- /dev/null
+++ b/luci-theme-openwrt-2020/Makefile
@@ -0,0 +1,14 @@
+#
+# Copyright (C) 2020 Jo-Philipp Wich
+#
+# This is free software, licensed under the Apache License, Version 2.0 .
+#
+
+include $(TOPDIR)/rules.mk
+
+LUCI_TITLE:=LuCI modern OpenWrt theme
+LUCI_DEPENDS:=
+
+include $(TOPDIR)/feeds/luci/luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/GalanoGrotesqueW00-Regular.woff2 b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/GalanoGrotesqueW00-Regular.woff2
new file mode 100755
index 000000000..950ac98cc
Binary files /dev/null and b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/GalanoGrotesqueW00-Regular.woff2 differ
diff --git a/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/cascade.css b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/cascade.css
new file mode 100755
index 000000000..b412764d4
--- /dev/null
+++ b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/cascade.css
@@ -0,0 +1,1925 @@
+:root {
+ --main-bright-color: #00A3E1;
+ --main-dark-color: #002B49;
+ --secondary-bright-color: #FFFFFF;
+ --secondary-dark-color: #212322;
+ --danger-color: #CC1111;
+ --warning-color: #CC8800;
+ --regular-font: "GalanoGrotesqueW00-Regular";
+ --base-font-size: 16px;
+}
+
+@font-face {
+ font-family: "GalanoGrotesqueW00-Regular";
+ src: url("GalanoGrotesqueW00-Regular.woff2") format("woff2");
+}
+
+/*
+ * resets and base style
+ */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ text-decoration: none;
+ list-style: none;
+ color: inherit;
+ font-family: var(--regular-font), "sans-serif";
+ border: none;
+ font-size: 100%;
+ background: none;
+ outline: none;
+ -webkit-appearance: none;
+ -webkit-text-size-adjust: none;
+}
+
+html {
+ height: 100%;
+ width: 100%;
+ max-width: 1366px;
+ margin: 0 auto;
+ background: #fff linear-gradient(90deg, rgba(0, 0, 0, .8), rgba(0, 0, 0 ,.5), rgba(0, 0, 0, .8));
+}
+
+body {
+ background: var(--secondary-bright-color);
+ color: var(--secondary-dark-color);
+ font-size: var(--base-font-size);
+ cursor: default;
+ display: inline-flex;
+ flex-direction: column;
+ min-height: 100%;
+ min-width: 100%;
+}
+
+/*
+ * scaffholding
+ */
+
+#menubar {
+ background-color: var(--main-bright-color);
+ background-image: url("omr-logo.png");
+ background-position: 10px center;
+ background-size: 50px 50px;
+ background-repeat: no-repeat;
+ padding: 0 1em 0 70px;
+ min-height: 70px;
+ display: flex;
+ align-items: center;
+ color: var(--secondary-bright-color);
+ flex: 0;
+ width: 100%;
+ box-shadow: inset 0 0 1px var(--main-dark-color);
+}
+
+#menubar > * {
+ flex: 1 1 auto;
+}
+
+#menubar .hostname {
+ font-weight: bold;
+ font-size: 2em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#menubar .distversion {
+ flex: 3;
+}
+
+#indicators {
+ flex: 1 1 25%;
+ text-align: right;
+}
+
+#indicators > * {
+ background: var(--secondary-bright-color);
+ color: var(--main-bright-color);
+ display: inline-block;
+ font-size: .85em;
+ line-height: 1.5em;
+ padding: 0 .5em;
+ margin: .125em;
+ border-radius: 1em;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+#indicators > [data-style="inactive"] {
+ background: var(--main-bright-color);
+ color: var(--secondary-bright-color);
+ border: 2px solid var(--secondary-bright-color);
+ line-height: calc(1.5em - 4px);
+ padding: 0 calc(.5em - 2px);
+}
+
+#menubar h2,
+.skiplink {
+ display: none;
+}
+
+#modemenu {
+ background: var(--main-bright-color);
+ padding: .5rem 1rem;
+ display: flex;
+ align-items: center;
+ color: var(--secondary-bright-color);
+ box-shadow: inset 0 0 1px var(--main-dark-color);
+ font-size: 1rem;
+ flex-wrap: wrap;
+}
+
+#modemenu > * {
+ margin: .125rem;
+}
+
+#modemenu > .active {
+ font-weight: bold;
+ border-bottom: 2px solid var(--secondary-bright-color);
+}
+
+#maincontainer {
+ flex-direction: row;
+ display: inline-flex;
+ flex: 1 0 auto;
+}
+
+#mainmenu {
+ flex: 1 1 200px;
+ background: var(--main-dark-color);
+ color: var(--main-bright-color);
+ padding: 1em;
+}
+
+#mainmenu:empty {
+ max-width: 0;
+ padding: 1em 0;
+ transition: all .2s ease-in-out;
+}
+
+#mainmenu > div {
+ position: sticky;
+ top: 1em;
+}
+
+#mainmenu ul {
+ padding: 0;
+ margin: 0 0 .5em .5em;
+ line-height: 1.5em;
+}
+
+#mainmenu ul > li {
+ list-style: none;
+}
+
+#mainmenu li > ul {
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height .1s ease-in-out;
+}
+
+#mainmenu li.selected > a {
+ color: var(--secondary-bright-color);
+}
+
+#mainmenu ul:not(.active) > li.selected > ul,
+#mainmenu li.active > ul {
+ max-height: 3000px;
+ transition: max-height 1s ease-in-out;
+}
+
+#mainmenu .l1 > li > a {
+ font-weight: bold;
+ font-size: 1.05em;
+}
+
+#maincontent {
+ flex: 10;
+ padding: 1em 1em 0 1em;
+}
+
+body > .luci {
+ flex: 0;
+ font-size: .7em;
+ padding: .25em;
+ text-align: right;
+ background: var(--main-bright-color);
+ color: var(--secondary-bright-color);
+ margin: 0;
+}
+
+/*
+ * modal
+ */
+
+body.modal-overlay-active {
+ overflow: hidden;
+}
+
+body.modal-overlay-active #modal_overlay {
+ left: 0;
+ right: 0;
+ opacity: 1;
+}
+
+#modal_overlay {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: -10000px;
+ right: 10000px;
+ background: rgba(0, 0, 0, 0.7);
+ z-index: 10000;
+ overflow-y: scroll;
+ -webkit-overflow-scrolling: touch;
+ transition: opacity .125s ease-in;
+ opacity: 0;
+}
+
+#modal_overlay > .modal {
+ max-width: 1300px;
+ width: 80%;
+ margin: 10% auto 5rem auto;
+ background: var(--secondary-bright-color);
+ box-shadow: 0 0 3px 1px var(--main-bright-color);
+ padding: .5em;
+ border-radius: .25em;
+ display: flex;
+ flex-direction: column;
+}
+
+.modal > h4:first-child {
+ padding: .5rem;
+ margin: -.5rem -.5rem .5rem -.5rem;
+ background: var(--main-bright-color);
+ color: var(--secondary-bright-color);
+ border-radius: .25rem .25rem 0 0;
+}
+
+.modal > *:first-child:last-child {
+ margin: .5em 0 !important;
+}
+
+.modal .cbi-section > legend:first-child { font-size: 120%; }
+
+
+/*
+ * table layout
+ */
+
+.table {
+ display: table;
+ width: 100%;
+ margin: 0 0 1rem 0;
+ position: relative;
+}
+
+.tr {
+ display: table-row;
+}
+
+.tr.cbi-section-table-titles[data-title]::before {
+ font-weight: bold;
+ border-top: none;
+}
+
+.tr[data-title]::before {
+ content: attr(data-title);
+ display: table-cell;
+ border-top: 1px solid var(--main-dark-color);
+ padding: .5em;
+}
+
+.th {
+ font-weight: bold;
+ display: table-cell;
+ padding: .5em;
+ /* word-break: break-word; */
+}
+
+.cbi-section-table-descr .th {
+ opacity: .8;
+ font-size: 90%;
+ font-weight: normal;
+}
+
+.td {
+ display: table-cell;
+ border-top: 1px solid var(--main-dark-color);
+ padding: .5em;
+ vertical-align: middle;
+}
+
+.td input:not([type]),
+.td input[type="text"],
+.td input[type="password"],
+.td select,
+.td .cbi-dropdown:not(.btn):not(.cbi-button),
+.td .cbi-dynlist,
+.td .control-group {
+ min-width: auto;
+ width: 100%;
+}
+
+.tr.drag-over-above {
+ box-shadow: 0 -6px 6px var(--main-bright-color);
+}
+
+.tr.drag-over-below {
+ box-shadow: 0 6px 6px var(--main-bright-color);
+}
+
+.tr.placeholder {
+ height: 4em;
+ position: relative;
+}
+
+.tr.placeholder > .td {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ text-align: center;
+ line-height: 3em;
+ font-size: 90%;
+ opacity: .8;
+}
+
+/*
+ * view specific table invariants
+ */
+
+ #cbi-wireless-wifi-device .ifacebadge {
+ flex-direction: column;
+ justify-content: space-around;
+ }
+
+.assoclist .td,
+[data-page="admin-status-overview"] .td {
+ font-size: .9rem;
+ vertical-align: middle;
+}
+
+.assoclist .td:nth-of-type(3) > span {
+ display: block;
+ max-width: 270px;
+ font-size: .8rem;
+}
+
+.assoclist .td:nth-of-type(5) > span {
+ font-size: .8rem;
+}
+
+.assoclist .td > .ifacebadge {
+ flex-wrap: wrap;
+ justify-content: space-around;
+ max-width: 120px;
+ padding: .2em;
+}
+
+.assoclist .td > .ifacebadge::after {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.assoclist .td > .ifacebadge > img {
+ margin: 0 25px;
+}
+
+.assoclist .td > .ifacebadge[data-ssid][data-ifname] > span {
+ display: none;
+}
+
+.assoclist .td > .ifacebadge[data-ssid][data-ifname]::after {
+ content: attr(data-ssid) " (" attr(data-ifname) ")";
+}
+
+[data-page="admin-status-overview"] .td:nth-of-type(3) {
+ min-width: 100px;
+}
+
+[data-page="admin-network-firewall"] .table > .tr > *:nth-child(1) {
+ flex: 1 1 30%;
+}
+
+[data-page="admin-network-wireless"] .cbi-section-actions > div {
+ display: flex;
+}
+
+[data-page="admin-network-wireless"] .cbi-section-actions > div > * {
+ flex: 1;
+}
+
+[data-page="admin-status-processes"] .table .td:nth-of-type(3),
+[data-tab="leases"] .table .td[data-name="duid"] {
+ word-break: break-word;
+}
+
+/*
+ * uci changelog
+ */
+
+.uci-change-list {
+ font-size: 90%;
+ white-space: pre;
+ overflow: hidden;
+}
+
+.uci-change-list del,
+.uci-change-list ins,
+.uci-change-list var,
+.uci-change-legend-label del,
+.uci-change-legend-label ins,
+.uci-change-legend-label var {
+ text-decoration: none;
+ font-family: monospace;
+ font-style: normal;
+ border: 1px solid #ccc;
+ background: #eee;
+ padding: 2px;
+ display: block;
+ line-height: 15px;
+ margin-bottom: 1px;
+}
+
+.uci-change-list h5 {
+ margin: .5em 0 .25em 0;
+}
+
+.uci-change-list ins,
+.uci-change-legend-label ins {
+ border-color: #0f0;
+ background: #cfc;
+}
+
+.uci-change-list del,
+.uci-change-legend-label del {
+ border-color: #f00;
+ background: #fcc;
+}
+
+.uci-change-list var,
+.uci-change-legend-label var {
+ border-color: #ccc;
+ background: #eee;
+}
+
+.uci-change-list var ins,
+.uci-change-list var del {
+ display: inline-block;
+ border: none;
+ width: 100%;
+ padding: 0;
+}
+
+.uci-change-legend {
+ margin: .5em 0 0 0;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.uci-change-legend-label {
+ flex: 1 1 10em;
+ white-space: nowrap;
+}
+
+.uci-change-legend-label > ins,
+.uci-change-legend-label > del,
+.uci-change-legend-label > var {
+ float: left;
+ margin-right: 4px;
+ width: 16px;
+ height: 16px;
+ display: block;
+ position: relative;
+}
+
+.uci-change-legend-label var ins,
+.uci-change-legend-label var del {
+ border: none;
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ right: 2px;
+ bottom: 2px;
+}
+
+/*
+ * alignment helpers
+ */
+
+.left {
+ text-align: left !important;
+}
+
+.right {
+ text-align: right !important;
+}
+
+.center {
+ text-align: center !important;
+}
+
+.top {
+ vertical-align: top !important;
+}
+
+.bottom {
+ vertical-align: bottom !important;
+}
+
+.middle {
+ vertical-align: middle !important;
+}
+
+.nowrap {
+ white-space: nowrap !important;
+}
+
+.hidden {
+ display: none !important;
+}
+
+/*
+ * legacy hacks
+ */
+
+[width="33%"] {
+ width: 33%;
+ max-width: 33%;
+}
+
+[width="50%"] {
+ width: 50%;
+ max-width: 50%;
+}
+
+[data-name="_freq"] select {
+ min-width: auto;
+}
+
+.cbi-value-field > div:first-child + br {
+ display: none;
+}
+
+/*
+ * typography
+ */
+
+h1, h2, h3, h4, h5, h6,
+.cbi-section > legend:first-child {
+ font-weight: bold;
+ margin: 0 0 1rem 0;
+}
+
+strong, b {
+ font-weight: bold;
+}
+
+h1 { font-size: 160%; }
+h2 { font-size: 150%; }
+h3 { font-size: 140%; }
+h4 { font-size: 130%; }
+h5 { font-size: 120%; }
+h6 { font-size: 110%; }
+
+.cbi-section > legend:first-child { font-size: 140%; }
+
+p, ul, textarea {
+ margin: 0 0 1em 0;
+}
+
+p > textarea:last-child {
+ margin: 0;
+}
+
+var {
+ color: var(--main-dark-color);
+ font-weight: bold;
+}
+
+code {
+ font-family: monospace;
+ color: var(--main-dark-color);
+}
+
+pre {
+ font-family: monospace;
+ margin: 0 0 1em 0;
+ font-size: .9rem;
+ box-shadow: inset 0 0 2px var(--main-dark-color);
+ padding: .25rem;
+ overflow: auto;
+}
+
+big {
+ font-size: 110%;
+}
+
+small {
+ font-size: 95%;
+}
+
+ul {
+ padding: 0 0 0 1.5em;
+}
+
+ul > li {
+ list-style: disc;
+}
+
+/*
+ * widgets
+ */
+
+.ifacebox, .ifacebadge, .zonebadge {
+ display: inline-flex;
+ line-height: 1.8em;
+ padding: 0 .25em;
+ margin: .25em;
+ box-shadow: 0px 0px 2px var(--main-dark-color);
+ font-size: .9em;
+ border-radius: .5em;
+ overflow: hidden;
+ font-size: .8rem;
+ vertical-align: text-top;
+ background: var(--secondary-bright-color);
+ align-items: center;
+ color: var(--secondary-dark-color);
+ vertical-align: middle;
+}
+
+.zonebadge > .ifacebadge {
+ margin: .125em -.125em .125em .35em;
+}
+
+.zonebadge > .ifacebadge > img
+{
+ margin: .125em 0 .125em .25em;
+}
+
+.ifacebox {
+ display: inline-flex;
+ flex-direction: column;
+ padding: 0;
+ text-align: center;
+ width: 100%;
+ max-width: 100px;
+}
+
+.ifacebox-head {
+ background: var(--main-bright-color);
+ width: 100%;
+}
+
+.ifacebox-body {
+ text-align: center;
+ padding: .3em .25em .25em .25em;
+ white-space: nowrap;
+}
+
+.ifacebadge {
+ display: inline-flex;
+ align-items: center;
+}
+
+.ifacebadge.large {
+ line-height: 1.3em;
+}
+
+.ifacebadge > img {
+ vertical-align: text-bottom;
+ margin: .25em;
+ height: 16px;
+}
+
+.ifacebadge > * {
+ margin-left: .25em;
+}
+
+.network-status-table {
+ display: inline-flex;
+ flex-wrap: wrap;
+ width: 100%;
+ margin: 0 -.2em 1em -.2em;
+}
+
+.network-status-table > .ifacebox {
+ max-width: none;
+ flex: 1 1 45%;
+ margin: .25em;
+ min-width: 250px;
+}
+
+.network-status-table > .ifacebox .ifacebadge {
+ font-size: 100%;
+ max-width: none;
+ flex: 1 1 45%;
+ margin: .2em;
+}
+
+.network-status-table .ifacebox-body > div {
+ display: flex;
+ flex-wrap: wrap;
+ margin: .3em -.1em -.1em -.1em;
+}
+
+.cbi-tooltip-container {
+ cursor: help;
+}
+
+.cbi-tooltip {
+ position: absolute;
+ z-index: 10000;
+ left: -10000px;
+ box-shadow: 0 0 2px rgba(0, 0, 0, .7);
+ border-radius: 3px;
+ background: var(--secondary-bright-color);
+ white-space: pre;
+ padding: 2px 5px;
+ opacity: 0;
+ transition: opacity .25s ease-in;
+ font-size: .8rem;
+}
+
+.cbi-tooltip.error {
+ color: var(--danger-color);
+}
+
+.cbi-tooltip-container:hover .cbi-tooltip:not(:empty) {
+ left: auto;
+ opacity: 1;
+ transition: opacity .25s ease-in;
+}
+
+.zone-forwards {
+ display: flex;
+ align-items: center;
+}
+
+.cbi-progressbar {
+ border-radius: .25em;
+ position: relative;
+ min-width: 20rem;
+ height: 1.5em;
+ box-shadow: 0 0 2px var(--main-dark-color);
+ overflow: hidden;
+ margin: .125rem 0;
+}
+
+.cbi-progressbar > div {
+ background: var(--main-bright-color);
+ height: 100%;
+ transition: width .25s ease-in;
+ width: 0%;
+}
+
+.cbi-progressbar::after {
+ position: absolute;
+ bottom: 0;
+ top: 0;
+ right: 0;
+ left: 0;
+ text-align: center;
+ text-shadow: 0 0 2px var(--secondary-bright-color);
+ content: attr(title);
+ white-space: nowrap;
+ line-height: 1.5em;
+}
+
+.cbi-tabmenu {
+ padding: 0;
+ margin: 0 -.5em 1em -.5em;
+ font-weight: bold;
+ color: var(--main-dark-color);
+}
+
+.cbi-tabmenu > li {
+ display: inline-flex;
+ white-space: nowrap;
+ opacity: 1;
+ height: 1.8em;
+ max-height: none;
+ overflow: visible;
+}
+
+.cbi-tabmenu > li > a {
+ flex: 1;
+ margin: .1em .5em;
+}
+
+.cbi-tabmenu > .cbi-tab > a {
+ border-bottom: 2px solid var(--main-dark-color);
+}
+
+[data-tab] {
+ opacity: 0;
+ max-height: 0;
+ transition: opacity .25s ease-in-out;
+ overflow: hidden;
+}
+
+[data-tab-active="true"] {
+ opacity: 1;
+ height: auto;
+ max-height: none;
+ overflow: visible;
+}
+
+.alert-message:not(.modal) {
+ box-shadow: 0 0 3px var(--secondary-dark-color);
+ padding: .5em;
+ margin: 0 0 1em 0;
+ background: var(--warning-color);
+ color: var(--secondary-bright-color);
+ transition: opacity .4s ease;
+}
+
+.alert-message + .alert-message {
+ margin: -.5em 0 1em 0;
+}
+
+.alert-message.info {
+ background: var(--main-bright-color);
+}
+
+.alert-message.warning {
+ background: var(--warning-color);
+}
+
+.alert-message.danger {
+ background: var(--danger-color);
+}
+
+.alert-message .btn {
+ background: inherit;
+ box-shadow: 0 0 2px var(--secondary-bright-color);
+}
+
+.alert-message .btn:hover {
+ box-shadow: 0 0 4px 1px var(--secondary-bright-color);
+}
+
+@keyframes fade-in {
+ 0% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+@keyframes fade-out {
+ 0% { opacity: 1; }
+ 100% { opacity: 0; }
+}
+
+.fade-in {
+ animation: fade-in .4s ease;
+}
+
+.fade-out {
+ animation: fade-out .4s ease;
+ opacity: 0;
+}
+
+/*
+ * forms
+ */
+
+.cbi-button, button, .btn {
+ background: var(--main-bright-color);
+ color: var(--secondary-bright-color);
+ line-height: 1.5em;
+ border-radius: .25em;
+ cursor: pointer;
+ box-shadow: 0 0 2px var(--main-dark-color);
+ padding: 0 .5em;
+ display: inline-block;
+}
+
+.cbi-button:hover, button:hover, .btn:hover {
+ box-shadow: 0 0 6px var(--main-bright-color);
+}
+
+button + button, .btn + .btn, button + .btn, .btn + button, select + button {
+ margin-left: .25em;
+}
+
+button.important {
+ background: var(--main-dark-color);
+}
+
+button[disabled], button.disabled, .btn[disabled], .btn.disabled {
+ pointer-events: none;
+ opacity: .6;
+}
+
+.cbi-button-apply, .cbi-button-positive {
+ background: var(--main-dark-color);
+}
+
+.cbi-button-negative, .cbi-button-remove {
+ background: var(--danger-color);
+}
+
+.cbi-checkbox {
+ position: relative;
+}
+
+.cbi-checkbox input[type="checkbox"] {
+ position: absolute;
+ z-index: 10;
+ -webkit-appearence: button;
+ height: 1.3em;
+ width: 1.3em;
+ opacity: 0;
+ cursor: pointer;
+}
+
+.cbi-checkbox input[type="checkbox"] + label {
+ position: relative;
+ display: inline-block;
+ width: 1.3em;
+ height: 1.3em;
+ vertical-align: text-top;
+}
+
+.cbi-checkbox input[type="checkbox"] + label::before {
+ content: "\0a";
+ height: 1em;
+ width: 1em;
+ box-shadow: 0 0 2px var(--main-dark-color);
+ display: inline-block;
+ border-radius: .25em;
+ margin: .15em 0;
+ position: absolute;
+ left: 0;
+ top: 0;
+}
+
+.cbi-checkbox input[type="checkbox"]:checked + label::after {
+ content: "\0a";
+ position: absolute;
+ display: inline-block;
+ background: var(--main-dark-color);
+ top: .35em;
+ left: .2em;
+ width: .6em;
+ height: .6em;
+ border-radius: .15em;
+ cursor: pointer;
+}
+
+.cbi-checkbox input.cbi-input-invalid[type="checkbox"] + label::before {
+ box-shadow: 0 0 2px var(--danger-color);
+}
+
+.cbi-checkbox input.cbi-input-invalid[type="checkbox"]:checked + label::after {
+ background: var(--danger-color);
+}
+
+.cbi-checkbox input[type="checkbox"][disabled] + label::before,
+.cbi-checkbox input[type="checkbox"][disabled] + label::after {
+ pointer-events: none;
+ opacity: .6;
+}
+
+.cbi-checkbox input[type="checkbox"][disabled] {
+ pointer-events: none;
+}
+
+input:not([type]),
+input[type="text"],
+input[type="password"],
+select,
+.cbi-dropdown:not(.btn):not(.cbi-button) {
+ border-bottom: 2px solid transparent;
+ box-shadow: inset 0 0 1px var(--main-dark-color);
+ padding: 0 .2rem;
+ line-height: 1.5rem;
+ min-height: calc(1.5rem + 2px);
+ min-width: 20rem;
+ border-radius: .25em;
+}
+
+input:not([type]):focus,
+input[type="text"]:focus,
+input[type="password"]:focus,
+select:focus,
+.cbi-dropdown:not(.btn):not(.cbi-button):focus,
+.cbi-dropdown[open]:not(.btn):not(.cbi-button) {
+ border-color: var(--main-dark-color);
+}
+
+input[disabled]:not([type]),
+input[disabled][type="text"],
+input[disabled][type="password"],
+select[disabled],
+.cbi-dynlist[disabled] {
+ opacity: .6;
+ pointer-events: none;
+}
+
+input:not([type]) + .btn, input:not([type]) + button,
+input[type="text"] + .btn, input[type="text"] + button,
+input[type="password"] + .btn, input[type="password"] + button {
+ margin: 0 0 2px -1px;
+ background: var(--main-dark-color);
+ border-radius: 0 .25em .25em 0;
+}
+
+.control-group > select + .btn, .control-group > select + button {
+ margin-left: .25em;
+}
+
+.control-group > input:not([type]) + .btn, .control-group > input:not([type]) + button,
+.control-group > input[type="text"] + .btn, .control-group > input[type="text"] + button,
+.control-group > input[type="password"] + .btn, .control-group > input[type="password"] + button {
+ margin: .125em .125em calc(.125em + 2px) calc(-.125em - .25em) !important;
+}
+
+input[type="checkbox"] {
+ height: 1em;
+ vertical-align: middle;
+ -webkit-appearance: checkbox;
+}
+
+select {
+ padding: .1rem 0;
+ -webkit-appearance: menulist;
+}
+
+textarea {
+ width: 100%;
+ box-shadow: inset 0 0 2px var(--main-dark-color);
+ font-family: monospace;
+ font-size: .9rem;
+ padding: .2rem;
+}
+
+.cbi-input-invalid,
+.cbi-input-invalid:focus {
+ color: var(--danger-color);
+ border-color: var(--danger-color) !important;
+ box-shadow: inset 0 0 2px var(--danger-color);
+}
+
+.control-group {
+ display: inline-flex;
+ margin: 0 -.125rem;
+ min-width: 20.25em;
+}
+
+.control-group > *,
+.control-group > .cbi-dropdown > ul > li {
+ justify-content: space-around;
+}
+
+.control-group > * {
+ margin: .125rem !important;
+ min-width: auto !important;
+}
+
+.control-group > select,
+.control-group > input:not([type]),
+.control-group > input[type="text"],
+.control-group > input[type="password"] {
+ flex: 10;
+}
+
+.cbi-value {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0 0 1em 0;
+}
+
+.cbi-value > label:first-child {
+ flex: 1 1 40%;
+ padding: 0 .5em 0 0;
+}
+
+.cbi-value > .cbi-value-field {
+ flex: 2 2 55%;
+}
+
+.cbi-value > .cbi-section {
+ flex: 1 1 100%;
+}
+
+.cbi-map-descr,
+.cbi-tab-descr,
+.cbi-section-descr,
+.cbi-value-description,
+.cbi-value[data-widget="CBI.DummyValue"] > div:first-child {
+ opacity: .8;
+ font-size: .9rem;
+ padding: .2em 0;
+}
+
+.cbi-map-descr,
+.cbi-tab-descr,
+.cbi-section-descr,
+.cbi-section-table,
+.cbi-section-create {
+ margin: 0 0 1em 0;
+}
+
+.cbi-dynlist {
+ display: inline-block;
+ font-size: 90%;
+ min-height: calc(1.5em + 2px);
+ line-height: 1.5em;
+ min-width: 20rem;
+ flex-wrap: wrap;
+}
+
+.cbi-dynlist > .item {
+ box-shadow: 0 0 2px var(--main-dark-color);
+ margin: .3em 0;
+ padding: .15em 2em .15em .2em;
+ border-radius: .25em;
+ position: relative;
+ overflow: hidden;
+ transition: box-shadow .25s ease-in-out;
+ pointer-events: none;
+ flex: 1 1 100%;
+ word-break: break-all;
+}
+
+.cbi-dynlist > .item::after {
+ content: "-";
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 1.6rem;
+ background: var(--main-bright-color);
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+ position: absolute;
+ box-shadow: 0 0 2px var(--main-dark-color);
+ text-align: center;
+ color: var(--secondary-bright-color);
+ cursor: pointer;
+ pointer-events: all;
+}
+
+.cbi-dynlist[disabled] > .item::after {
+ pointer-events: none;
+}
+
+.cbi-dynlist > .item:hover {
+ box-shadow: 0 0 2px var(--main-bright-color);
+}
+
+.cbi-dynlist > .add-item {
+ flex: 1;
+ display: flex;
+}
+
+.cbi-dynlist > .add-item > input {
+ flex: 1;
+ min-width: 18.5rem;
+ border-radius: .25rem 0 0 .25rem;
+}
+
+.cbi-dynlist > .add-item > .btn {
+ flex: 0 0 1.6rem;
+ margin: 0 0 2px -1px;
+ width: 1.6rem;
+ text-align: center;
+}
+
+.cbi-dropdown {
+ display: inline-flex !important;
+ cursor: pointer;
+ height: auto;
+ position: relative;
+ padding: 0 !important;
+}
+
+.cbi-dropdown:not(.btn):not(.cbi-button) {
+ box-shadow: inset 0 0 1px var(--main-dark-color);
+}
+
+.cbi-dropdown > ul {
+ margin: 0 !important;
+ padding: 0;
+ list-style: none;
+ overflow-x: hidden;
+ overflow-y: auto;
+ display: flex;
+ width: 100%;
+}
+
+.cbi-dropdown.btn > ul:not(.dropdown) {
+ padding-left: .5em;
+}
+
+.cbi-dropdown.btn.spinning > ul:not(.dropdown) {
+ padding-left: 0;
+}
+
+.cbi-dropdown.btn > ul.dropdown > li {
+ color: var(--main-dark-color);
+}
+
+.cbi-dropdown > ul.preview {
+ display: none;
+}
+
+.cbi-dropdown > .open,
+.cbi-dropdown > .more {
+ flex-grow: 0;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ text-align: center;
+ padding: 0 .25em;
+}
+
+.cbi-dropdown.btn > .open,
+.cbi-dropdown.cbi-button > .open {
+ padding: 0 .5em;
+ margin-left: .5em;
+ border-left: 1px solid;
+}
+
+.cbi-dropdown > .more,
+.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[placeholder] {
+ display: none;
+ justify-content: center;
+ color: rgba(0, 0, 0, .5);
+}
+
+.cbi-dropdown > ul > li {
+ display: none;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex-shrink: 1;
+ flex-grow: 1;
+ align-items: center;
+ align-self: center;
+ color: inherit;
+}
+
+.cbi-dropdown > ul.dropdown > li,
+.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li {
+ padding: 0 .25em;
+}
+
+.cbi-dropdown > ul > li .hide-open { display: block; display: initial; }
+.cbi-dropdown > ul > li .hide-close { display: none; }
+
+.cbi-dropdown > ul > li[display]:not([display="0"]) {
+ border-left: 1px solid #ccc;
+}
+
+.cbi-dropdown[empty] > ul {
+ max-width: 1px;
+ max-height: 1.5em;
+}
+
+.cbi-dropdown > ul > li > form {
+ display: none;
+ margin: 0;
+ padding: 0;
+ pointer-events: none;
+}
+
+.cbi-dropdown > ul > li img {
+ align-self: center;
+ margin-right: .25em;
+}
+
+.cbi-dropdown > ul > li input[type="text"] {
+ margin: .25em 0;
+ border: none;
+ background: var(--secondary-bright-color);
+}
+
+.cbi-dropdown[open] {
+ position: relative;
+}
+
+.cbi-dropdown[open] > ul.dropdown {
+ display: block;
+ background: var(--secondary-bright-color);
+ box-shadow: 0 0 1px var(--main-dark-color), 0 0 4px rgba(0, 0, 0, .7);
+ position: absolute;
+ z-index: 1100;
+ max-width: none;
+ min-width: 100%;
+ width: auto;
+ transition: max-height .125s ease-in;
+}
+
+.cbi-dropdown > ul > li[display],
+.cbi-dropdown[open] > ul.preview,
+.cbi-dropdown[open] > ul.dropdown > li,
+.cbi-dropdown[multiple] > ul > li > label,
+.cbi-dropdown[multiple][open] > ul.dropdown > li,
+.cbi-dropdown[multiple][more] > .more,
+.cbi-dropdown[multiple][empty] > .more {
+ flex-grow: 1;
+ display: flex !important;
+}
+
+.cbi-dropdown[empty] > ul > li,
+.cbi-dropdown[optional][open] > ul.dropdown > li[placeholder],
+.cbi-dropdown[multiple][open] > ul.dropdown > li > form {
+ display: block !important;
+}
+
+.cbi-dropdown[open] > ul.dropdown > li .hide-open { display: none; }
+.cbi-dropdown[open] > ul.dropdown > li .hide-close { display: block; display: initial; }
+
+.cbi-dropdown[open] > ul.dropdown > li {
+ border-bottom: 1px solid #ccc;
+}
+
+.cbi-dropdown[open] > ul.dropdown > li[selected] {
+ background: var(--main-dark-color);
+ color: var(--secondary-bright-color);
+}
+
+.cbi-dropdown[open] > ul.dropdown > li.focus {
+ background: var(--main-bright-color);
+}
+
+.cbi-dropdown[open] > ul.dropdown > li:last-child {
+ margin-bottom: 0;
+ border-bottom: none;
+}
+
+.cbi-dropdown[open] > ul.dropdown > li[unselectable] {
+ opacity: 0.7;
+}
+
+.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child {
+ width: 100%;
+}
+
+.cbi-dropdown[disabled] {
+ pointer-events: none;
+ opacity: .6;
+}
+
+.cbi-filebrowser {
+ max-width: 100%;
+ width: 1px;
+ box-shadow: 0 0 2px var(--main-dark-color);
+ border-radius: .25rem;
+ display: flex;
+ flex-direction: column;
+ opacity: 0;
+ height: 0;
+ overflow: hidden;
+}
+
+.cbi-filebrowser.open {
+ min-width: 20rem;
+ width: auto;
+ opacity: 1;
+ height: auto;
+ overflow: visible;
+ transition: opacity .25s ease-in;
+}
+
+.cbi-filebrowser > * {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 0 0 .25em 0;
+ margin: .25em .25em 0px .25em;
+ white-space: nowrap;
+ border-bottom: 1px solid var(--main-dark-color);
+}
+
+.cbi-filebrowser .cbi-button-positive {
+ margin-right: .25em;
+}
+
+.cbi-filebrowser > div {
+ border-bottom: none;
+}
+
+.cbi-filebrowser > ul > li {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.cbi-filebrowser > ul > li a:hover {
+ font-weight: bold;
+ text-decoration: underline;
+}
+
+.cbi-filebrowser > ul > li > div:first-child {
+ flex: 10;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cbi-filebrowser > ul > li > div:last-child {
+ flex: 3 3 10em;
+ text-align: right;
+}
+
+.cbi-filebrowser > ul > li > div:last-child > button {
+ padding: .125em .25em;
+ margin: 1px 0 1px .25em;
+}
+
+.cbi-filebrowser .upload {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ margin: 0 -.125em .25em -.125em;
+ padding: 0 0 .125em 0px;
+ border-bottom: 1px solid var(--main-dark-color);
+}
+
+.cbi-filebrowser .upload > * {
+ margin: .125em;
+ flex: 1;
+}
+
+.cbi-filebrowser .upload > div > input {
+ width: 100%;
+}
+
+.cbi-section-actions {
+ text-align: right;
+}
+
+.cbi-page-actions {
+ flex-wrap: wrap;
+ width: 100%;
+ justify-content: flex-end;
+ margin-bottom: 1em;
+ margin-top: 1em;
+ border-top: 1px solid var(--main-dark-color);
+ padding-top: 1em;
+ text-align: right;
+}
+
+div[id$=".ipaddr"] > input,
+.cbi-value-field > div > input[type="password"] {
+ min-width: 18.5rem;
+ border-radius: .25rem 0 0 .25rem;
+}
+
+div[id$=".txpower"] {
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+div[id$=".txpower"] > span {
+ white-space: nowrap;
+ margin-left: .25em;
+}
+
+div[id$=".editlist"] {
+ flex: 1;
+}
+
+[data-errors]::after {
+ content: attr(data-errors);
+ background: var(--danger-color);
+ color: var(--secondary-bright-color);
+ border-radius: .6rem;
+ height: 1.1rem;
+ padding: 0 .25rem;
+ font-size: .9rem;
+ display: inline-block;
+ font-weight: bold;
+ min-width: .6rem;
+ line-height: 1rem;
+ margin: -.1rem 0 0 -.2rem;
+ text-align: center;
+}
+
+@keyframes spin {
+ 100% { transform: rotate(360deg); }
+}
+
+.spinning {
+ position: relative;
+ padding-left: 2.1em !important;
+}
+
+.spinning::before {
+ position: absolute;
+ display: block;
+ align-items: center;
+ top: 0;
+ bottom: 0;
+ left: .4em;
+ width: 1.3em;
+ height: 1.3em;
+ animation: spin 1s linear infinite;
+ content: url("spinner.svg");
+ margin: auto;
+}
+
+button.spinning, .btn.spinning {
+ padding-left: 1.6em !important;
+}
+
+button.spinning::before, .btn.spinning::before {
+ filter: invert(1);
+ left: .2em;
+ width: 1.2em;
+ height: 1.2em;
+}
+
+#view > div.spinning:first-child {
+ padding: .5em 0;
+}
+
+#view > *:last-child {
+ margin: 0 0 1em 0;
+}
+
+.label {
+ background: var(--main-bright-color);
+ color: var(--secondary-bright-color);
+ font-size: .8rem;
+ padding: 0 .4rem;
+ border-radius: .5rem;
+}
+
+.label.warning {
+ background: var(--danger-color);
+}
+
+ul.deps {
+ margin: 0;
+ padding: 0;
+ font-size: .9rem;
+}
+
+ul.errors {
+ margin: 0 0 1em 0;
+ padding: 0;
+}
+
+@media only screen and (max-width: 800px) {
+ body {
+ padding-top: 70px;
+ }
+
+ #maincontent {
+ padding: .25em;
+ max-width: 100vw;
+ }
+
+ #menubar {
+ background: var(--main-bright-color);
+ padding: 0 .5em;
+ position: fixed;
+ top: 0;
+ z-index: 1000;
+ }
+
+ #menubar > h2 {
+ flex: 0 0 2em;
+ display: block;
+ border: 2px solid var(--secondary-bright-color);
+ color: var(--secondary-bright-color);
+ border-radius: .5em;
+ cursor: pointer;
+ font-size: 100%;
+ margin: 0 1em 0 0;
+ }
+
+ #menubar > h2:hover {
+ border-color: var(--secondary-bright-color);
+ color: var(--secondary-bright-color);
+ }
+
+ #menubar > h2 > * {
+ display: none;
+ }
+
+ #menubar > h2::before {
+ content: "☰";
+ width: 35px;
+ line-height: 35px;
+ text-align: center;
+ display: inline-block;
+ color: inherit;
+ font-weight: bold;
+ }
+
+ #menubar > h2.active::before {
+ content: "×";
+ font-size: 200%;
+ }
+
+ #menubar .hostname {
+ font-size: 1.6em;
+ }
+
+ .distversion {
+ display: none;
+ }
+
+ #modemenu {
+ padding: .125em .25em;
+ }
+
+ #mainmenu {
+ overflow-x: hidden;
+ overflow-y: auto;
+ max-width: 0;
+ padding: 1em 0;
+ transition: max-width .25s ease-in-out, padding .25s ease-in-out;
+ position: fixed;
+ z-index: 900;
+ height: 100%;
+ }
+
+ #mainmenu.active {
+ max-width: 200px;
+ padding: 1em 1em calc(1em + 70px) 1em;
+ overflow-x: visible;
+ }
+
+ #mainmenu > div {
+ position: static;
+ }
+
+ #mainmenu ul > li {
+ padding: .25em 0;
+ }
+
+ .hide-xs {
+ display: none !important;
+ }
+
+ .table {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .tr {
+ display: block;
+ border-bottom: 1px solid var(--main-dark-color);
+ margin-bottom: .5em;
+ padding-bottom: .5em;
+ }
+
+ .tr.cbi-section-table-titles[data-title]::before,
+ .tr.cbi-section-table-titles,
+ .tr.cbi-section-table-descr {
+ display: none;
+ }
+
+ .tr[data-title]::before {
+ display: block;
+ font-weight: bold;
+ border-top: none;
+ padding: .4em 0;
+ font-size: 110%;
+ }
+
+ .td {
+ display: block;
+ border-top: none;
+ text-align: left !important;
+ padding: .2em 0;
+ }
+
+ .th, .table-titles {
+ display: none;
+ }
+
+ .td[data-title] {
+ position: relative;
+ padding: .2em 0 .2em 40%;
+ }
+
+ .td[data-title]::before {
+ content: attr(data-title) ": ";
+ white-space: nowrap;
+ font-weight: bold;
+ width: 40%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ padding: .2em 0;
+ text-align: left;
+ display: inline-flex;
+ align-items: center;
+ }
+
+ .td[data-title]::after {
+ content: "";
+ width: 2em;
+ position: absolute;
+ left: calc(40% - 2em);
+ top: 0;
+ bottom: 0;
+ display: block;
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0), var(--secondary-bright-color) 90%);
+ }
+
+ [data-page="admin-status-overview"] .cbi-section:nth-of-type(1) .td:first-of-type,
+ [data-page="admin-status-overview"] .cbi-section:nth-of-type(2) .td:first-of-type {
+ font-weight: bold;
+ max-width: none;
+ width: 100%;
+ }
+
+ [data-page="admin-status-overview"] .td > span > span { font-size: .9rem; }
+
+ [data-page="admin-status-routes"] .table:nth-of-type(3) .td:nth-of-type(1) { word-break: break-all; }
+
+ [data-page="admin-network-firewall-zones"] .td[data-name="_info"] {
+ padding: .2em 0;
+ line-height: 2.2rem;
+ }
+
+ [data-page="admin-network-firewall-zones"] .td[data-name="_info"]::before,
+ [data-page="admin-network-firewall-zones"] .td[data-name="_info"]::after {
+ display: none;
+ }
+
+ [data-page="admin-network-firewall-zones"] .td[data-name="_info"] label {
+ font-size: 1rem;
+ }
+
+ #cbi-wireless-wifi-device .tr { display: flex; flex-wrap: wrap; }
+ #cbi-wireless-wifi-device .tr > *:nth-child(1) { flex: 1 1 20%; align-self: center; }
+ #cbi-wireless-wifi-device .tr > *:nth-child(2) { flex: 2 2 75%; }
+ #cbi-wireless-wifi-device .tr > *:nth-child(3) { flex: 3 3 100%; }
+
+ #cbi-network-interface .tr { display: flex; flex-wrap: wrap; }
+ #cbi-network-interface .tr > *:nth-child(1) { flex: 1 1 33%; align-self: center; }
+ #cbi-network-interface .tr > *:nth-child(2) { flex: 2 2 60%; align-self: center; font-size: .9rem; overflow: hidden; }
+ #cbi-network-interface .tr > *:nth-child(3) { flex: 3 3 100%; }
+ #cbi-network-interface .tr > *:nth-child(2) > div { overflow: hidden; text-overflow: ellipsis; }
+
+ .assoclist .tr {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .assoclist .td > .ifacebadge {
+ max-width: 90px;
+ }
+
+ .assoclist .td > .ifacebadge > img {
+ margin: 0 35px;
+ }
+
+ .assoclist .td > .ifacebadge > span {
+ display: none;
+ }
+
+ .assoclist .td > .ifacebadge[data-ifname]::after {
+ content: attr(data-ifname);
+ }
+
+ .assoclist .td > .ifacebadge[data-signal]::after {
+ content: attr(data-signal) " dBm";
+ }
+
+ .assoclist .td:nth-of-type(3) {
+ font-weight: bold;
+ font-size: 1rem;
+ }
+
+ .assoclist .td:nth-of-type(1), .assoclist .td:nth-of-type(4) {
+ flex: 1 1 100px;
+ margin-right: .5em;
+ }
+
+ .assoclist .td:nth-of-type(3), .assoclist .td:nth-of-type(5) {
+ flex: 2 2 calc(100% - 110px);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ align-self: center;
+ }
+
+ .assoclist .td:nth-of-type(6) { flex: 1; text-align: right !important; }
+ .assoclist .td[data-title] { padding: .2em 0; }
+ .assoclist .td[data-title]::before,
+ .assoclist .td[data-title]::after { display: none; }
+
+ .leases6 .td:nth-of-type(3) { word-wrap: break-word; }
+
+ .td.cbi-section-actions > div { display: flex; }
+ .td.cbi-section-actions > div > * { flex: 1; }
+
+ body.modal-overlay-active #modal_overlay > .modal {
+ width: 95%;
+ margin: 5% auto;
+ }
+
+ input:not([type]),
+ input[type="text"],
+ input[type="password"],
+ select,
+ .cbi-dropdown:not(.btn):not(.cbi-button),
+ .cbi-dynlist {
+ min-height: calc(2.2rem + 2px);
+ line-height: 2.2rem;
+ font-size: 1.2rem;
+ min-width: 10rem;
+ }
+
+ button, .btn {
+ line-height: 1.8rem;
+ font-size: 1.2rem;
+ }
+
+ select {
+ padding: .4em 0;
+ }
+
+ .cbi-value > .cbi-value-field {
+ flex: 1 0 100%;
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ }
+
+ .cbi-value > .cbi-value-field > div[id] {
+ display: flex;
+ flex-direction: row;
+ }
+
+ .cbi-value > .cbi-value-field > div[id] > input,
+ .cbi-value > .cbi-value-field > div[id] > select,
+ .cbi-value > .cbi-value-field > div[id] > .cbi-filebrowser.open {
+ flex: 1;
+ width: 100%;
+ }
+
+ .cbi-dynlist .item::after,
+ .cbi-dynlist .add-item > .btn {
+ line-height: 2em;
+ flex-basis: 2rem;
+ width: 2rem;
+ }
+
+ .ifacebadge.large {
+ font-size: .9rem;
+ }
+
+ .control-group > *,
+ .control-group > .cbi-dropdown > ul > li {
+ flex: 1;
+ white-space: normal;
+ word-wrap: break-word;
+ }
+
+ .cbi-page-actions .cbi-dropdown,
+ .cbi-page-actions .cbi-button-apply:first-child {
+ flex-basis: 100%;
+ }
+
+ .cbi-checkbox {
+ margin: .25rem;
+ }
+
+ .cbi-tabmenu {
+ margin: 0 -.25em 1em -.25em;
+ }
+
+ .cbi-tooltip {
+ font-size: 1rem;
+ box-shadow: 0 0 4px rgba(0, 0, 0, .7);
+ }
+
+ .cbi-value > label:first-child {
+ padding: 0 0 .5em 0;
+ }
+
+ [data-page="admin-system-admin-sshkeys"] .cbi-dynlist > .item {
+ font-size: .9rem;
+ line-height: 1rem;
+ }
+
+ [data-page="admin-system-opkg"] .control-group {
+ flex-wrap: wrap;
+ }
+
+ [data-page="admin-status-iptables"] h2 + div.right {
+ margin: 0 0 1em 0 !important;
+ display: flex;
+ }
+}
+
+@media only screen and (min-width: 800px) and (max-width: 1200px) {
+ .assoclist .tr > *:nth-of-type(2) {
+ display: none;
+ }
+}
diff --git a/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/favicon.png b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/favicon.png
new file mode 100755
index 000000000..7c3f3acb1
Binary files /dev/null and b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/favicon.png differ
diff --git a/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/omr-logo.png b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/omr-logo.png
new file mode 100755
index 000000000..f8584ac11
Binary files /dev/null and b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/omr-logo.png differ
diff --git a/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/spinner.svg b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/spinner.svg
new file mode 100755
index 000000000..f3b52efac
--- /dev/null
+++ b/luci-theme-openwrt-2020/htdocs/luci-static/openwrt2020/spinner.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/luci-theme-openwrt-2020/htdocs/luci-static/resources/menu-openwrt2020.js b/luci-theme-openwrt-2020/htdocs/luci-static/resources/menu-openwrt2020.js
new file mode 100755
index 000000000..b6fbfd4c2
--- /dev/null
+++ b/luci-theme-openwrt-2020/htdocs/luci-static/resources/menu-openwrt2020.js
@@ -0,0 +1,147 @@
+'use strict';
+'require baseclass';
+'require ui';
+
+return baseclass.extend({
+ __init__: function() {
+ ui.menu.load().then(L.bind(this.render, this));
+ },
+
+ render: function(tree) {
+ var node = tree,
+ url = '';
+
+ this.renderModeMenu(node);
+
+ if (L.env.dispatchpath.length >= 3) {
+ for (var i = 0; i < 3 && node; i++) {
+ node = node.children[L.env.dispatchpath[i]];
+ url = url + (url ? '/' : '') + L.env.dispatchpath[i];
+ }
+
+ if (node)
+ this.renderTabMenu(node, url);
+ }
+
+ document.querySelector('#menubar > .navigation')
+ .addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle'));
+ },
+
+ handleMenuExpand: function(ev) {
+ var a = ev.target, ul1 = a.parentNode.parentNode, ul2 = a.nextElementSibling;
+
+ document.querySelectorAll('ul.mainmenu.l1 > li.active').forEach(function(li) {
+ if (li !== a.parentNode)
+ li.classList.remove('active');
+ });
+
+ if (!ul2)
+ return;
+
+ if (ul2.parentNode.offsetLeft + ul2.offsetWidth <= ul1.offsetLeft + ul1.offsetWidth)
+ ul2.classList.add('align-left');
+
+ ul1.classList.add('active');
+ a.parentNode.classList.add('active');
+ a.blur();
+
+ ev.preventDefault();
+ ev.stopPropagation();
+ },
+
+ renderMainMenu: function(tree, url, level) {
+ var l = (level || 0) + 1,
+ ul = E('ul', { 'class': 'mainmenu l%d'.format(l) }),
+ children = ui.menu.getChildren(tree);
+
+ if (children.length == 0 || l > 2)
+ return E([]);
+
+ for (var i = 0; i < children.length; i++) {
+ var isActive = (L.env.dispatchpath[l] == children[i].name),
+ isReadonly = children[i].readonly,
+ activeClass = 'mainmenu-item-%s%s'.format(children[i].name, isActive ? ' selected' : '');
+
+ ul.appendChild(E('li', { 'class': activeClass }, [
+ E('a', {
+ 'href': L.url(url, children[i].name),
+ 'click': (l == 1) ? ui.createHandlerFn(this, 'handleMenuExpand') : null
+ }, [ _(children[i].title) ]),
+ this.renderMainMenu(children[i], url + '/' + children[i].name, l)
+ ]));
+ }
+
+ if (l == 1)
+ document.querySelector('#mainmenu').appendChild(E('div', [ ul ]));
+
+ return ul;
+ },
+
+ renderModeMenu: function(tree) {
+ var menu = document.querySelector('#modemenu'),
+ children = ui.menu.getChildren(tree);
+
+ for (var i = 0; i < children.length; i++) {
+ var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0);
+
+ if (i > 0)
+ menu.appendChild(E([], ['\u00a0|\u00a0']));
+
+ menu.appendChild(E('div', { 'class': isActive ? 'active' : null }, [
+ E('a', { 'href': L.url(children[i].name) }, [ _(children[i].title) ])
+ ]));
+
+ if (isActive)
+ this.renderMainMenu(children[i], children[i].name);
+ }
+
+ if (menu.children.length > 1)
+ menu.style.display = '';
+ },
+
+ renderTabMenu: function(tree, url, level) {
+ var container = document.querySelector('#tabmenu'),
+ l = (level || 0) + 1,
+ ul = E('ul', { 'class': 'cbi-tabmenu' }),
+ children = ui.menu.getChildren(tree),
+ activeNode = null;
+
+ if (children.length == 0)
+ return E([]);
+
+ for (var i = 0; i < children.length; i++) {
+ var isActive = (L.env.dispatchpath[l + 2] == children[i].name),
+ activeClass = isActive ? ' cbi-tab' : '',
+ className = 'tabmenu-item-%s %s'.format(children[i].name, activeClass);
+
+ ul.appendChild(E('li', { 'class': className }, [
+ E('a', { 'href': L.url(url, children[i].name) }, [ _(children[i].title) ] )
+ ]));
+
+ if (isActive)
+ activeNode = children[i];
+ }
+
+ container.appendChild(ul);
+ container.style.display = '';
+
+ if (activeNode)
+ container.appendChild(this.renderTabMenu(activeNode, url + '/' + activeNode.name, l));
+
+ return ul;
+ },
+
+ handleSidebarToggle: function(ev) {
+ var btn = ev.currentTarget,
+ bar = document.querySelector('#mainmenu');
+
+ if (btn.classList.contains('active')) {
+ btn.classList.remove('active');
+ bar.classList.remove('active');
+ }
+ else {
+ btn.classList.add('active');
+ bar.classList.add('active');
+ }
+ }
+});
diff --git a/luci-theme-openwrt-2020/luasrc/view/themes/openwrt2020/footer.htm b/luci-theme-openwrt-2020/luasrc/view/themes/openwrt2020/footer.htm
new file mode 100755
index 000000000..e9122f0b5
--- /dev/null
+++ b/luci-theme-openwrt-2020/luasrc/view/themes/openwrt2020/footer.htm
@@ -0,0 +1,17 @@
+<%#
+ Copyright 2020 Jo-Philipp Wich
+ Licensed to the public under the Apache License 2.0.
+-%>
+
+
+
+
+
+ <% local ver = require "luci.version" -%>
+ Powered by <%= ver.luciname %> (<%= ver.luciversion %>)
+
+
+
+
+