diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index 5a2ab8610..ba2bcc827 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -1,5 +1,6 @@ /* Igneum Miner dashboard. The site's tokens (site/index.html): obsidian, graphite, ember, molten, bone, ash; - Unbounded for headings, IBM Plex Sans for text, IBM Plex Mono for numbers and labels. Fonts ship in the binary. */ + Unbounded for headings, IBM Plex Sans for text, IBM Plex Mono for numbers and labels. Fonts ship in the binary. + One type scale (--t-*), one spacing scale (--s-*), one colour set (:root), used by every screen. */ @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexMono-400.woff2) format('woff2')} @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/IBMPlexMono-500.woff2) format('woff2')} @font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexSans-400.woff2) format('woff2')} @@ -9,53 +10,66 @@ @font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/Unbounded-700.woff2) format('woff2')} @font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(fonts/Unbounded-900.woff2) format('woff2')} -:root{--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E; ---gutter:28px;--card-pad:22px;--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:20px;--gap-tile:12px; ---sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--head:'Unbounded',sans-serif; ---top:60px;--bottom:52px} +:root{ + /* colour */ + --obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--ember-hi:#FF6A2B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E; + --ember-12:rgba(242,84,27,.12);--ember-40:rgba(242,84,27,.4);--molten-10:rgba(255,179,92,.1);--molten-40:rgba(255,179,92,.4);--node-blue:#7FA7C9;--nvidia:#8BE37A; + /* type scale */ + --t-xs:11px;--t-sm:12px;--t-base:13px;--t-md:14px;--t-lg:15px;--t-xl:16px;--t-2xl:18px;--t-num:26px;--t-h3:16px;--t-h2:32px;--t-h1:52px; + /* spacing scale */ + --s-1:4px;--s-2:8px;--s-3:12px;--s-4:16px;--s-5:20px;--s-6:28px;--s-7:40px; + --gutter:var(--s-6);--card-pad:22px;--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:var(--s-5);--gap-tile:var(--s-3); + --sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--head:'Unbounded',sans-serif; + --top:60px;--bottom:52px;--drawer-h:260px} *{box-sizing:border-box} html,body{height:100%} -body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none} -.mono,code,pre{font-family:var(--mono)} +body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:var(--t-lg);line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums} +.mono,code,pre{font-family:var(--mono);font-variant-numeric:tabular-nums} .dim{color:var(--ash)} h1,h2,h3{font-family:var(--head);margin:0;line-height:1.1;text-wrap:balance} -h1{font-weight:900;font-size:52px;letter-spacing:-.01em} -h2{font-weight:700;font-size:32px} -h3{font-weight:700;font-size:16px} +h1{font-weight:900;font-size:var(--t-h1);letter-spacing:-.01em} +h2{font-weight:700;font-size:var(--t-h2)} +h3{font-weight:700;font-size:var(--t-h3)} p{margin:0} a{color:inherit} button{font:inherit;color:inherit} -.eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:8px} +input,textarea{font-variant-numeric:tabular-nums} +.eyebrow{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.18em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:var(--s-2);white-space:nowrap} .eyebrow.ember{color:var(--ember)} [hidden]{display:none !important} +::selection{background:rgba(242,84,27,.45)} /* buttons */ -.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:10px 20px;border-radius:10px;font-weight:600;font-size:15px;border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease;white-space:nowrap} +.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2);min-height:44px;padding:10px 20px;border-radius:10px;font-weight:600;font-size:var(--t-lg);border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease,color .15s ease;white-space:nowrap} .btn:hover{transform:translateY(-1px);border-color:var(--ash)} .btn:active{transform:none} .btn:disabled{opacity:.4;cursor:default;transform:none} .btn.primary{background:var(--ember);color:var(--ember-ink);border-color:var(--ember)} -.btn.primary:hover{background:#FF6A2B;border-color:#FF6A2B} -.btn.big{min-height:52px;padding:12px 28px;font-size:16px} -.btn.small{min-height:34px;padding:6px 14px;font-size:13px;border-radius:8px} -.btn.tiny{min-height:26px;padding:2px 10px;font-size:12px;border-radius:7px;font-family:var(--mono);font-weight:500} +.btn.primary:hover{background:var(--ember-hi);border-color:var(--ember-hi)} +.btn.big{min-height:52px;padding:12px 28px;font-size:var(--t-xl)} +.btn.small{min-height:34px;padding:6px 14px;font-size:var(--t-base);border-radius:8px} +.btn.tiny{min-height:26px;padding:2px 10px;font-size:var(--t-sm);border-radius:7px;font-family:var(--mono);font-weight:500} .btn.ghost{border-color:transparent;color:var(--ink-2)} .btn.ghost:hover{border-color:var(--line-2);color:var(--bone)} +.btn.ghost.on{color:var(--molten);border-color:var(--molten-40);background:var(--molten-10)} .btn.danger:hover{color:var(--ember);border-color:var(--ember)} +.btn .chev{transition:transform .2s ease;color:var(--ash)} +.btn.on .chev{transform:rotate(180deg);color:var(--molten)} .icon-btn{width:38px;height:38px;border-radius:10px;border:1px solid var(--line-2);background:transparent;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:var(--ink-2);font-size:20px;line-height:1} .icon-btn:hover{color:var(--bone);border-color:var(--ash)} :focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px} +@media (prefers-reduced-motion:reduce){.btn,.btn .chev{transition:none}} /* top bar */ -.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;padding:0 var(--gutter);background:rgba(12,12,14,.86);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid rgba(42,42,48,.7);z-index:20;-webkit-app-region:drag} +.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:0 var(--gutter);background:rgba(12,12,14,.86);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid rgba(42,42,48,.7);z-index:20;-webkit-app-region:drag} .top button,.top .pill{-webkit-app-region:no-drag} body.mac .top{padding-left:92px} -.brand{display:flex;align-items:center;gap:10px} +.brand{display:flex;align-items:center;gap:10px;min-width:0} .brand .word{font-family:var(--head);font-weight:900;font-size:20px;letter-spacing:.06em} -.brand .miner{font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--ash);margin-left:4px;padding-top:3px} -.top-right{display:flex;align-items:center;gap:12px} -.pill{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite)} -.pill.on{color:var(--molten);border-color:rgba(255,179,92,.35)} +.brand .miner{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.22em;color:var(--ash);margin-left:var(--s-1);padding-top:3px} +.top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto} +.pill{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;min-width:112px;justify-content:center} +.pill.on{color:var(--molten);border-color:var(--molten-40)} .pill.warn{color:var(--ember)} .dot{width:8px;height:8px;border-radius:50%;background:var(--ash);display:inline-block;flex:0 0 8px} .dot.small{width:7px;height:7px;flex-basis:7px} @@ -64,115 +78,118 @@ body.mac .top{padding-left:92px} @keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,179,92,.5)}50%{box-shadow:0 0 0 7px rgba(255,179,92,0)}} @media (prefers-reduced-motion:reduce){.on .dot,.dot.live{animation:none}} -/* update banner */ -.banner{position:fixed;top:var(--top);left:0;right:0;z-index:19;display:flex;align-items:center;justify-content:center;gap:14px;padding:10px var(--gutter);background:rgba(242,84,27,.12);border-bottom:1px solid rgba(242,84,27,.4);font-size:14px} - +/* banners under the top bar: clock, update, job */ +.banner{position:fixed;top:var(--top);left:0;right:0;z-index:19;display:flex;align-items:center;justify-content:center;gap:14px;padding:10px var(--gutter);background:var(--ember-12);border-bottom:1px solid var(--ember-40);font-size:var(--t-md);line-height:1.4} .banner.clock{background:rgba(242,84,27,.14);border-bottom-color:rgba(242,84,27,.5);flex-wrap:wrap} -.banner.clock.warn{background:rgba(255,179,92,.1);border-bottom-color:rgba(255,179,92,.4)} -.banner .hint{font-size:11px;color:var(--ash);flex-basis:100%;text-align:center} -.banner.update{flex-wrap:wrap;row-gap:8px} +.banner.clock.warn{background:var(--molten-10);border-bottom-color:var(--molten-40)} +.banner .hint{font-size:var(--t-xs);color:var(--ash);flex-basis:100%;text-align:center} +.banner.update{flex-wrap:wrap;row-gap:var(--s-2)} .banner.update.urgent{background:rgba(242,84,27,.55);border-bottom-color:var(--ember);color:#fff;font-weight:600} .banner .prog{flex-basis:100%;height:3px;background:rgba(255,255,255,.12);border-radius:2px;overflow:hidden;margin-top:-2px} .banner .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear} -/* remote job strip (src/jobrun.rs): running, then the outcome */ -.banner.job{flex-wrap:wrap;row-gap:8px;background:rgba(255,179,92,.1);border-bottom-color:rgba(255,179,92,.4)} +.banner.job{flex-wrap:wrap;row-gap:var(--s-2);background:var(--molten-10);border-bottom-color:var(--molten-40)} .banner.job.failed{background:rgba(242,84,27,.14);border-bottom-color:rgba(242,84,27,.5)} -.banner.job .job-results{flex-basis:100%;margin:0;font-size:11px;line-height:1.5;color:var(--ink-2);white-space:pre-wrap;word-break:break-word;max-height:120px;overflow:auto} -.job-history table{margin-top:8px} -.job-history th{text-align:left;font-weight:500;color:var(--ash);font-size:11px;padding:4px 6px 4px 0} +.banner.job .job-results{flex-basis:100%;margin:0;font-size:var(--t-xs);line-height:1.5;color:var(--ink-2);white-space:pre-wrap;word-break:break-word;max-height:120px;overflow:auto} +.job-history table{margin-top:var(--s-2)} +.job-history th{text-align:left;font-weight:500;color:var(--ash);font-size:var(--t-xs);padding:4px 6px 4px 0} .job-history td{padding:5px 6px 5px 0;border-top:1px solid var(--line);vertical-align:top} .job-history tr.failed td,.job-history tr.timeout td,.job-history tr.aborted td{color:var(--ember)} .job-history tr.running td{color:var(--molten)} -.clock-card{margin-top:12px;border:1px solid rgba(242,84,27,.5);background:rgba(242,84,27,.08);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px} -.clock-card.warn{border-color:rgba(255,179,92,.4);background:rgba(255,179,92,.06)} -.clock-msg{font-size:14px;color:var(--bone);line-height:1.45} +.clock-card{margin-top:var(--s-3);border:1px solid rgba(242,84,27,.5);background:rgba(242,84,27,.08);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)} +.clock-card.warn{border-color:var(--molten-40);background:rgba(255,179,92,.06)} +.clock-msg{font-size:var(--t-md);color:var(--bone);line-height:1.45} .clock-card .note{margin-top:0} /* screens */ -main{position:absolute;top:var(--top);bottom:0;left:0;right:0;overflow:auto;padding:0 var(--gutter)} +main{position:absolute;top:var(--top);bottom:0;left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain} body.has-bottom main{bottom:var(--bottom)} -body.drawer-open main{bottom:calc(var(--bottom) + 260px)} +body.drawer-open main{bottom:calc(var(--bottom) + var(--drawer-h))} .screen{display:none;max-width:1080px;margin:0 auto;animation:rise .45s ease} body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-cards,body[data-phase="address"] #screen-address,body[data-phase="dashboard"] #screen-dashboard{display:block} @keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} @media (prefers-reduced-motion:reduce){.screen{animation:none}} /* welcome */ -.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:18px;padding:40px 0 48px} +.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--s-4);padding:var(--s-7) 0 48px} .coin-wrap{position:relative;width:148px;height:148px;margin-bottom:6px} .coin-wrap::before{content:"";position:absolute;inset:-40px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.28) 0,rgba(242,84,27,0) 65%);animation:breathe 4s ease-in-out infinite} .coin{position:relative;display:block;border-radius:50%;filter:drop-shadow(0 10px 30px rgba(242,84,27,.35))} @keyframes breathe{0%,100%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}} -.lead{font-size:18px;color:var(--ink-2);max-width:54ch} +@media (prefers-reduced-motion:reduce){.coin-wrap::before{animation:none}} +.lead{font-size:var(--t-2xl);color:var(--ink-2);max-width:54ch} .three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap-tile);width:100%;max-width:860px;margin-top:10px;text-align:left} .tile{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);display:flex;flex-direction:column;gap:6px;min-width:0} -.tile .k{font-family:var(--mono);font-size:11px;letter-spacing:.14em;color:var(--ember)} -.tile .t{font-family:var(--head);font-weight:700;font-size:15px;line-height:1.25} -.tile .s{font-size:13px;color:var(--ash);line-height:1.45} -.cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;justify-content:center;margin-top:10px} -.seedline{font-size:12px;color:var(--ash);letter-spacing:.04em} +.tile .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;color:var(--ember)} +.tile .t{font-family:var(--head);font-weight:700;font-size:var(--t-lg);line-height:1.25} +.tile .s{font-size:var(--t-base);color:var(--ash);line-height:1.45} +.tile .h{font-family:var(--head);font-weight:700;font-size:var(--t-h3);margin-bottom:var(--s-2)} +.cta{display:flex;flex-wrap:wrap;gap:var(--s-3);align-items:center;justify-content:center;margin-top:10px} +.seedline{font-size:var(--t-sm);color:var(--ash);letter-spacing:.04em} /* steps */ -.step{max-width:720px;margin:0 auto;padding:56px 0 48px;display:flex;flex-direction:column;gap:16px} -.step .sub{font-size:16px;color:var(--ink-2);max-width:60ch} -.step .cta{justify-content:flex-start;margin-top:8px} -.note{font-size:13px;color:var(--ash);line-height:1.5} -.note.small{font-size:12px;word-break:break-all} -.cards{display:flex;flex-direction:column;gap:12px;margin-top:8px} +.step{max-width:720px;margin:0 auto;padding:56px 0 48px;display:flex;flex-direction:column;gap:var(--s-4)} +.step .sub{font-size:var(--t-xl);color:var(--ink-2);max-width:60ch} +.step .cta{justify-content:flex-start;margin-top:var(--s-2)} +.note{font-size:var(--t-base);color:var(--ash);line-height:1.5} +.note.small{font-size:var(--t-sm);word-break:break-all} +.cards{display:flex;flex-direction:column;gap:var(--s-3);margin-top:var(--s-2)} .card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0} .card.detecting{display:flex;align-items:center;gap:18px} -.card.detecting .t{font-family:var(--head);font-weight:700;font-size:16px} -.card.detecting .s{font-size:12px;color:var(--ash);margin-top:4px} +.card.detecting .t{font-family:var(--head);font-weight:700;font-size:var(--t-xl)} +.card.detecting .s{font-size:var(--t-sm);color:var(--ash);margin-top:var(--s-1)} .spinner{width:28px;height:28px;border-radius:50%;border:3px solid var(--line-2);border-top-color:var(--ember);animation:spin 1s linear infinite;flex:0 0 28px} @keyframes spin{to{transform:rotate(360deg)}} .gpu{display:flex;align-items:center;gap:18px} -.gpu .badge{width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;letter-spacing:.08em;flex:0 0 52px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} +.gpu .badge{width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 52px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} .gpu .badge.apple{color:var(--bone)} -.gpu .badge.nvidia{color:#8BE37A} +.gpu .badge.nvidia{color:var(--nvidia)} .gpu .badge.amd{color:var(--ember)} -.gpu .name{font-family:var(--head);font-weight:700;font-size:18px;line-height:1.2} -.gpu .meta{font-family:var(--mono);font-size:12px;color:var(--ash);margin-top:5px;display:flex;flex-wrap:wrap;gap:6px 14px} +.gpu .name{font-family:var(--head);font-weight:700;font-size:var(--t-2xl);line-height:1.2} +.gpu .meta{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);margin-top:5px;display:flex;flex-wrap:wrap;gap:6px 14px} .gpu .meta b{color:var(--ink-2);font-weight:500} .gpu .tick{margin-left:auto;width:36px;height:36px;border-radius:50%;background:var(--ember);display:flex;align-items:center;justify-content:center;flex:0 0 36px} .gpu.off .tick{background:var(--line-2)} .gpu .tick svg path{stroke-dasharray:30;stroke-dashoffset:30;animation:draw .8s .2s ease forwards} @keyframes draw{to{stroke-dashoffset:0}} -.gpu .msg{font-size:12px;color:var(--ember);margin-top:4px} +.gpu .msg{font-size:var(--t-sm);color:var(--ember);margin-top:var(--s-1)} /* GPU rows (first run and settings) */ -.gpu-row{display:flex;align-items:center;gap:16px;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0} +.gpu-row{display:flex;align-items:center;gap:var(--s-4);background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0;flex-wrap:wrap} .gpu-row.off{opacity:.72} -.gpu-row .badge{width:48px;height:48px;border-radius:13px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;letter-spacing:.08em;flex:0 0 48px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} +.gpu-row .badge{width:48px;height:48px;border-radius:13px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;flex:0 0 48px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} .gpu-row .badge.apple{color:var(--bone)} -.gpu-row .badge.nvidia{color:#8BE37A} +.gpu-row .badge.nvidia{color:var(--nvidia)} .gpu-row .badge.amd{color:var(--ember)} -.gpu-row .info{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px} -.gpu-row .name{font-family:var(--head);font-weight:700;font-size:16px;line-height:1.2;display:flex;align-items:center;gap:10px;flex-wrap:wrap} -.kind{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;border-radius:999px;padding:2px 8px;border:1px solid var(--line-2);color:var(--ash);font-weight:500} -.kind.discrete,.kind.apple{color:var(--molten);border-color:rgba(255,179,92,.4)} +.gpu-row .info{flex:1;min-width:180px;display:flex;flex-direction:column;gap:var(--s-1)} +.gpu-row .name{font-family:var(--head);font-weight:700;font-size:var(--t-xl);line-height:1.2;display:flex;align-items:center;gap:10px;flex-wrap:wrap} +.kind{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;border-radius:999px;padding:2px 8px;border:1px solid var(--line-2);color:var(--ash);font-weight:500;white-space:nowrap} +.kind.discrete,.kind.apple{color:var(--molten);border-color:var(--molten-40)} .kind.external{color:var(--bone)} -.gpu-row .meta{font-family:var(--mono);font-size:12px;color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 14px} +.gpu-row .meta{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 14px} .gpu-row .meta b{color:var(--ink-2);font-weight:500} -.gpu-row .reason{font-size:12px;color:var(--ash)} -.gpu-row .msg{font-size:12px;color:var(--ember)} +.gpu-row .reason{font-size:var(--t-sm);color:var(--ash)} +.gpu-row .msg{font-size:var(--t-sm);color:var(--ember)} .ids{display:flex;align-items:center;gap:6px;flex:0 0 auto} -.ids .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);margin-right:4px} +.ids .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);margin-right:var(--s-1)} .ids button{width:30px;height:30px;border-radius:8px;border:1px solid var(--line-2);background:transparent;color:var(--bone);cursor:pointer;font-size:16px;line-height:1} .ids button:hover{border-color:var(--ash)} -.ids input{width:44px;text-align:center;background:var(--obsidian);border:1px solid var(--line-2);border-radius:8px;padding:5px 4px;color:var(--bone);font-family:var(--mono);font-size:13px;user-select:text;-webkit-user-select:text} +.ids input{width:44px;text-align:center;background:var(--obsidian);border:1px solid var(--line-2);border-radius:8px;padding:5px 4px;color:var(--bone);font-family:var(--mono);font-size:var(--t-base);user-select:text;-webkit-user-select:text} .ids input:focus{outline:none;border-color:var(--ember)} .ids input::-webkit-inner-spin-button,.ids input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} .gpu-row.off .ids{opacity:.4;pointer-events:none} .gpu-row .switch{padding:0;flex:0 0 auto} -.cards.compact .gpu-row{padding:12px 14px;gap:12px;border-radius:14px} +.cards.compact .gpu-row{padding:12px 14px;gap:var(--s-3);border-radius:14px} .cards.compact .gpu-row .badge{width:38px;height:38px;flex-basis:38px;border-radius:10px} -.cards.compact .gpu-row .name{font-size:14px} +.cards.compact .gpu-row .name{font-size:var(--t-md)} +.cards.compact .gpu-row .info{flex-basis:calc(100% - 50px);min-width:120px} +.cards.compact .gpu-row .power{margin-left:50px} +.cards.compact .gpu-row .switch{margin-left:auto} .cards.compact .ids .k{display:none} -.power{display:flex;align-items:center;gap:8px;flex:0 0 auto} +.power{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto} .power .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} .power input[type=range]{width:110px;accent-color:var(--ember)} -.power .pv{font-size:12px;color:var(--ink-2);min-width:84px} +.power .pv{font-size:var(--t-sm);color:var(--ink-2);min-width:84px} .cards.compact .power .k{display:none} .cards.compact .power input[type=range]{width:80px} .gpu-tile .m.tele .warm,.gpu-tile .m.tele .warm b{color:var(--molten)} @@ -186,22 +203,22 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card /* per-card tiles on the dashboard */ .gpu-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--gap-tile)} .gpu-tile{background:var(--obsidian);border:1px solid var(--line);border-radius:var(--tile-r);padding:14px 16px;display:flex;flex-direction:column;gap:6px;min-width:0} -.gpu-tile .n{font-family:var(--head);font-weight:700;font-size:14px;line-height:1.25;display:flex;align-items:center;gap:8px;flex-wrap:wrap} +.gpu-tile .n{font-family:var(--head);font-weight:700;font-size:var(--t-md);line-height:1.25;display:flex;align-items:center;gap:var(--s-2);flex-wrap:wrap} .gpu-tile .h{font-family:var(--head);font-weight:700;font-size:24px;color:var(--ember);line-height:1.1;display:flex;align-items:baseline;gap:6px;font-variant-numeric:tabular-nums} -.gpu-tile .h .unit{font-family:var(--mono);font-size:11px;color:var(--ash);font-weight:500;letter-spacing:.08em} +.gpu-tile .h .unit{font-family:var(--mono);font-size:var(--t-xs);color:var(--ash);font-weight:500;letter-spacing:.08em} .gpu-tile.idle .h{color:var(--ash)} -.gpu-tile .m{font-family:var(--mono);font-size:12px;color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 12px} +.gpu-tile .m{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 12px} .gpu-tile .m b{color:var(--ink-2);font-weight:500} -.gpu-tile .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--ash)} +.gpu-tile .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash)} .gpu-tile .st.mining{color:var(--molten)} .gpu-tile .st.bad{color:var(--ember)} .gpu-tile .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block} -.gpu-tile .msg{font-size:12px;color:var(--ash)} -.gpu-off{margin-top:12px;font-size:12px;color:var(--ash)} +.gpu-tile .msg{font-size:var(--t-sm);color:var(--ash)} +.gpu-off{margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ash)} /* address options */ -.options{display:flex;flex-direction:column;gap:12px;margin-top:6px} -.option{display:flex;gap:16px;align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease} +.options{display:flex;flex-direction:column;gap:var(--s-3);margin-top:6px} +.option{display:flex;gap:var(--s-4);align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease} .option:hover{border-color:var(--line-2)} .option.on{border-color:var(--ember);background:#1A1614} .option input[type=radio]{position:absolute;opacity:0;width:0;height:0} @@ -209,61 +226,63 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .option.on .radio{border-color:var(--ember)} .option.on .radio::after{content:"";position:absolute;inset:4px;border-radius:50%;background:var(--ember)} .option .body{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1} -.option .t{font-family:var(--head);font-weight:700;font-size:16px;display:flex;align-items:center;gap:10px} -.option .s{font-size:14px;color:var(--ash);line-height:1.5} -.tag{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--molten);border:1px solid rgba(255,179,92,.4);border-radius:999px;padding:2px 8px} -.addr-input{width:100%;margin-top:8px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;color:var(--bone);font-size:14px;letter-spacing:.02em;user-select:text;-webkit-user-select:text} +.option .t{font-family:var(--head);font-weight:700;font-size:var(--t-xl);display:flex;align-items:center;gap:10px;flex-wrap:wrap} +.option .s{font-size:var(--t-md);color:var(--ash);line-height:1.5} +.tag{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--molten);border:1px solid var(--molten-40);border-radius:999px;padding:2px 8px} +.addr-input{width:100%;margin-top:var(--s-2);background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;color:var(--bone);font-size:var(--t-md);letter-spacing:.02em;user-select:text;-webkit-user-select:text} .addr-input:focus{outline:none;border-color:var(--ember)} .option:not(.on) .addr-input{display:none} -.err{font-size:13px;color:var(--ember)} +.err{font-size:var(--t-base);color:var(--ember)} /* dashboard */ #screen-dashboard{padding:22px 0 28px;display:none;flex-direction:column;gap:var(--gap)} body[data-phase="dashboard"] #screen-dashboard{display:flex} .strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap-tile)} .cell{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);display:flex;flex-direction:column;gap:6px;min-width:0} -.cell .k{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} -.cell .v{font-family:var(--head);font-weight:700;font-size:26px;line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:baseline;gap:8px} +.cell .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash);white-space:nowrap} +.cell .v{font-family:var(--head);font-weight:700;font-size:var(--t-num);line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:baseline;gap:var(--s-2);min-height:1.1em} .cell.ember .v{color:var(--ember)} -.cell .v .unit{font-family:var(--mono);font-size:12px;color:var(--ash);font-weight:500;letter-spacing:.08em} +.cell .v .unit{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);font-weight:500;letter-spacing:.08em} .cell .v.state{text-transform:capitalize;font-size:22px} -.cell .s{font-family:var(--mono);font-size:12px;color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.cell .s{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .cell.ok .v.state{color:var(--molten)} .cell.bad .v.state{color:var(--ember)} +.cell.bad .s{color:var(--ember)} .grid{display:grid;grid-template-columns:1.35fr .85fr;gap:var(--gap);align-items:start} .col-main,.col-side{display:flex;flex-direction:column;gap:var(--gap);min-width:0} -.card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;flex-wrap:wrap} -.stats{display:flex;flex-wrap:wrap;gap:12px 16px;font-size:12px;color:var(--ash)} +.card-head{display:flex;justify-content:space-between;align-items:center;gap:var(--s-3);margin-bottom:var(--s-3);flex-wrap:wrap} +.stats{display:flex;flex-wrap:wrap;gap:12px 16px;font-size:var(--t-sm);color:var(--ash)} .stats b{color:var(--bone);font-weight:500;font-variant-numeric:tabular-nums} #dag{display:block;width:100%;height:190px;border-radius:12px;background:var(--obsidian)} -.legend{display:flex;flex-wrap:wrap;gap:14px 18px;margin-top:12px;font-size:12px;color:var(--ink-2)} -.legend span{display:inline-flex;align-items:center;gap:8px} +.legend{display:flex;flex-wrap:wrap;gap:14px 18px;margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ink-2)} +.legend span{display:inline-flex;align-items:center;gap:var(--s-2)} .sw{width:12px;height:12px;border-radius:3px;display:inline-block;border:1px solid var(--line-2)} .sw.ember{background:var(--ember);border-color:var(--ember)} .sw.glow{border-color:var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)} .sw.line{width:18px;height:0;border:0;border-top:1px dashed var(--line-2);border-radius:0} .tbl{overflow-x:auto} -table{border-collapse:collapse;width:100%;font-size:13px} +table{border-collapse:collapse;width:100%;font-size:var(--t-base)} th,td{padding:9px 8px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap} th{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);font-weight:500} td.n,th.n{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--mono)} tr:last-child td{border-bottom:0} -td .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--ash)} +td .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash)} td .st.mining{color:var(--molten)} td .st.bad{color:var(--ember)} td .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block} -td .sub{display:block;font-family:var(--mono);font-size:11px;color:var(--ash);white-space:normal;max-width:280px} -.empty{color:var(--ash);font-size:13px;padding:10px 0} +td .sub{display:block;font-family:var(--mono);font-size:var(--t-xs);color:var(--ash);white-space:normal;max-width:280px} +.empty{color:var(--ash);font-size:var(--t-base);padding:10px 0} .kv{display:flex;flex-direction:column} -.kv>div{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:7px 0;border-bottom:1px solid var(--line)} +.kv>div{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s-3);padding:7px 0;border-bottom:1px solid var(--line)} .kv>div:last-child{border-bottom:0} -.kv .k{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ash)} -.kv .v{font-size:14px;font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.kv .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.1em;text-transform:uppercase;color:var(--ash);white-space:nowrap} +.kv .v{font-size:var(--t-md);font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right} .card .note{margin-top:10px} -.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:12px;color:var(--ink-2);max-height:300px;overflow:auto} +.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);max-height:300px;overflow:auto} .feed>div{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:8px 0;align-items:baseline} .feed>div:last-child{border-bottom:0} -.feed .t{color:var(--ash);flex:0 0 auto;font-size:11px} +.feed>div>span:first-child{min-width:0} +.feed .t{color:var(--ash);flex:0 0 auto;font-size:var(--t-xs)} .feed .k{color:var(--molten);margin-right:6px} .feed .k.error{color:var(--ember)} .feed .k.block{color:var(--ember)} @@ -271,61 +290,128 @@ td .sub{display:block;font-family:var(--mono);font-size:11px;color:var(--ash);wh /* sheet (the key) */ .sheet-wrap,.panel-wrap{position:fixed;inset:0;z-index:30;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px} -.sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px rgba(0,0,0,.6);animation:rise .3s ease} -.sheet .sub{font-size:15px;color:var(--ink-2)} -.field{display:flex;flex-direction:column;gap:8px;margin-top:6px} -.field .k{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} -.box{display:flex;align-items:center;gap:10px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;font-size:13px;word-break:break-all;user-select:text;-webkit-user-select:text} +.sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;max-height:calc(100vh - 48px);overflow:auto;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px rgba(0,0,0,.6);animation:rise .3s ease} +.sheet .sub{font-size:var(--t-lg);color:var(--ink-2)} +.field{display:flex;flex-direction:column;gap:var(--s-2);margin-top:6px} +.field .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} +.box{display:flex;align-items:center;gap:10px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;font-size:var(--t-base);word-break:break-all;user-select:text;-webkit-user-select:text} .box span{flex:1;min-width:0} .box.key{color:var(--molten)} -.check{display:flex;align-items:center;gap:10px;font-size:14px;cursor:pointer;margin-top:4px} -.check.small{font-size:12px;color:var(--ash)} -.check input{width:18px;height:18px;accent-color:var(--ember)} +.check{display:flex;align-items:center;gap:10px;font-size:var(--t-md);cursor:pointer;margin-top:var(--s-1)} +.check.small{font-size:var(--t-sm);color:var(--ash);margin-top:0} +.check input{width:18px;height:18px;accent-color:var(--ember);margin:0} +.check.small input{width:15px;height:15px} .sheet .cta{justify-content:flex-start} /* settings panel */ .panel-wrap{justify-content:flex-end;padding:0} .panel{width:min(440px,100%);height:100%;background:var(--graphite);border-left:1px solid var(--line-2);display:flex;flex-direction:column;animation:slide .25s ease} @keyframes slide{from{transform:translateX(30px);opacity:0}to{transform:none;opacity:1}} -.panel-head{display:flex;justify-content:space-between;align-items:center;padding:18px 22px;border-bottom:1px solid var(--line)} -.panel-body{padding:14px 22px 28px;overflow:auto;display:flex;flex-direction:column;gap:18px} -.row{display:flex;gap:10px;align-items:center} +@media (prefers-reduced-motion:reduce){.panel,.sheet{animation:none}} +.panel-head{display:flex;justify-content:space-between;align-items:center;padding:18px 22px;border-bottom:1px solid var(--line);flex:0 0 auto} +.panel-body{padding:14px 22px 28px;overflow:auto;display:flex;flex-direction:column;gap:18px;overscroll-behavior:contain} +.row{display:flex;gap:10px;align-items:center;min-width:0} .row.between{justify-content:space-between} -.row .addr-input{margin-top:0} -.num{width:90px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:9px 12px;color:var(--bone);font-size:14px;user-select:text;-webkit-user-select:text} +.row .addr-input{margin-top:0;min-width:0} +.num{width:90px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:9px 12px;color:var(--bone);font-size:var(--t-md);user-select:text;-webkit-user-select:text} .num:focus{outline:none;border-color:var(--ember)} -.switch{display:flex;align-items:center;gap:12px;font-size:14px;cursor:pointer;padding:6px 0} +.switch{display:flex;align-items:center;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:6px 0;line-height:1.35} .switch input{position:absolute;opacity:0;width:0;height:0} .switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease} .switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--bone);transition:transform .15s ease} .switch input:checked+.track{background:var(--ember)} .switch input:checked+.track::after{transform:translateX(18px)} +.switch input:focus-visible+.track{outline:2px solid var(--ember);outline-offset:3px} /* bottom bar */ -.bottom{position:fixed;left:0;right:0;bottom:0;height:var(--bottom);display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 var(--gutter);background:rgba(12,12,14,.92);border-top:1px solid var(--line);z-index:21} -.bottom .left,.bottom .right{display:flex;align-items:center;gap:8px} -.bottom .mid{font-size:12px;color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;flex:1} -.bottom .right .mono{font-size:12px} +.bottom{position:fixed;left:0;right:0;bottom:0;height:var(--bottom);display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);padding:0 var(--gutter);background:rgba(12,12,14,.92);border-top:1px solid var(--line);z-index:21} +.bottom .left,.bottom .right{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto} +.bottom .mid{font-size:var(--t-sm);color:var(--ash);display:flex;align-items:center;justify-content:center;gap:0;min-width:0;flex:1;overflow:hidden;white-space:nowrap} +.bottom .mid .pc{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis} +.bottom .mid .pc+.pc::before{content:"·";margin:0 7px;color:var(--line-2)} +.bottom .mid .nm{color:var(--ink-2);max-width:220px;flex-shrink:1} +.bottom .mid .ad{flex-shrink:0} +.bottom .right .mono{font-size:var(--t-sm)} +@media (max-width:1100px){.bottom .mid .up{display:none}.bottom .mid .nm{max-width:140px}.log-tools .at{display:none}.log-search{flex-basis:120px;max-width:200px}} +@media (max-width:960px){.bottom .mid .ad{display:none}} /* log drawer */ -.drawer{position:fixed;left:0;right:0;bottom:var(--bottom);height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:20;transition:height .2s ease;display:flex;flex-direction:column} -body.drawer-open .drawer{height:260px} -.drawer-head{display:flex;justify-content:space-between;align-items:center;padding:8px var(--gutter);border-bottom:1px solid var(--line);flex:0 0 auto} -.chips{display:flex;gap:6px} -.chip{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--line);border-radius:999px;padding:4px 10px;background:transparent;color:var(--ash);cursor:pointer} -.chip.on{color:var(--molten);border-color:rgba(255,179,92,.4)} -.log{margin:0;flex:1;overflow:auto;padding:10px var(--gutter);font-size:12px;line-height:1.55;color:var(--ink-2);white-space:pre-wrap;word-break:break-all;user-select:text;-webkit-user-select:text} +.drawer{position:fixed;left:0;right:0;bottom:var(--bottom);height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:20;transition:height .2s ease;display:flex;flex-direction:column;box-shadow:0 -12px 30px rgba(0,0,0,.35)} +body.drawer-open .drawer{height:var(--drawer-h)} +body.drawer-drag .drawer{transition:none} +body.drawer-drag{cursor:row-resize} +body.drawer-drag main{pointer-events:none} +@media (prefers-reduced-motion:reduce){.drawer{transition:none}} +.drawer-grip{height:10px;flex:0 0 10px;cursor:row-resize;display:flex;align-items:center;justify-content:center;touch-action:none} +.drawer-grip i{width:44px;height:3px;border-radius:2px;background:var(--line-2);transition:background .15s ease} +.drawer-grip:hover i,body.drawer-drag .drawer-grip i{background:var(--ash)} +.drawer-head{display:flex;align-items:center;gap:var(--s-3);padding:2px var(--gutter) 8px;border-bottom:1px solid var(--line);flex:0 0 auto;flex-wrap:wrap;row-gap:6px} +.chips{display:flex;gap:6px;flex-wrap:wrap} +.chip{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--line);border-radius:999px;padding:4px 10px;background:transparent;color:var(--ash);cursor:pointer;white-space:nowrap;line-height:1.3;transition:color .15s ease,border-color .15s ease} +.chip:hover{color:var(--ink-2);border-color:var(--line-2)} +.chip.on{color:var(--molten);border-color:var(--molten-40)} +.chip:disabled{opacity:.4;cursor:default} +.log-search{display:flex;align-items:center;gap:6px;background:var(--graphite);border:1px solid var(--line);border-radius:999px;padding:3px 10px 3px 10px;color:var(--ash);flex:1 1 160px;min-width:140px;max-width:300px} +.log-search:focus-within{border-color:var(--molten-40);color:var(--ink-2)} +.log-search input{flex:1;min-width:0;background:transparent;border:0;outline:0;color:var(--bone);font-family:var(--mono);font-size:var(--t-sm);padding:2px 0;user-select:text;-webkit-user-select:text} +.log-search input::-webkit-search-cancel-button{-webkit-appearance:none} +.log-search input::placeholder{color:var(--ash)} +.log-search .count{font-size:var(--t-xs);color:var(--ash);white-space:nowrap;font-variant-numeric:tabular-nums} +.log-nav{display:flex;align-items:center;gap:6px} +.log-nav input{width:84px;background:var(--graphite);border:1px solid var(--line);border-radius:999px;padding:4px 10px;color:var(--bone);font-size:var(--t-xs);letter-spacing:.06em;outline:0;user-select:text;-webkit-user-select:text;text-align:center} +.log-nav input:focus{border-color:var(--molten-40)} +.log-nav input::placeholder{color:var(--ash);letter-spacing:.04em} +.log-tools{display:flex;align-items:center;gap:var(--s-2);margin-left:auto} +.log-tools .at{font-size:var(--t-xs);min-width:64px;text-align:right} +.log-ruler{position:relative;height:16px;flex:0 0 16px;background:var(--graphite);border-bottom:1px solid var(--line);cursor:pointer;overflow:hidden} +.log-ruler.empty{cursor:default} +.log-ruler.empty>*{display:none} +.log-ruler .t0,.log-ruler .t1{position:absolute;top:0;z-index:2;font-size:9px;line-height:16px;color:var(--ash);padding:0 6px;margin:0 var(--gutter);pointer-events:none;letter-spacing:.04em;background:var(--graphite);border-radius:0 0 6px 0} +.log-ruler .t1{right:0;border-radius:0 0 0 6px} +.log-ruler .mk{position:absolute;top:4px;bottom:4px;width:2px;margin-left:-1px;background:var(--ember);opacity:.9;pointer-events:none} +.log-ruler .mk.swap{background:var(--molten)} +.log-ruler .win{position:absolute;top:0;bottom:0;background:rgba(244,241,236,.14);border-left:1px solid rgba(244,241,236,.35);border-right:1px solid rgba(244,241,236,.35);min-width:3px;pointer-events:none} +.log-ruler:hover .win{background:rgba(244,241,236,.2)} +.log-view{position:relative;flex:1;overflow:auto;overscroll-behavior:contain;user-select:text;-webkit-user-select:text;contain:strict} +.log-pad{position:relative;min-height:100%} +.log{margin:0;position:absolute;top:0;left:0;right:0;padding:6px var(--gutter);font-size:var(--t-sm);line-height:19px;color:var(--ink-2);white-space:pre;will-change:transform} +.log .ln{height:19px;overflow:hidden;text-overflow:ellipsis;white-space:pre} +.log-view.wrap .log{position:relative;padding-bottom:12px} +.log-view.wrap .log .ln{height:auto;white-space:pre-wrap;word-break:break-all;overflow:visible} .log .src{color:var(--ash)} -.log .src.node{color:#7FA7C9} -.log .src.miner1,.log .src.miner2,.log .src.miner3,.log .src.miner4{color:var(--molten)} +.log .src.node{color:var(--node-blue)} +.log .src.miner1,.log .src.miner2,.log .src.miner3,.log .src.miner4,.log .src.miner5,.log .src.miner6,.log .src.miner7,.log .src.miner8{color:var(--molten)} .log .src.app{color:var(--ember)} -.log .e{color:var(--ember)} +.log .src.watch{color:var(--ash)} +.log .e,.log .e .src{color:var(--ember)} +.log .ln.hit{background:rgba(255,179,92,.18);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)} +.log mark{background:rgba(255,179,92,.3);color:var(--bone);border-radius:2px;padding:0 1px} +.log-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--ash);font-size:var(--t-sm);padding:0 var(--gutter);text-align:center} +.log-jump{position:absolute;right:calc(var(--gutter) + 14px);bottom:14px;background:var(--graphite);border-color:var(--molten-40);color:var(--molten);box-shadow:0 8px 24px rgba(0,0,0,.5);z-index:2;gap:6px;padding:6px 12px} +.log-jump:hover{background:#1c1a18;border-color:var(--molten)} -.toast{position:fixed;left:50%;bottom:calc(var(--bottom) + 16px);transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:13px;z-index:40;box-shadow:0 10px 30px rgba(0,0,0,.5)} +.toast{position:fixed;left:50%;bottom:calc(var(--bottom) + 16px);transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:var(--t-base);z-index:40;box-shadow:0 10px 30px rgba(0,0,0,.5);max-width:min(90vw,520px);text-align:center} +body.drawer-open .toast{bottom:calc(var(--bottom) + var(--drawer-h) + 16px)} +/* narrow windows: the strip folds to two columns, the side column drops under the main one */ @media (max-width:860px){ .strip{grid-template-columns:repeat(2,minmax(0,1fr))} .grid{grid-template-columns:1fr} .three{grid-template-columns:1fr} h1{font-size:40px} } +/* short windows (under 700 px): tighter paddings, a lower hero, a smaller canvas, so the bottom bar and the drawer + always have room */ +@media (max-height:700px){ + :root{--card-pad:18px;--tile-pad:14px 16px;--gap:var(--s-4)} + #screen-dashboard{padding:16px 0 20px} + #dag{height:150px} + .hero{gap:var(--s-3);padding:var(--s-5) 0 var(--s-6)} + .coin-wrap{width:104px;height:104px} + .coin{width:104px;height:104px} + h1{font-size:40px} + .lead{font-size:var(--t-xl)} + .step{padding:32px 0 32px} + .feed{max-height:220px} + .drawer-head{padding-bottom:6px} +} diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 9c512dc45..7fe3d0508 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -11,6 +11,22 @@ if (params.get('screen')) forced = params.get('screen'); var forcedUpdate = params.get('update'); // a sample update state for screenshots: available|downloading|ready|waiting|applying|urgent|manual|error|updated if (params.get('host') === 'mac') document.body.classList.add('mac'); + // ?debug=1 prints a budget line every 5 s (ms of JS per second in the canvas, the state render and the log) and + // exposes the counters as window.__igneumPerf + var debug = params.get('debug') === '1'; + // the hosts hide the window to the menu bar or the tray: nothing draws or polls fast while document.hidden. + // ?visible=1 ignores that (screenshots from a hidden pane). + var forceVisible = params.get('visible') === '1'; + function hiddenNow() { return document.hidden && !forceVisible; } + var perf = { frames: 0, draw: 0, render: 0, log: 0, poll: 0, polls: 0, since: performance.now() }; + if (debug) { + window.__igneumPerf = perf; + setInterval(function () { + var s = (performance.now() - perf.since) / 1000; + console.log('[perf] ' + (hiddenNow() ? 'hidden' : 'visible') + ' ' + (perf.frames / s).toFixed(1) + ' fps · js ms/s: canvas ' + (perf.draw / s).toFixed(2) + ' render ' + (perf.render / s).toFixed(2) + ' log ' + (perf.log / s).toFixed(2) + ' poll ' + (perf.poll / s).toFixed(2) + ' · ' + (perf.polls / s).toFixed(2) + ' polls/s · total ' + ((perf.draw + perf.render + perf.log + perf.poll) / s / 10).toFixed(2) + '% of one core'); + perf.frames = 0; perf.draw = 0; perf.render = 0; perf.log = 0; perf.poll = 0; perf.polls = 0; perf.since = performance.now(); + }, 5000); + } // ---------- helpers ---------- function api(path, body) { @@ -26,11 +42,12 @@ function clock(t) { var d = new Date(t * 1000); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0') + ':' + String(d.getSeconds()).padStart(2, '0'); } var toastT = null; function toast(text) { var t = $('toast'); t.textContent = text; t.hidden = false; clearTimeout(toastT); toastT = setTimeout(function () { t.hidden = true; }, 2200); } - function copyText(text) { - if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { toast('Copied'); }, function () { fallbackCopy(text); }); } - else fallbackCopy(text); + function copyText(text, msg) { + msg = msg || 'Copied'; + if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function () { toast(msg); }, function () { fallbackCopy(text, msg); }); } + else fallbackCopy(text, msg); } - function fallbackCopy(text) { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); toast('Copied'); } catch (e) { toast('Select and copy it by hand'); } document.body.removeChild(ta); } + function fallbackCopy(text, msg) { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); toast(msg || 'Copied'); } catch (e) { toast('Select and copy it by hand'); } document.body.removeChild(ta); } document.addEventListener('click', function (e) { var b = e.target.closest('[data-copy]'); if (!b) return; copyText($(b.getAttribute('data-copy')).textContent); @@ -44,7 +61,7 @@ $('bottom').hidden = !dash; $('btn-settings').hidden = !dash; document.body.classList.toggle('has-bottom', dash); - if (!dash) document.body.classList.remove('drawer-open'); + if (!dash && drawerOpen()) setDrawer(false); if (p === 'cards' && !cardsSeen) { cardsSeen = true; api('api/detect', {}); } api('api/phase', { phase: p }); } @@ -157,72 +174,278 @@ if (!state) return; api(state.mining.paused ? 'api/resume' : 'api/pause', {}); }); - $('btn-logs').addEventListener('click', function () { document.body.classList.toggle('drawer-open'); if (document.body.classList.contains('drawer-open')) pollLog(); }); + $('btn-logs').addEventListener('click', function () { setDrawer(!document.body.classList.contains('drawer-open')); }); $('btn-quit').addEventListener('click', function () { if (!confirm('Quit Igneum Miner? The miner stops first, then the node.')) return; api('api/quit', {}); $('pill-text').textContent = 'stopping'; $('btn-quit').disabled = true; }); // ---------- log drawer ---------- - var logAfter = 0, logFilter = '', logLines = []; - document.querySelectorAll('.chip').forEach(function (c) { c.addEventListener('click', function () { document.querySelectorAll('.chip').forEach(function (x) { x.classList.toggle('on', x === c); }); logFilter = c.getAttribute('data-src'); renderLog(); }); }); - function renderLog() { - var el = $('log'), follow = $('log-follow').checked; - var lines = logLines.filter(function (l) { return !logFilter || l.src.indexOf(logFilter) === 0; }).slice(-600); - el.innerHTML = lines.map(function (l) { return '' + clock(l.t) + ' ' + esc(l.src.padEnd(6)) + ' ' + (l.err ? '' : '') + esc(l.text) + (l.err ? '' : ''); }).join('\n'); - if (follow) el.scrollTop = el.scrollHeight; + // logLines: everything fetched this session (newest last, capped at LOG_CAP). view: the lines after the source chip + // and the search box. The list is virtualised: rows have a fixed height (RH) and only the rows in the viewport are in + // the DOM, so 20,000 lines cost the same as 20. Wrap mode renders the whole view with wrapped lines when it is small + // enough. Polling runs only while the drawer is open and the window is visible. + var LOG_CAP = 20000, WRAP_MAX = 5000, RH = 19, OVERSCAN = 8; + var logAfter = 0, logFilter = '', logQuery = '', logLines = [], view = [], viewDirty = true, wrap = false, follow = true; + var logView = $('log-view'), logPre = $('log'), logPad = $('log-pad'); + var winStart = -1, winEnd = -1, autoScroll = false, scrollRaf = 0, rulerAt = 0, rulerCount = -1, searchT = null, logTimer = null; + var ERR_RE = /\[error\]|\berror\b|\bfailed\b|\bpanic\b|exited with code/i, SWAP_RE = /hourly program changed|program changed|seed change|hot swap|new program|exit-on-seed-change/i; + function isErr(l) { return l.err || ERR_RE.test(l.text); } + function isSwap(l) { return SWAP_RE.test(l.text); } + function drawerOpen() { return document.body.classList.contains('drawer-open'); } + function setDrawer(open) { + document.body.classList.toggle('drawer-open', open); + var b = $('btn-logs'); b.classList.toggle('on', open); b.setAttribute('aria-expanded', open ? 'true' : 'false'); + $('btn-logs-text').textContent = open ? 'Close logs' : 'Logs'; + if (open) { applyDrawerHeight(); pollLog(); scheduleLogPoll(); viewDirty = true; renderLog(true); } + else if (logTimer) { clearTimeout(logTimer); logTimer = null; } } + // height: dragged by the grip, clamped to [140, 70% of the window]; a short window gets a smaller default + var drawerH = 0; + function defaultDrawerH() { return window.innerHeight < 700 ? 180 : 260; } + function applyDrawerHeight() { + var max = Math.floor(window.innerHeight * 0.7), min = 140; + if (!drawerH) { try { drawerH = parseInt(localStorage.getItem('igneum.drawer-h'), 10) || 0; } catch (e) { } } + var h = Math.max(min, Math.min(max, drawerH || defaultDrawerH())); + document.body.style.setProperty('--drawer-h', h + 'px'); + if (drawerOpen()) renderLog(true); + } + (function () { + var grip = $('drawer-grip'), startY = 0, startH = 0; + grip.addEventListener('pointerdown', function (e) { + startY = e.clientY; startH = parseInt(getComputedStyle(document.body).getPropertyValue('--drawer-h'), 10) || defaultDrawerH(); + grip.setPointerCapture(e.pointerId); document.body.classList.add('drawer-drag'); e.preventDefault(); + }); + grip.addEventListener('pointermove', function (e) { + if (!document.body.classList.contains('drawer-drag')) return; + drawerH = startH + (startY - e.clientY); applyDrawerHeight(); + }); + function end(e) { if (!document.body.classList.contains('drawer-drag')) return; document.body.classList.remove('drawer-drag'); try { localStorage.setItem('igneum.drawer-h', String(parseInt(getComputedStyle(document.body).getPropertyValue('--drawer-h'), 10))); } catch (x) { } } + grip.addEventListener('pointerup', end); grip.addEventListener('pointercancel', end); + grip.addEventListener('dblclick', function () { drawerH = defaultDrawerH(); applyDrawerHeight(); try { localStorage.removeItem('igneum.drawer-h'); } catch (x) { } }); + })(); + window.addEventListener('resize', function () { if (drawerOpen()) applyDrawerHeight(); }); + document.querySelectorAll('#log-chips .chip').forEach(function (c) { c.addEventListener('click', function () { document.querySelectorAll('#log-chips .chip').forEach(function (x) { x.classList.toggle('on', x === c); }); logFilter = c.getAttribute('data-src'); viewDirty = true; renderLog(true); }); }); + $('log-search').addEventListener('input', function () { clearTimeout(searchT); var v = this.value; searchT = setTimeout(function () { logQuery = v.trim().toLowerCase(); viewDirty = true; renderLog(true); }, 120); }); + $('log-search').addEventListener('keydown', function (e) { if (e.key === 'Escape') { this.value = ''; logQuery = ''; viewDirty = true; renderLog(true); } }); + $('log-wrap').addEventListener('click', function () { wrap = !wrap; this.classList.toggle('on', wrap); logView.classList.toggle('wrap', wrap); renderLog(true); }); + $('log-follow').addEventListener('change', function () { setFollow(this.checked); if (follow) renderLog(true); }); + $('log-jump').addEventListener('click', function () { setFollow(true); renderLog(true); }); + $('log-copy').addEventListener('click', function () { + var r = visibleRange(), rows = view.slice(r[0], r[1]); + if (!rows.length) { toast('Nothing to copy'); return; } + copyText(rows.map(function (l) { return clock(l.t) + ' ' + l.src.padEnd(6) + ' ' + l.text; }).join('\n'), 'Copied ' + rows.length + ' line' + (rows.length === 1 ? '' : 's')); + }); + $('log-last-error').addEventListener('click', function () { jumpToLast(isErr, 'No error in the log'); }); + $('log-last-swap').addEventListener('click', function () { jumpToLast(isSwap, 'No program swap in the log yet'); }); + $('log-time').addEventListener('keydown', function (e) { if (e.key === 'Enter') { jumpToTime(this.value); } }); + $('log-time').addEventListener('change', function () { jumpToTime(this.value); }); + $('log-ruler').addEventListener('click', function (e) { + if (!view.length) return; + var r = this.getBoundingClientRect(), f = Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)); + var t0 = view[0].t, t1 = view[view.length - 1].t; + jumpToIndex(lowerBound(view, t0 + f * (t1 - t0))); + }); + function setFollow(on) { follow = on; $('log-follow').checked = on; $('log-jump').hidden = on; } + function lowerBound(arr, t) { var lo = 0, hi = arr.length; while (lo < hi) { var mid = (lo + hi) >> 1; if (arr[mid].t < t) lo = mid + 1; else hi = mid; } return lo; } + function jumpToTime(v) { + var m = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec((v || '').trim()); + if (!m) { toast('Type a time as HH:MM:SS'); return; } + if (!view.length) { toast('Nothing in the log'); return; } + var last = new Date(view[view.length - 1].t * 1000), d = new Date(last.getTime()); + d.setHours(+m[1], +m[2], +(m[3] || 0), 0); + if (d.getTime() > last.getTime() + 1000) d.setDate(d.getDate() - 1); // a time later than the newest line means yesterday + var target = d.getTime() / 1000; + if (target < view[0].t - 1) { toast('The log starts at ' + clock(view[0].t)); return; } + jumpToIndex(lowerBound(view, target)); + } + function jumpToLast(test, missing) { + for (var i = view.length - 1; i >= 0; i--) if (test(view[i])) { jumpToIndex(i); return; } + toast(missing); + } + var hitSeq = 0, hitT = 0; + function jumpToIndex(i) { + if (!view.length) return; + i = Math.max(0, Math.min(view.length - 1, i)); + setFollow(false); + hitSeq = view[i].seq; hitT = performance.now(); + autoScroll = true; + logView.scrollTop = Math.max(0, rowTop(i) - Math.floor(logView.clientHeight / 3)); + renderLog(true); + setTimeout(function () { hitSeq = 0; renderLog(true); }, 1600); + } + function rowTop(i) { + if (!wrap) return i * RH; + var kids = logPre.children; if (i < kids.length) return kids[i].offsetTop; return logPre.scrollHeight; + } + function visibleRange() { + if (!view.length) return [0, 0]; + if (wrap) { // find by offsetTop (the rows are all in the DOM) + var kids = logPre.children, top = logView.scrollTop, bottom = top + logView.clientHeight, a = -1, b = kids.length; + for (var k = 0; k < kids.length; k++) { var y = kids[k].offsetTop; if (a < 0 && y + kids[k].offsetHeight > top) a = k; if (y >= bottom) { b = k; break; } } + return [Math.max(0, a), b]; + } + var s = Math.floor(logView.scrollTop / RH), n = Math.ceil(logView.clientHeight / RH) + 1; + return [Math.max(0, s), Math.min(view.length, s + n)]; + } + function rebuildView() { + var q = logQuery; + view = logLines.filter(function (l) { return (!logFilter || l.src.indexOf(logFilter) === 0) && (!q || l.text.toLowerCase().indexOf(q) >= 0 || l.src.indexOf(q) >= 0); }); + viewDirty = false; winStart = winEnd = -1; + } + function appendView(lines) { + var q = logQuery; + for (var i = 0; i < lines.length; i++) { var l = lines[i]; if ((!logFilter || l.src.indexOf(logFilter) === 0) && (!q || l.text.toLowerCase().indexOf(q) >= 0 || l.src.indexOf(q) >= 0)) view.push(l); } + } + function rowHtml(l) { + var text = esc(l.text), q = logQuery; + if (q) { // highlight every match (case-insensitive, on the escaped text; the query itself is escaped too) + var eq = esc(q), lower = text.toLowerCase(), out = '', at = 0, idx; + while ((idx = lower.indexOf(eq, at)) >= 0) { out += text.slice(at, idx) + '' + text.slice(idx, idx + eq.length) + ''; at = idx + eq.length; } + text = out + text.slice(at); + } + return '