;; 共享视觉层:公开页与管理后台均按 docs 参考图排版。
(defmodule lamina_ui
(export all))
(defun public-css ()
(lists:flatten
(list
":root{color-scheme:light only;--ink:#1f2328;--muted:#656d76;--link:#1f2328;--line:#d0d7de;--bg:#ffffff;--fill:#f6f8fa;--btn:#1f2328}"
"*{box-sizing:border-box}html,body{margin:0;background:var(--bg);color:var(--ink);"
"font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5}"
"a{color:var(--ink);text-decoration:none}a:hover{color:var(--muted);text-decoration:none}"
".shell{max-width:980px;margin:0 auto;padding:0 32px 28px}"
".nav{display:flex;align-items:center;gap:16px;height:32px;border-bottom:1px solid var(--line)}"
".brand{font-size:16px;font-weight:700;color:var(--ink);letter-spacing:-.02em;text-decoration:none;margin-right:8px}"
".brand:hover{text-decoration:none}.nav-links{display:flex;gap:16px;font-size:13px;font-weight:400}"
".nav-links a,.signin,.signin a{color:var(--ink);text-decoration:none}"
".nav-links a:hover,.signin a:hover{color:var(--muted);text-decoration:none}"
".signin{margin-left:auto;font-size:13px;display:flex;align-items:center;gap:14px}.tagline{text-align:right;color:var(--muted);font-size:13px;padding:8px 0 4px}"
"main{padding:18px 0 8px}h1{font-size:32px;font-weight:600;letter-spacing:-.03em;margin:0 0 8px;color:var(--ink)}"
".lede{margin:0 0 28px;color:var(--muted);font-size:16px}"
".section{margin:28px 0 8px}.section h2{font-size:20px;font-weight:600;margin:0 0 10px;padding-bottom:8px;border-bottom:1px solid var(--line)}"
".section p{color:var(--ink);margin:12px 0}"
"table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:10px 12px 10px 0;border-bottom:1px solid #eaeef2;vertical-align:top}"
"th{font-size:14px;font-weight:600;color:var(--ink)}td{font-size:14px;color:var(--ink)}"
"th:last-child,td:last-child{text-align:left;white-space:nowrap;color:var(--muted)}"
"a.repo{color:var(--ink);font-weight:600}"
"tr.group td{border-bottom:0;padding:18px 0 6px;font-size:13px;font-style:italic;color:var(--muted)}"
".clone{background:var(--fill);border:1px solid var(--line);border-radius:6px;padding:12px 16px;overflow:auto;"
"font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;color:var(--ink);margin:14px 0}"
"label{display:block;font-weight:600;font-size:14px;margin:18px 0 0}"
"input,textarea,select{display:block;width:100%;max-width:640px;margin-top:6px;padding:7px 12px;border:1px solid var(--line);"
"border-radius:6px;font:inherit;background:#fff}textarea{min-height:108px}"
".btn{display:inline-block;background:var(--btn);color:#fff;border:0;border-radius:6px;padding:8px 16px;font-size:14px;"
"font-weight:500;cursor:pointer;margin-top:22px;text-decoration:none}.btn:hover{background:#32383f;text-decoration:none;color:#fff}"
".btn-ghost{background:#fff;color:var(--ink);border:1px solid var(--line)}"
".hint{color:var(--muted);font-size:14px}.err{color:#9b1c1c;background:#ffebe9;border:1px solid #ff8182;border-radius:6px;padding:10px 12px;margin:16px 0;max-width:640px}"
".foot{display:flex;justify-content:space-between;gap:16px;align-items:center;border-top:1px solid var(--line);margin-top:48px;padding:16px 0 8px;color:var(--muted);font-size:13px}"
".foot nav{display:flex;gap:18px}.foot a{color:var(--muted)}.foot a:hover{color:var(--ink);text-decoration:none}"
".login-block{max-width:640px}.login-block h2{font-size:20px;font-weight:600;margin:0 0 10px}.rule{border:0;border-top:1px solid var(--line);margin:36px 0 28px}"
".empty{color:var(--muted);padding:8px 0 24px}"
".nav-links a.active{font-weight:650}"
".docs-shell{max-width:1120px}"
".docs{display:grid;grid-template-columns:240px minmax(0,1fr);gap:40px;align-items:start;padding-top:12px}"
".docs-side{position:sticky;top:18px;max-height:calc(100dvh - 36px);overflow:auto;padding-right:8px}"
".docs-side h2{font-size:12px;font-weight:650;letter-spacing:.04em;text-transform:uppercase;color:var(--muted);margin:18px 0 6px}"
".docs-side h2:first-child{margin-top:0}"
".docs-side ul{list-style:none;margin:0;padding:0}"
".docs-side a{display:block;padding:4px 8px;border-radius:6px;font-size:14px;color:var(--ink);text-decoration:none}"
".docs-side a:hover{background:#f6f8fa;color:var(--ink);text-decoration:none}"
".docs-side a.active{background:#f3f4f6;font-weight:650}"
".docs-body{min-width:0}"
".docs-body h1{font-size:32px}"
".docs-body h2{font-size:22px;font-weight:650;margin:28px 0 8px;padding-bottom:6px;border-bottom:1px solid var(--line)}"
".docs-body h3{font-size:18px;font-weight:650;margin:22px 0 8px}"
".docs-body p,.docs-body ul,.docs-body ol,.docs-body blockquote,.docs-body pre,.docs-body table{margin:12px 0}"
".docs-body a{text-decoration:underline;text-underline-offset:2px}"
".docs-body a:hover{color:var(--muted)}"
".docs-body ul,.docs-body ol{padding-left:1.3em}"
".docs-body li{margin:4px 0}"
".docs-body blockquote{margin-left:0;padding:2px 0 2px 14px;border-left:3px solid var(--line);color:var(--muted)}"
".docs-body pre{background:var(--fill);border-radius:10px;padding:14px 16px;overflow:auto}"
".docs-body code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;background:#eef0f3;border-radius:6px;padding:1px 6px}"
".docs-body pre code{background:transparent;padding:0;font-size:13px}"
".docs-body img{max-width:100%;border-radius:8px}"
".docs-body table{display:block;overflow:auto;border-collapse:collapse}"
".docs-body th,.docs-body td{border-bottom:1px solid #eaeef2;padding:8px 16px 8px 0;white-space:normal;color:inherit;text-align:left}"
".docs-body hr{border:0;border-top:1px solid var(--line);margin:22px 0}"
".library-nav{display:flex;flex-wrap:wrap;gap:8px 18px;border-bottom:1px solid var(--line);margin-top:8px}"
".library-nav a{color:var(--muted);text-decoration:none;padding:8px 0;border-bottom:2px solid transparent;font-size:14px}"
".library-nav a.active,.library-nav a:hover{color:var(--ink);text-decoration:none}"
".library-nav a.active{font-weight:650;border-bottom-color:var(--ink)}"
"@media(max-width:720px){.shell{padding:0 16px 24px}.nav{height:auto;min-height:32px;flex-wrap:wrap;padding:6px 0;gap:10px}"
".nav-links{order:3;width:100%;gap:16px}.signin{margin-left:auto}.tagline{text-align:left}h1{font-size:26px}"
".foot{display:block}.foot nav{margin-top:8px}th:last-child,td:last-child{white-space:normal}"
".docs{grid-template-columns:1fr;gap:18px}.docs-side{position:static;max-height:none}"
".docs-body h1{font-size:26px}}")))
(defun admin-css ()
(lists:flatten
(list
":root{color-scheme:light only;--ink:#1f2328;--muted:#656d76;--link:#1f2328;--line:#d0d7de;--bg:#f6f8fa;--panel:#fff;--active:#f3f4f6}"
"*{box-sizing:border-box}html,body{margin:0;background:var(--bg);color:var(--ink);"
"font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif}"
"a{color:var(--ink);text-decoration:none}a:hover{color:var(--muted);text-decoration:none}"
".app{display:flex;min-height:100dvh}"
".side{width:248px;flex:none;background:var(--panel);border-right:1px solid var(--line);padding:20px 14px 24px}"
".brand{font-size:18px;font-weight:700;color:var(--ink);text-decoration:none;display:block}"
".brand:hover{color:var(--muted);text-decoration:none}"
".brand-sub{font-size:12px;color:var(--muted);margin:2px 0 22px}"
".side-nav a{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:8px;color:var(--ink);margin-bottom:2px;font-size:14px}"
".side-nav a:hover{background:#f3f4f6;text-decoration:none;color:var(--ink)}.side-nav a.active{background:var(--active);color:var(--ink);font-weight:600}"
".side-nav svg{flex:none;opacity:.85}"
".workspace{flex:1;min-width:0;display:flex;flex-direction:column}"
".topbar{display:flex;justify-content:flex-end;align-items:center;gap:12px;padding:14px 28px 0}"
".topbar .hint{color:var(--ink);text-decoration:none;font-size:14px}.topbar .hint:hover{color:var(--muted);text-decoration:none}"
".btn{display:inline-flex;align-items:center;gap:6px;background:#1f2328;color:#fff;border:0;border-radius:6px;padding:7px 14px;"
"font-size:14px;font-weight:500;cursor:pointer;text-decoration:none}.btn:hover{background:#32383f;color:#fff;text-decoration:none}"
".btn.secondary,.btn-ghost{background:#fff;color:var(--ink);border:1px solid var(--line)}.btn.secondary:hover,.btn-ghost:hover{background:#f6f8fa;color:var(--ink)}"
".user{display:inline-flex;align-items:center;gap:8px;color:var(--ink);font-size:14px;font-weight:500}"
".avatar{width:32px;height:32px;border-radius:50%;background:#1f2328;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-weight:600}"
".content{padding:18px 32px 32px;flex:1}"
"h1{font-size:28px;font-weight:600;letter-spacing:-.03em;margin:0 0 6px}"
".lede{margin:0;color:var(--muted)}"
".hello{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:22px}"
".hello-meta{text-align:right;color:var(--muted);font-size:13px;line-height:1.6}"
".cards{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:16px}"
".stat{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:10px}"
".stat-top{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;font-weight:500}"
".stat-top svg{opacity:.75}.stat .big{font-size:28px;font-weight:650;letter-spacing:-.03em;line-height:1}"
".stat .sub{color:var(--muted);font-size:12px}"
".dash{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(280px,1fr);gap:14px}"
".panel{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px 18px}"
".panel h2{font-size:16px;margin:0;font-weight:600}.panel-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}"
".repo-row,.org-row{display:flex;align-items:flex-start;gap:12px;padding:12px 0;border-bottom:1px solid #eaeef2}"
".repo-row:last-child,.org-row:last-child{border-bottom:0}"
".repo-row .name,.org-row .name{font-weight:600;color:var(--ink)}"
".panel-head a,.topline a,.topline-actions a{color:var(--muted);font-size:14px}.panel-head a:hover,.topline a:hover,.topline-actions a:hover{color:var(--ink)}"
".repo-row p,.org-row p{margin:2px 0 0;color:var(--muted);font-size:13px}"
".grow{flex:1;min-width:0}.badge{font-size:12px;color:#1a7f37;background:#dafbe1;border-radius:999px;padding:2px 8px;margin:0 8px}"
".when{color:var(--muted);font-size:12px;white-space:nowrap;margin-left:auto}"
".org-mark{width:36px;height:36px;border-radius:10px;background:#f3f4f6;color:var(--ink);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex:none}"
".new-org{display:block;text-align:center;border:1px dashed var(--line);border-radius:8px;padding:10px;margin-top:8px;color:var(--muted);font-size:14px}"
".new-org:hover{border-color:var(--ink);color:var(--ink);text-decoration:none}"
".qa a{display:flex;align-items:center;gap:8px;padding:8px 0;color:var(--ink);font-size:14px}"
".banner{display:flex;gap:12px;align-items:flex-start;margin-top:16px;position:relative}"
".banner h2{margin-bottom:4px}.banner .clone{margin:10px 0 0;display:flex;justify-content:space-between;gap:12px;align-items:center}"
".banner button{border:0;background:transparent;color:var(--muted);cursor:pointer;font-size:18px}"
".clone{background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:10px 12px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px}"
".admin-foot{display:flex;justify-content:space-between;gap:16px;padding:8px 32px 20px;color:var(--muted);font-size:12px}"
".admin-foot nav{display:flex;gap:14px}.admin-foot a{color:var(--muted)}.admin-foot a:hover{color:var(--ink);text-decoration:none}"
".topline{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:16px}"
".tablebox{background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:auto}"
"table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:12px 16px;border-bottom:1px solid #eaeef2;font-size:14px}"
"th{color:var(--muted);font-weight:600;font-size:12px;text-transform:none}"
"label{display:block;font-weight:600;margin:16px 0 0;max-width:720px}"
"input,textarea,select{display:block;width:100%;margin-top:6px;padding:8px 10px;border:1px solid var(--line);border-radius:6px;font:inherit}"
"textarea{min-height:120px}fieldset{border:1px solid var(--line);border-radius:12px;padding:12px 16px 16px;margin:18px 0;max-width:720px;background:var(--panel)}"
".box,.cardbox{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:18px;max-width:720px}"
".hint{color:var(--muted);font-size:13px}.err{color:#9b1c1c;background:#ffebe9;border:1px solid #ff8182;border-radius:6px;padding:10px;margin:12px 0;max-width:720px}"
".notice{color:#1a7f37;background:#dafbe1;border:1px solid #4ac26b;border-radius:6px;padding:10px;margin:12px 0;max-width:720px}"
".danger{background:#cf222e;color:#fff;border:0;border-radius:6px;padding:8px 14px;cursor:pointer;font:inherit;font-size:14px;font-weight:500}"
".settings-section{margin-top:20px}.actions{margin:18px 0;display:flex;gap:8px;flex-wrap:wrap}"
".settings-page{max-width:800px}"
".settings-page .lede{margin:0 0 18px}"
".settings-card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:20px 22px;margin:0 0 16px}"
".settings-card h2{font-size:16px;font-weight:600;margin:0 0 6px}"
".settings-card h3{font-size:13px;font-weight:600;margin:16px 0 6px}"
".settings-card .hint{margin:0 0 10px;max-width:none}"
".settings-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:4px 16px}"
".settings-page label{margin-top:12px;max-width:none;font-size:13px}"
".settings-page input,.settings-page textarea,.settings-page select{width:100%;max-width:none}"
".settings-page textarea{min-height:88px}"
".settings-page .nav-area textarea{min-height:112px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px}"
".settings-page .span-2{grid-column:1/-1}"
".settings-actions{margin:4px 0 8px}"
".settings-page .btn{margin-top:8px}"
".library-page{max-width:none}"
".library-page .lede{max-width:68ch}"
".library-split{display:grid;grid-template-columns:320px minmax(0,1fr);gap:20px;align-items:start}"
".library-add{position:sticky;top:16px;margin:0}"
".library-head{display:flex;justify-content:space-between;align-items:baseline;margin:0 0 10px}"
".library-head h2{font-size:16px;font-weight:600;margin:0}"
".library-empty{margin:0;padding:14px 16px}"
".library-table{width:100%;table-layout:fixed}"
".library-table th{white-space:nowrap}"
".library-table th,.library-table td{padding:8px 10px}"
".library-table td{vertical-align:middle}"
".library-table input,.library-table select{margin:0;padding:6px 8px}"
".library-page .library-table select,.library-page .library-table td.library-name input{width:100%;min-width:0;max-width:100%}"
".library-table td.library-name,.library-table th:nth-child(1){width:9.5rem}"
".library-table td.library-repo,.library-table th:nth-child(2){width:6.5rem}"
".library-table td.library-branch,.library-table th:nth-child(4){width:6.5rem}"
".library-table td.library-order,.library-table th:nth-child(5){width:7.5rem}"
".library-table td.library-actions,.library-table th:nth-child(6){width:9.25rem}"
".library-table td.library-name,.library-table td.library-repo,.library-table td.library-branch,.library-table td.library-order,.library-table td.library-actions{white-space:nowrap}"
".library-table td.library-repo,.library-table td.library-address{overflow:hidden}"
".library-table .btn{margin:0;padding:5px 8px;font-size:13px;flex:none}"
".library-ops{display:inline-flex;gap:6px;align-items:center;white-space:nowrap;vertical-align:middle}"
".library-repo{font-weight:600}"
".library-addr{flex:1 1 12rem;min-width:0;overflow:hidden;color:var(--muted);font-size:13px;white-space:nowrap;text-overflow:ellipsis}"
".library-addr:hover{color:var(--ink)}"
".library-meta{display:flex;flex-wrap:wrap;align-items:center;column-gap:12px;row-gap:4px;min-width:0}"
".library-facts{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:4px 14px;max-width:100%;margin-left:auto;color:var(--muted);font-size:13px}"
".library-facts b{color:var(--ink);font-weight:600;margin-right:4px}"
".library-miss{color:var(--muted)}"
".settings-page .notice{max-width:none}"
".repo-form-page{max-width:800px}"
".repo-form-page .topline{align-items:center;flex-wrap:wrap;margin-bottom:20px}"
".repo-form-page .topline .lede{margin:4px 0 0}"
".repo-form-page .topline-actions{flex:none}"
".topline .btn,.topline-actions .btn{margin-top:0}"
".topline a.btn,.topline-actions a.btn{color:#fff;font-size:14px}"
".topline a.btn:hover,.topline-actions a.btn:hover{color:#fff}"
".topline a.btn.secondary,.topline-actions a.btn.secondary{color:var(--ink)}"
".topline a.btn.secondary:hover,.topline-actions a.btn.secondary:hover{color:var(--ink)}"
".repo-form-page form>.form-actions{margin-bottom:24px}"
".choice-row{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}"
".settings-page .choice{flex:1;min-width:180px;display:flex;gap:10px;align-items:flex-start;margin:0;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#fff;font-weight:500;cursor:pointer}"
".choice:has(input:checked){border-color:var(--ink);background:#f3f4f6}"
".settings-page .choice input{width:auto;margin:3px 0 0;padding:0;display:inline-block;border:0;box-shadow:none}"
".choice small{display:block;font-weight:400;color:var(--muted);margin-top:2px}"
".settings-page .check-row{display:flex;align-items:center;gap:8px;margin-top:12px;font-weight:500;max-width:none}"
".settings-page .check-row input{width:auto;margin:0;padding:0;display:inline-block;border:0;box-shadow:none}"
"input[readonly]{background:#f6f8fa;color:var(--muted)}"
".form-actions{display:flex;gap:10px;align-items:center;margin-top:8px;flex-wrap:wrap}"
".org-form .form-actions{margin-top:22px}"
".form-actions .btn{margin-top:0}"
".form-actions a{color:var(--muted);font-size:14px}"
".topline-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}"
".pill{display:inline-flex;align-items:center;padding:3px 9px;border-radius:999px;font-size:12px;font-weight:600;border:1px solid var(--line);background:#fff;color:var(--muted)}"
".pill-up{color:#1a7f37;background:#dafbe1;border-color:#4ac26b}"
".pill-starting{color:#9a6700;background:#fff8c5;border-color:#d4a72c}"
".pill-down,.pill-unknown{color:#9b1c1c;background:#ffebe9;border-color:#ff8182}"
".meta-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px;margin-bottom:14px}"
".meta-item{background:#f6f8fa;border-radius:8px;padding:10px 12px}"
".meta-item span{display:block;color:var(--muted);font-size:12px;font-weight:600;margin-bottom:2px}"
".settings-card>.meta-item{margin-top:12px}"
".settings-card .meta-item a{word-break:break-all}"
".danger-card{border-color:#ffc1c0}"
".danger-card h2{color:#cf222e}"
".settings-page a.danger{display:inline-block;text-decoration:none;margin-top:16px}"
".ops-note{margin:4px 0 0;color:var(--muted);font-size:13px}"
".compact-table{margin:0}.compact-table th,.compact-table td{padding:8px 10px;font-size:13px}"
".log-empty{margin:0}"
"code{background:#f6f8fa;padding:2px 6px;border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}"
"@media(max-width:1100px){.cards{grid-template-columns:1fr 1fr}.dash{grid-template-columns:1fr}"
".library-split{grid-template-columns:1fr}.library-add{position:static}}"
"@media(max-width:800px){.app{display:block}.side{width:auto;border-right:0;border-bottom:1px solid var(--line)}"
".side-nav{display:flex;flex-wrap:wrap}.content,.topbar,.admin-foot{padding-left:16px;padding-right:16px}"
".hello{display:block}.hello-meta{text-align:left;margin-top:8px}.cards{grid-template-columns:1fr}"
".settings-grid,.meta-row{grid-template-columns:1fr}}")))
(defun setup-css ()
(lists:flatten
(list
":root{color-scheme:light only;--ink:#1f2328;--muted:#656d76;--link:#1f2328;--line:#d0d7de}"
"*{box-sizing:border-box}body{margin:0;background:#fff;color:var(--ink);"
"font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Helvetica,Arial,sans-serif}"
".shell{max-width:1080px;margin:0 auto;padding:0 36px 24px}"
".top{display:flex;justify-content:space-between;align-items:flex-start;padding:22px 0 14px;border-bottom:1px solid var(--line)}"
".brand{color:var(--ink);font-size:22px;font-weight:700;text-decoration:none}.tag{margin:2px 0 0;color:var(--muted);font-size:13px}"
".top a{color:var(--ink);text-decoration:none}.top a:hover{color:var(--muted);text-decoration:none}"
".wrap{display:grid;grid-template-columns:200px minmax(0,1fr);gap:48px;padding:28px 0 40px}"
".steps div{padding:8px 12px;margin-bottom:2px;color:var(--ink);font-size:14px;border-radius:6px}"
".steps .cur{background:#f3f4f6;font-weight:600}"
"h1{font-size:28px;margin:0 0 6px}h2{font-size:16px;margin:22px 0 8px}"
".kicker{color:var(--muted);margin:0 0 18px}.main p{max-width:62ch}"
"hr{border:0;border-top:1px solid var(--line);margin:22px 0}"
"label{display:block;font-weight:600;margin:16px 0 0}input{display:block;width:100%;max-width:520px;margin-top:6px;padding:8px 10px;border:1px solid var(--line);border-radius:6px;font:inherit}"
"a{color:var(--ink);text-decoration:none}a:hover{color:var(--muted);text-decoration:none}"
".btn{background:#1f2328;color:#fff;border:0;border-radius:6px;padding:8px 16px;margin-top:22px;cursor:pointer;font-size:14px}"
".err{color:#9b1c1c;background:#ffebe9;border:1px solid #ff8182;border-radius:6px;padding:10px;margin:0 0 16px}"
"ul.check{padding-left:18px}ul.check li{margin:6px 0}.ok{color:#1a7f37}.fail{color:#cf222e}"
".foot{display:flex;justify-content:space-between;border-top:1px solid var(--line);padding:14px 0;color:var(--muted);font-size:13px}"
".foot a{margin-left:14px;color:var(--muted);text-decoration:none}.foot a:hover{color:var(--ink)}"
"@media(max-width:800px){.wrap{grid-template-columns:1fr;gap:18px}.shell{padding:0 16px}}")))
(defun public-nav (site-name)
(public-nav site-name 'error))
(defun public-nav (site-name session)
(public-nav site-name session 'none))
(defun public-nav (site-name session page)
(list
"<header class=\"nav\"><a class=\"brand\" href=\"/\">" (escape site-name) "</a>"
"<nav class=\"nav-links\">"
(nav-link "/" "Explore" (=:= page 'explore))
(nav-link "/docs" "Docs" (=:= page 'docs))
(nav-link "/library" "Library" (=:= page 'library))
(lists:map #'nav-item-html/1 (lamina_site_mgr:nav-items))
"</nav>"
(nav-account session)
"</header>"))
(defun nav-link (href label active)
(list "<a href=\"" href "\""
(if active " class=\"active\"" "")
">" (escape label) "</a>"))
(defun nav-item-html (item)
(let ((label (maps:get 'label item ""))
(url (maps:get 'url item "")))
(list "<a href=\"" (escape url) "\""
(if (or (lists:prefix "http://" url) (lists:prefix "https://" url))
" target=\"_blank\" rel=\"noopener\""
"")
">" (escape label) "</a>")))
(defun nav-account (session)
(case session
(`#(ok ,_)
(list "<span class=\"signin\"><a href=\"/admin\">Dashboard</a>"
"<a href=\"/logout\">Logout</a></span>"))
(_
"<a class=\"signin\" href=\"/login\">Sign in</a>")))
(defun public-tagline (tagline)
(list "<div class=\"tagline\">" (escape tagline) "</div>"))
(defun public-footer (site)
(let ((text (footer-text site))
(terms (maps:get 'footer_terms_url site ""))
(privacy (maps:get 'footer_privacy_url site ""))
(contact (maps:get 'footer_contact_url site "")))
(list "<footer class=\"foot\"><span>" (escape text) "</span><nav>"
(footer-link terms "Terms")
(footer-link privacy "Privacy")
(footer-link contact "Contact")
"</nav></footer>")))
(defun footer-text (site)
(let ((text (to-str (maps:get 'footer_text site "")))
(name (to-str (maps:get 'site_name site "lamina"))))
(if (=:= "" text)
(lists:flatten (list name " — simple code hosting for small teams."))
text)))
(defun footer-link (url label)
(if (=:= "" (to-str url))
""
(list "<a href=\"" (escape url) "\">" label "</a>")))
(defun public-head (title)
(public-head title "shell"))
(defun public-head (title shell-class)
(list "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"utf-8\">"
"<meta name=\"color-scheme\" content=\"light only\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
"<title>" (escape title) "</title><style>" (public-css) "</style></head><body><div class=\""
shell-class "\">"))
(defun clone-box (command)
(list "<pre class=\"clone\">" (escape command) "</pre>"))
(defun clone-command (root path)
(lists:flatten
(list "fossil clone " (trim-right-slash (to-str root)) (to-str path))))
(defun greeting (username)
(let* ((now (calendar:local_time))
(hour (element 1 (element 2 now)))
(hello (cond
((< hour 12) "Good morning")
((< hour 18) "Good afternoon")
('true "Good evening"))))
(lists:flatten (list hello ", " (to-str username)))))
(defun today-label ()
(let* ((date (element 1 (calendar:local_time)))
(y (element 1 date))
(m (element 2 date))
(d (element 3 date)))
(lists:flatten
(io_lib:format "~s, ~s ~p, ~p"
(list (weekday date) (month-name m) d y)))))
(defun weekday (date)
(case (calendar:day_of_the_week date)
(1 "Monday") (2 "Tuesday") (3 "Wednesday") (4 "Thursday")
(5 "Friday") (6 "Saturday") (7 "Sunday")
(_ "Today")))
(defun month-name (m)
(case m
(1 "Jan") (2 "Feb") (3 "Mar") (4 "Apr") (5 "May") (6 "Jun")
(7 "Jul") (8 "Aug") (9 "Sep") (10 "Oct") (11 "Nov") (12 "Dec")
(_ "")))
(defun relative-time (stamp)
(let ((s (to-str stamp)))
(if (=:= "" s)
"No activity yet"
(case (catch (parse-stamp s))
(`#(EXIT ,_) s)
(then
(let ((diff (- (os:system_time 'second) then)))
(cond
((< diff 0) s)
((< diff 90) "just now")
((< diff 3600) (lists:flatten (io_lib:format "~p minutes ago" (list (div diff 60)))))
((< diff 86400) (lists:flatten (io_lib:format "~p hours ago" (list (div diff 3600)))))
((< diff 604800) (lists:flatten (io_lib:format "~p days ago" (list (div diff 86400)))))
((< diff 2419200) (lists:flatten (io_lib:format "~p weeks ago" (list (div diff 604800)))))
('true s))))))))
(defun parse-stamp (s)
(let ((normalized (string:replace (string:replace s " " "T" 'all) " " "T" 'leading)))
(let ((candidate (if (lists:member 84 normalized)
(if (or (lists:member 90 normalized) (lists:member 43 normalized))
normalized
(lists:flatten (list normalized "Z")))
s)))
(calendar:rfc3339_to_system_time candidate (maps:from_list (list (tuple 'unit 'second)))))))
(defun icon (name)
(case name
("repo" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5v-9Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM4.5 11.5a1 1 0 1 0 0 2h8.75a.75.75 0 0 0 0-1.5H4.5Z\"/></svg>")
("org" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M1.75 0A1.75 1.75 0 0 0 0 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0 0 16 14.25V1.75A1.75 1.75 0 0 0 14.25 0ZM1.5 1.75a.25.25 0 0 1 .25-.25h12.5a.25.25 0 0 1 .25.25V4.5h-13ZM1.5 6h4v9h-3.75a.25.25 0 0 1-.25-.25Zm5.5 9V6h7.5v8.75a.25.25 0 0 1-.25.25Z\"/></svg>")
("disk" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M2.5 1.75v11.5c0 .192.168.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.56L10.94 1.5H2.75a.25.25 0 0 0-.25.25ZM1 1.75C1 .784 1.784 0 2.75 0h8.69c.464 0 .91.184 1.237.513l2.81 2.81A1.75 1.75 0 0 1 16 4.56v8.69A1.75 1.75 0 0 1 14.25 15H2.75A1.75 1.75 0 0 1 1 13.25ZM4.75 13h6.5a.75.75 0 0 0 0-1.5h-6.5a.75.75 0 0 0 0 1.5ZM4 6.25a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 0 1.5h-6.5A.75.75 0 0 1 4 6.25Z\"/></svg>")
("clock" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm7-3.25v3.02l2.14 1.27a.75.75 0 1 1-.76 1.3L7.03 8.73a.75.75 0 0 1-.28-.52V4.75a.75.75 0 0 1 1.5 0Z\"/></svg>")
("plus" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z\"/></svg>")
("gear" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M8 0a1.5 1.5 0 0 1 1.48 1.22l.12.58a5.5 5.5 0 0 1 1.3.75l.52-.28a1.5 1.5 0 0 1 1.95.4l1 1.73a1.5 1.5 0 0 1-.27 1.9l-.45.4c.06.34.1.7.1 1.06 0 .37-.04.72-.1 1.07l.45.39a1.5 1.5 0 0 1 .27 1.9l-1 1.73a1.5 1.5 0 0 1-1.95.4l-.52-.28a5.5 5.5 0 0 1-1.3.75l-.12.58A1.5 1.5 0 0 1 8 16a1.5 1.5 0 0 1-1.48-1.22l-.12-.58a5.5 5.5 0 0 1-1.3-.75l-.52.28a1.5 1.5 0 0 1-1.95-.4l-1-1.73a1.5 1.5 0 0 1 .27-1.9l.45-.39A6.5 6.5 0 0 1 2 8c0-.36.04-.72.1-1.06l-.45-.4a1.5 1.5 0 0 1-.27-1.9l1-1.73a1.5 1.5 0 0 1 1.95-.4l.52.28a5.5 5.5 0 0 1 1.3-.75l.12-.58A1.5 1.5 0 0 1 8 0Zm0 5a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z\"/></svg>")
("compass" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M8.5.75a.75.75 0 0 0-1.5 0v5.19L3.47 2.47a.75.75 0 0 0-1.06 1.06L5.94 7H.75a.75.75 0 0 0 0 1.5h5.19l-3.53 3.53a.75.75 0 1 0 1.06 1.06L7 9.56v5.69a.75.75 0 0 0 1.5 0V9.56l3.53 3.53a.75.75 0 1 0 1.06-1.06L9.56 8.5h5.69a.75.75 0 0 0 0-1.5H9.56l3.53-3.53a.75.75 0 1 0-1.06-1.06L8.5 5.94Z\"/></svg>")
("home" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M6.906.64a1.75 1.75 0 0 1 2.187 0l5.25 4.2c.415.332.657.835.657 1.367v7.04A1.75 1.75 0 0 1 13.25 15h-3.5a.75.75 0 0 1-.75-.75V9H7v5.25a.75.75 0 0 1-.75.75h-3.5A1.75 1.75 0 0 1 1 13.25V6.207c0-.532.242-1.035.657-1.366Z\"/></svg>")
("book" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M0 1.75A.75.75 0 0 1 .75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0 1 11.006 1h4.245a.75.75 0 0 1 .75.75v10.5a.75.75 0 0 1-.75.75h-4.507a2.25 2.25 0 0 0-1.591.659l-.622.621a.75.75 0 0 1-1.06 0l-.622-.621A2.25 2.25 0 0 0 5.258 13.5H.75a.75.75 0 0 1-.75-.75Zm7.251 10.324.004-5.073-.002-2.253A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.757a3.75 3.75 0 0 1 1.994.574ZM8.755 4.75l-.004 7.322a3.752 3.752 0 0 1 1.992-.572H14.5v-9h-3.495a2.25 2.25 0 0 0-2.25 2.25Z\"/></svg>")
("library" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M1.75 1.5h3.5A.75.75 0 0 1 6 2.25v11.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75V2.25a.75.75 0 0 1 .75-.75Zm5 2h3.5a.75.75 0 0 1 .75.75v9.5a.75.75 0 0 1-.75.75h-3.5a.75.75 0 0 1-.75-.75V4.25a.75.75 0 0 1 .75-.75Zm5-1h2.5A.75.75 0 0 1 15 3.25v10.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1-.75-.75V3.25a.75.75 0 0 1 .75-.75Z\"/></svg>")
("help" "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" aria-hidden=\"true\"><path fill=\"currentColor\" d=\"M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.75.75 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4c.763 0 1.337.182 1.72.407.385.226.673.55.836.973.168.43.188.956-.056 1.496-.256.557-.699.943-1.156 1.168-.455.222-.858.254-1.098.254a.75.75 0 0 1 0-1.5c.032 0 .217-.03.47-.154.255-.125.425-.33.514-.546.082-.2.07-.313.05-.364C9.22 5.654 9.1 5.5 8 5.5c-.369 0-.595.09-.74.187a1.05 1.05 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z\"/></svg>")
(_ "")))
(defun initials (name)
(let ((s (to-str name)))
(cond
((=:= "" s) "??")
((> (length s) 1) (upper-two (lists:sublist s 1 2)))
('true (upper-two s)))))
(defun upper-two (chars)
(lists:map
(lambda (c)
(if (and (>= c 97) (=< c 122)) (- c 32) c))
chars))
(defun html (code body)
(let ((b (if (is_binary body) body (list_to_binary (lists:flatten body)))))
`#(status ,code #M(#"content-type" #"text/html; charset=utf-8") ,b)))
(defun trim-right-slash (value)
(let ((s (to-str value)))
(if (and (> (length s) 0) (=:= (lists:last s) 47))
(trim-right-slash (lists:sublist s 1 (- (length s) 1)))
s)))
(defun to-str (value)
(cond
((is_binary value) (binary_to_list value))
((is_list value) value)
((or (=:= 'undefined value) (=:= 'null value)) "")
('true (lists:flatten (io_lib:format "~p" (list value))))))
(defun escape (value)
(let ((s (to-str value)))
(string:replace
(string:replace
(string:replace
(string:replace
(string:replace s "&" "&" 'all)
"<" "<" 'all)
">" ">" 'all)
"\"" """ 'all)
"'" "'" 'all)))