@quotivity/cpq-inventory-core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ @font-face{font-family:Lexend Deca;src:url(../fonts/LexendDeca-VariableFont_wght.ttf)format("truetype-variations");font-weight:100 900;font-style:normal;font-display:swap}:root{--font-sans:"Lexend Deca", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono:"Andale Mono", ui-monospace, "SF Mono", Menlo, monospace;--page:#fff9f4;--ink:#1c2a23;--ink-strong:#0f2e22;--ink-body:#3c4a42;--ink-soft:#5c6b62;--ink-muted:#6e7c73;--ink-faint:#8a9490;--ink-ghost:#a79c8e;--line:#e6ded4;--line-strong:#ded5c9;--line-soft:#efe8df;--line-faint:#f3ede5;--field-border:#c9bfb2;--dot:#d8cec1;--track:#e9e0d6;--white:#fff;--brand:#047251;--brand-accent:#07ae77;--brand-electric:#00db9a;--brand-tint:#f2faf5;--brand-tint-border:#cfe3d8;--banner-fg:#dcefe5;--clear-fg:#047251;--clear-accent:#07ae77;--partial-path-fg:#a9611c;--partial-path-accent:#d97230;--notarget-fg:#b23a2e;--notarget-accent:#c0392b;--review-fg:#1f6fa8;--review-accent:#3e8dc4;--review-border:#c5d9e8;--review-tint:#f4f8fb;--warn-fg:#8a4a17;--warn-body:#5c4a38;--warn-border:#e8c9a6;--warn-tint:#fdf3e8;--partial:#e0a11b}html,body{background:var(--page);margin:0}body{font-family:var(--font-sans);color:var(--ink);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-weight:300}a{color:var(--brand);text-underline-offset:2px;text-decoration:underline}a:hover{color:var(--ink-strong)}button{font-family:var(--font-sans)}*{box-sizing:border-box}.page{background:var(--page);min-height:100vh}.wrap{max-width:920px;margin:0 auto;padding-inline:32px}.banner{background:var(--ink-strong);color:var(--banner-fg)}.banner .wrap{flex-wrap:wrap;align-items:center;gap:10px;padding-block:7px 8px;display:flex}.banner-text{letter-spacing:.01em;font-size:12px;line-height:1.5}.banner-host{font-family:var(--font-mono);color:var(--white);font-weight:600}.header{z-index:5;background:var(--page);border-bottom:1px solid var(--line);position:sticky;top:0}.header-row{flex-wrap:wrap;align-items:center;gap:20px;padding-block:13px;display:flex}.brand{align-items:center;gap:9px;display:flex}.brand img{width:auto;height:22px;display:block}.brand-sub{color:var(--ink-muted);font-size:13px}.spacer{flex:1;min-width:24px}.learn{color:var(--brand);align-items:center;gap:5px;font-size:14px;font-weight:600;text-decoration:none;display:inline-flex}.rail{flex-wrap:wrap;gap:24px;display:flex}.rail button{cursor:pointer;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);background:0 0;border:0;border-bottom:2px solid #0000;margin:0;padding:0 0 9px;font-size:12px;font-weight:600}.rail button.active{color:var(--ink-strong);border-bottom-color:var(--brand-accent)}.eyebrow{letter-spacing:.1em;text-transform:uppercase;color:var(--brand);margin-bottom:18px;font-size:12px;font-weight:600}.eyebrow.tight{margin-bottom:14px}h1{color:var(--ink-strong);letter-spacing:-.01em;margin:0 0 18px;font-size:32px;font-weight:700;line-height:1.12}h1.hero{letter-spacing:-.015em;text-wrap:pretty;margin-bottom:24px;font-size:40px;line-height:1.08}h1.h28{letter-spacing:0;margin-bottom:14px;font-size:28px;line-height:1.15}h1.h32-14{margin-bottom:14px}h1.h32-20{margin-bottom:20px}.lede{color:var(--ink-body);font-size:16px;line-height:1.6}.lede-17{color:var(--ink-body);gap:13px;font-size:17px;line-height:1.55;display:grid}.lede-grid{color:var(--ink-body);gap:11px;font-size:16px;line-height:1.6;display:grid}.muted-13{color:var(--ink-muted);font-size:13px;line-height:1.65}.muted-14{color:var(--ink-muted);font-size:14px;line-height:1.6}.soft-14{color:var(--ink-soft);font-size:14px;line-height:1.65}.mono{font-family:var(--font-mono)}.btn{background:var(--ink-strong);color:var(--page);cursor:pointer;border:0;border-radius:3px;padding:14px 26px;font-size:15px;font-weight:600;transition:background .12s ease-out}.btn:hover{background:#1a4634}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-outline{border:1px solid var(--field-border);color:var(--ink-strong);cursor:pointer;background:0 0;border-radius:3px;padding:13px 22px;font-size:15px;font-weight:600}.btn-chip{border:1px solid var(--field-border);cursor:pointer;color:var(--ink-strong);background:0 0;border-radius:3px;padding:9px 14px;font-size:13px;font-weight:600}.btn-text{cursor:pointer;color:var(--brand);text-align:left;background:0 0;border:0;margin:0;padding:0;font-size:13px;font-weight:600}.btn-back{cursor:pointer;color:var(--ink-muted);background:0 0;border:0;margin:0;padding:0;font-size:14px;font-weight:600}.box-brand{border:1px solid var(--brand-tint-border);border-left:3px solid var(--brand-accent);background:var(--brand-tint);border-radius:3px;padding:18px 20px}.box-brand.lg{padding:20px 22px}.box-warn{border:1px solid var(--warn-border);border-left:3px solid var(--partial-path-accent);background:var(--warn-tint);border-radius:3px;padding:16px 18px}.box-plain{border:1px solid var(--line);background:var(--white);border-radius:3px;padding:16px 18px}.box-plain.lg{padding:18px 20px}.box-plain.xl{padding:20px 22px}.box-error{border:1px solid #e8b4ae;border-left:3px solid var(--notarget-accent);background:#fdf0ee;border-radius:3px;padding:16px 18px}.checklist{gap:8px;display:grid}.checklist>div{color:var(--ink-body);align-items:baseline;gap:10px;font-size:14px;line-height:1.55;display:flex}.checklist .mark{width:12px;font-family:var(--font-mono);flex:none;font-weight:700}.mark.yes{color:var(--brand)}.mark.no{color:var(--notarget-accent)}.mark.partial{color:var(--partial)}.landing{padding-block:56px 96px}.landing-inner{flex-direction:column;min-height:540px;display:flex}.slide{flex:1}.cmp{border-top:1px solid var(--line-strong)}.cmp-head,.cmp-row{align-items:baseline;gap:16px;padding:9px 0;display:flex}.cmp-head{border-bottom:1px solid var(--line-strong);letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);font-size:11px;font-weight:600}.cmp-row{border-bottom:1px solid var(--line-faint)}.cmp-label{flex:1;min-width:170px}.cmp-row .cmp-label{color:var(--ink-body);font-size:14px;line-height:1.45}.cmp-group{letter-spacing:.08em;text-transform:uppercase;color:var(--ink-strong);padding:15px 0 7px;font-size:11px;font-weight:600}.cmp-hs{text-align:center;flex:none;width:104px;font-size:16px;font-weight:700}.cmp-head .cmp-hs{font-size:11px;font-weight:600}.cmp-q{text-align:center;width:118px;color:var(--brand);background:var(--brand-tint);flex:none;font-size:16px;font-weight:700}.cmp-head .cmp-q{background:0 0;font-size:11px;font-weight:600}.landing-nav{flex-wrap:wrap;align-items:center;gap:20px;margin-top:44px;display:flex}.dots{align-items:center;gap:9px;margin-right:4px;display:flex}.dots button{cursor:pointer;background:var(--dot);border:0;border-radius:4px;flex:none;width:8px;height:8px;margin:0;padding:0;transition:width .2s ease-out,background .2s ease-out}.dots button.active{background:var(--ink-strong);width:26px}.gate{padding-block:76px 104px}.label{color:var(--ink-strong);margin-bottom:6px;font-size:13px;font-weight:600;display:block}.input{border:1px solid var(--field-border);background:var(--white);width:100%;max-width:440px;font-family:var(--font-sans);color:var(--ink);border-radius:3px;outline:none;padding:12px;font-size:15px;font-weight:300}.input:focus{border-color:var(--brand);box-shadow:0 0 0 3px #07ae7726}.input.invalid{border-color:var(--notarget-accent)}.error-13{color:var(--notarget-accent);margin-top:7px;font-size:13px}.gate-foot{border-top:1px solid var(--line);margin-top:34px;padding-top:20px}.name-row{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;max-width:440px;display:grid}.name-row .input{max-width:none}@media (max-width:520px){.name-row{grid-template-columns:1fr}}.modal-overlay{z-index:20;background:#0f2e2273;justify-content:center;align-items:center;padding:24px;display:flex;position:fixed;inset:0}.modal{background:var(--page);border:1px solid var(--line);border-radius:3px;width:100%;max-width:560px;max-height:calc(100vh - 48px);padding:24px 26px 22px;overflow:auto;box-shadow:0 12px 32px #0f2e2233}.modal .input{max-width:none}.modal-title{color:var(--ink-strong);font-size:20px;font-weight:700}.modal-actions{align-items:center;gap:16px;margin-top:22px;display:flex}.input.textarea{resize:vertical;min-height:120px;line-height:1.55}.input.select{appearance:auto;padding:11px 12px}.scanning{padding-block:72px 104px}.progress-track{background:var(--track);border-radius:3px;height:4px;overflow:hidden}.progress-bar{background:var(--brand-accent);height:4px;transition:width .24s ease-out}.scan-log{gap:10px;margin-top:28px;display:grid}.scan-row{align-items:baseline;gap:12px;font-size:14px;display:flex}.scan-row .mark{width:12px;font-family:var(--font-mono);flex:none;font-size:13px}.scan-row .detail{font-family:var(--font-mono);color:var(--ink-faint);font-size:12px}.scan-row.done{color:var(--ink-body)}.scan-row.now{color:var(--ink-strong)}.scan-row.queued{color:var(--ink-ghost)}.stage{padding-block:52px 104px}.chips{flex-wrap:wrap;align-items:center;gap:10px;margin-top:24px;display:flex}.chip{border:1px solid var(--line);background:var(--white);color:var(--ink-body);border-radius:3px;padding:9px 14px;font-size:13px}.chip b{color:var(--ink-strong);font-weight:600}.phases{gap:34px;margin-top:34px;display:grid}.phase-head{border-bottom:1px solid var(--line-strong);align-items:baseline;gap:12px;padding-bottom:9px;display:flex}.phase-name{letter-spacing:.08em;text-transform:uppercase;color:var(--ink-strong);font-size:13px;font-weight:600}.phase-note{color:var(--ink-faint);font-size:13px}.cards{grid-template-columns:repeat(auto-fill,minmax(316px,1fr));align-items:start;gap:14px;margin-top:14px;display:grid}.card{border:1px solid var(--line);background:var(--white);border-radius:3px;flex-direction:column;gap:14px;padding:18px 20px;display:flex}.card-name{color:var(--ink-strong);font-size:16px;font-weight:600}.card-q{color:var(--ink-faint);margin-top:4px;font-size:13px}.metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 22px;display:grid}.metric-v{font-family:var(--font-mono);color:var(--ink-strong);font-size:19px;font-weight:600;line-height:1.1}.metric-v.attention{color:var(--notarget-fg)}.metric-v.zero{color:var(--ink-faint)}.metric-v.review{color:var(--review-fg)}.metric-v.unreliable{color:var(--partial-path-fg);font-size:13px}.metric-k{letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);margin-top:3px;font-size:11px;font-weight:600}.summary{color:var(--ink-body);gap:4px;font-size:14px;line-height:1.55;display:grid}.breakdown{border-top:1px solid var(--line-soft);gap:9px;padding-top:12px;display:grid}.brow{align-items:baseline;gap:10px;display:flex}.brow-label{color:var(--ink-body);font-size:13px}.brow-lands{color:var(--ink-faint);margin-top:2px;font-size:12px}.brow-cross{color:var(--ink-ghost);margin-top:2px;font-size:11px;font-style:italic}.brow-right{text-align:right;flex:none}.brow-count{font-family:var(--font-mono);color:var(--ink-strong);font-size:13px}.brow-count.zero{color:var(--ink-ghost)}.brow-tags{justify-content:flex-end;align-items:baseline;gap:6px;margin-top:2px;display:flex}.shape{letter-spacing:.04em;text-transform:uppercase;color:var(--ink-faint);font-size:10px;font-weight:600}.verdict{font-size:11px;font-weight:600}.v-clear{color:var(--clear-fg)}.v-partial-path{color:var(--partial-path-fg)}.v-notarget{color:var(--notarget-fg)}.v-review{color:var(--review-fg)}.bnote{color:var(--ink-faint);margin-top:2px;font-size:12px;line-height:1.55}.partial-flag{color:var(--partial-path-fg);font-size:11px;font-weight:600}.dense-row{border-bottom:1px solid var(--line-soft);padding:16px 0}.dense-head{flex-wrap:wrap;align-items:baseline;gap:24px;display:flex}.dense-name{flex:none;width:196px}.dense-name b{color:var(--ink-strong);font-size:15px;font-weight:600;display:block}.dense-name span{color:var(--ink-faint);margin-top:3px;font-size:12px;display:block}.dense-summary{min-width:240px;color:var(--ink-body);flex:1;gap:3px;font-size:14px;line-height:1.5;display:grid}.dense-metrics{flex-wrap:wrap;flex:none;gap:16px;width:210px;display:flex}.dense-metrics .metric-v{font-size:15px}.dense-metrics .metric-k{margin-top:2px;font-size:10px}.dense-rows{gap:8px;margin-top:14px;margin-left:220px;display:grid}.drow{align-items:baseline;gap:14px;display:flex}.drow-label{color:var(--ink-body);flex:1;font-size:13px}.drow-label span{color:var(--ink-faint)}.drow-count{text-align:right;width:72px;font-family:var(--font-mono);color:var(--ink-strong);flex:none;font-size:13px}.drow-shape{text-align:right;flex:none;width:58px}.drow-verdict{text-align:right;flex:none;width:98px;font-size:12px;font-weight:600}.tiles{grid-template-columns:repeat(auto-fit,minmax(212px,1fr));gap:12px;margin-top:30px;display:grid}.tile{border:1px solid var(--line);border-left:3px solid var(--ink-strong);background:var(--white);border-radius:3px;padding:14px 16px}.tile-top{align-items:baseline;gap:8px;display:flex}.tile-count{color:var(--ink-strong);font-size:22px;font-weight:700;line-height:1.1}.tile-pct{font-family:var(--font-mono);color:var(--ink-faint);font-size:12px}.tile-label{margin-top:5px;font-size:13px;font-weight:600}.tile-meaning{color:var(--ink-muted);margin-top:3px;font-size:12px;line-height:1.5}.legend{border:1px solid var(--line);background:var(--white);border-radius:3px;flex-wrap:wrap;align-items:baseline;gap:26px;margin-top:12px;padding:14px 18px;display:flex}.legend-title{letter-spacing:.06em;text-transform:uppercase;color:var(--ink-strong);font-size:11px;font-weight:600}.legend-item{align-items:baseline;gap:7px;display:flex}.legend-count{font-family:var(--font-mono);color:var(--ink-strong);font-size:13px;font-weight:600}.legend-label{letter-spacing:.04em;text-transform:uppercase;color:var(--ink-body);font-size:12px;font-weight:600}.legend-meaning{color:var(--ink-faint);font-size:12px}.map{border-top:1px solid var(--line-strong);margin-top:36px}.map-head,.map-row{align-items:baseline;gap:20px;display:flex}.map-head{border-bottom:1px solid var(--line-strong);letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);padding:10px 0;font-size:11px;font-weight:600}.map-row{border-bottom:1px solid var(--line-soft);flex-wrap:wrap;padding:13px 0}.map-bucket{width:150px;color:var(--ink-faint);flex:none;font-size:12px;font-weight:600}.map-from{min-width:170px;color:var(--ink-strong);flex:1.1;font-size:14px;line-height:1.5}.map-count{font-family:var(--font-mono);color:var(--ink-faint);margin-left:6px;font-size:12px}.map-to{min-width:190px;color:var(--ink-body);flex:1.25;font-size:14px;line-height:1.5}.map-shape{text-align:right;flex:none;width:62px}.map-verdict{text-align:right;flex:none;width:104px;font-size:13px;font-weight:600}.panel{border:1px solid var(--line-strong);background:var(--white);border-radius:3px;margin-top:36px;overflow:hidden}.panel.review{border-color:var(--review-border)}.panel-head{border-bottom:1px solid var(--line-soft);padding:18px 22px}.panel.review .panel-head{background:var(--review-tint)}.panel-title{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}.panel-title b{color:var(--ink-strong);font-size:16px;font-weight:700}.panel-count{font-family:var(--font-mono);color:var(--review-fg);font-size:13px;font-weight:600}.panel-sub{color:var(--ink-soft);margin-top:5px;font-size:13px;line-height:1.6}.panel-body{padding:4px 22px 14px}.review-row{border-bottom:1px solid var(--line-faint);padding:16px 0}.review-row:last-child{border-bottom:0}.review-top{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}.reason{letter-spacing:.06em;text-transform:uppercase;color:var(--review-fg);border:1px solid var(--review-border);border-radius:3px;padding:2px 6px;font-size:10px;font-weight:600}.review-subject{color:var(--ink-strong);font-size:14px;font-weight:600}.review-q{color:var(--ink-body);margin-top:9px;font-size:14px;line-height:1.55}.outcomes{gap:5px;margin-top:9px;display:grid}.outcome{color:var(--ink-soft);align-items:baseline;gap:9px;font-size:13px;line-height:1.55;display:flex}.outcome .arrow{font-weight:700}.outcome.a .arrow{color:var(--brand)}.outcome.b .arrow{color:var(--partial-path-fg)}.review-names{font-family:var(--font-mono);color:var(--ink-faint);margin-top:8px;font-size:12px;line-height:1.6}.notes{gap:14px;margin-top:28px;display:grid}.note-head{color:var(--ink-strong);margin-bottom:5px;font-size:14px;font-weight:600}.note-body{color:var(--ink-body);font-size:14px;line-height:1.65}.prereq-row{border-bottom:1px solid var(--line-faint);flex-wrap:wrap;align-items:baseline;gap:16px;padding:11px 0;display:flex}.prereq-field{min-width:200px;font-family:var(--font-mono);color:var(--ink-strong);flex:1;font-size:13px}.prereq-need{min-width:180px;color:var(--ink-soft);flex:1;font-size:13px}.prereq-flag{color:var(--review-fg);flex:none;font-size:11px;font-weight:600}.close{padding-block:76px 120px}.send-title{letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:12px;font-size:12px;font-weight:600}.actions{flex-wrap:wrap;align-items:center;gap:14px;margin-top:30px;display:flex}.filename{font-family:var(--font-mono);color:var(--ink-faint);margin-top:12px;font-size:12px}.footer{border-top:1px solid var(--line);font-family:var(--font-mono);color:var(--ink-faint);margin-top:48px;padding-top:22px;font-size:12px;line-height:1.7}.confirm-title{color:var(--ink-strong);margin-bottom:5px;font-size:14px;font-weight:600}.confirm-body{color:var(--ink-body);font-size:14px;line-height:1.65}.print-doc .stage{padding-block:32px 40px}.print-ident{font-family:var(--font-mono);color:var(--ink-faint);border-bottom:1px solid var(--line);flex-wrap:wrap;gap:18px;margin-bottom:24px;padding:10px 0;font-size:11px;display:flex}.print-ident b{color:var(--ink-strong);font-weight:600}@page{size:A4;margin:16mm 14mm 18mm}@media print{html,body{background:#fff}*,:before,:after{-webkit-print-color-adjust:exact;-moz-print-color-adjust:exact;print-color-adjust:exact;transition:none!important}.banner,.header,.rail,.no-print,.btn,.btn-outline,.btn-chip,.btn-text,.btn-back,.actions,.dots,.landing-nav{display:none!important}.page{min-height:0}.wrap{max-width:none;padding-inline:0}.stage,.close{padding-block:0 8mm}.breakdown,.dense-rows{display:grid!important}.card,.dense-row,.map-row,.brow,.drow,.review-row,.tile,.box-brand,.box-plain,.note,.prereq-row,.phase-head{break-inside:avoid;page-break-inside:avoid}.phase{break-inside:auto}.cards{grid-template-columns:1fr 1fr}thead{display:table-header-group}tr{break-inside:avoid}h1{break-after:avoid}.stage-break{break-before:page;page-break-before:always}.print-ident{display:flex}a{color:inherit;text-decoration:none}.learn:after{content:""}}
@@ -0,0 +1,6 @@
1
+ <svg width="324" height="324" viewBox="0 0 324 324" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <title>Quotivity</title>
3
+ <circle cx="162" cy="162" r="162" fill="#07AE77"/>
4
+ <path d="M70.8835 119.414C91.2643 76.2336 137.49 53.5985 184.442 63.8082C229.519 73.6105 263.011 114.401 262.631 160.909C262.527 173.518 259.56 186.487 255.727 198.6C252.06 210.187 251.023 221.131 253.152 231.653C228.843 202.908 221.016 194.278 207.703 177.81C209.387 177.642 211.146 177.186 212.983 176.418C220.856 173.126 222.767 166.285 222.125 158.421C220.183 134.618 208.508 117.19 186.824 107.389C164.325 97.2181 142.751 100.216 123.76 115.939C105.024 131.451 98.5569 152.025 103.638 175.564C110.534 207.519 142.059 227.612 174.224 220.747C180.858 219.33 185.295 220.542 190.079 224.922C198.064 232.235 206.571 238.978 215.555 246.549C183.676 269.796 133.313 267.224 100.295 241.638C63.273 212.947 50.9347 161.675 70.8835 119.414Z" fill="white"/>
5
+ <path d="M263.123 254.163C264.97 256.79 264.435 261.093 265.002 264.62C261.25 264 256.444 264.657 253.909 262.563C219.275 233.967 184.872 205.089 150.613 176.042C140.788 167.712 139.979 156.235 147.793 148.267C155.783 140.119 167.892 141.047 176.363 150.655C182.04 157.097 187.636 163.625 192.957 170.361C216.193 199.775 219.91 202.649 263.123 254.163Z" fill="white"/>
6
+ </svg>
@@ -4,9 +4,10 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <meta name="referrer" content="no-referrer" />
7
+ <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
7
8
  <title>Quotivity CPQ Inventory</title>
8
- <script type="module" crossorigin src="./assets/index-CqUjsqpS.js"></script>
9
- <link rel="stylesheet" crossorigin href="./assets/index-CKPi--yh.css">
9
+ <script type="module" crossorigin src="./assets/index-BaGavxLi.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-ktVHVKok.css">
10
11
  </head>
11
12
  <body>
12
13
  <div id="root"></div>
package/lib/banner.js CHANGED
@@ -8,6 +8,6 @@ export function launchBanner(org) {
8
8
  ].join('\n');
9
9
  }
10
10
  export function readyLine(url) {
11
- return `Report served at ${url} — bound to 127.0.0.1 only. Press Ctrl-C to stop.`;
11
+ return `Report served at ${url} — loopback only, nothing is reachable off this machine.`;
12
12
  }
13
13
  //# sourceMappingURL=banner.js.map
@@ -53,7 +53,7 @@ export const RULE_ROWS = [
53
53
  discriminator: 'multiple maintained rate periods per currency',
54
54
  bucket: 'catalog',
55
55
  lands: 'HubSpot holds one current rate per currency. There is no way to author a rate for a named past or future period',
56
- verdict: 'Degraded',
56
+ verdict: 'Partial path',
57
57
  unit: 'currencies',
58
58
  mapping: 'catalog.dcrTable',
59
59
  }),
@@ -98,7 +98,7 @@ export const RULE_ROWS = [
98
98
  discriminator: 'MinOptionCount > 1, or MaxOptionCount is a finite number > 1',
99
99
  bucket: 'configuration',
100
100
  lands: 'Allow-multiple, plus roll-up + Block to enforce the count',
101
- verdict: 'Degraded',
101
+ verdict: 'Partial path',
102
102
  mapping: 'configuration.minMax',
103
103
  }),
104
104
  row({
@@ -119,7 +119,7 @@ export const RULE_ROWS = [
119
119
  discriminator: 'OptionalSKU is itself configurable',
120
120
  bucket: 'configuration',
121
121
  lands: '—',
122
- verdict: 'No target',
122
+ verdict: 'No path',
123
123
  mapping: 'configuration.nested',
124
124
  }),
125
125
  row({
@@ -162,7 +162,7 @@ export const RULE_ROWS = [
162
162
  discriminator: 'Type = Dependency, any other shape',
163
163
  bucket: 'configuration',
164
164
  lands: 'Add add-on bundle product, or Block',
165
- verdict: 'Degraded',
165
+ verdict: 'Partial path',
166
166
  mapping: 'configuration.dependency',
167
167
  }),
168
168
  row({
@@ -183,7 +183,7 @@ export const RULE_ROWS = [
183
183
  discriminator: 'Type = Filter',
184
184
  bucket: 'configuration',
185
185
  lands: '—',
186
- verdict: 'No target',
186
+ verdict: 'No path',
187
187
  mapping: 'configuration.filter',
188
188
  }),
189
189
  row({
@@ -204,7 +204,7 @@ export const RULE_ROWS = [
204
204
  discriminator: 'Type contains Remove or Disable, no replacement',
205
205
  bucket: 'configuration',
206
206
  lands: '—',
207
- verdict: 'No target',
207
+ verdict: 'No path',
208
208
  mapping: 'configuration.nested',
209
209
  }),
210
210
  row({
@@ -226,7 +226,7 @@ export const RULE_ROWS = [
226
226
  discriminator: 'TargetObject = Configurator',
227
227
  bucket: 'configuration',
228
228
  lands: 'Compatibility Rules and Dynamic Property Sets evaluate in the configurator; pricing does not resolve until the bundle lands on the quote',
229
- verdict: 'Degraded',
229
+ verdict: 'Partial path',
230
230
  mapping: 'configuration.dependency',
231
231
  }),
232
232
  // ── Discovery & capture ─────────────────────────────────────────────────
@@ -354,7 +354,7 @@ export const RULE_ROWS = [
354
354
  discriminator: 'formula outside the supported grammar, or over the IF / length / nesting limits',
355
355
  bucket: 'price',
356
356
  lands: '—',
357
- verdict: 'No target',
357
+ verdict: 'No path',
358
358
  mapping: 'price.term',
359
359
  }),
360
360
  row({
@@ -375,7 +375,7 @@ export const RULE_ROWS = [
375
375
  discriminator: 'TargetObject = Quote, SourceVariable empty',
376
376
  bucket: 'price',
377
377
  lands: '— no outcome writes an arbitrary quote property',
378
- verdict: 'No target',
378
+ verdict: 'No path',
379
379
  mapping: 'price.term',
380
380
  }),
381
381
  row({
@@ -386,7 +386,7 @@ export const RULE_ROWS = [
386
386
  bucket: 'price',
387
387
  crossBucket: 'lifecycle',
388
388
  lands: 'Ramp pricing',
389
- verdict: 'Degraded',
389
+ verdict: 'Partial path',
390
390
  unit: 'products',
391
391
  mapping: 'price.mdq',
392
392
  }),
@@ -398,7 +398,7 @@ export const RULE_ROWS = [
398
398
  bucket: 'price',
399
399
  crossBucket: 'lifecycle',
400
400
  lands: 'Stepped periods built with Update Bundle Members rules — the Ramp Pricing feature itself cannot be applied to a bundle',
401
- verdict: 'Degraded',
401
+ verdict: 'Partial path',
402
402
  unit: 'products',
403
403
  mapping: 'price.mdq',
404
404
  }),
@@ -409,7 +409,7 @@ export const RULE_ROWS = [
409
409
  discriminator: 'ChargeType = Usage',
410
410
  bucket: 'price',
411
411
  lands: '—',
412
- verdict: 'No target',
412
+ verdict: 'No path',
413
413
  unit: 'products',
414
414
  mapping: 'price.term',
415
415
  }),
@@ -432,7 +432,7 @@ export const RULE_ROWS = [
432
432
  discriminator: 'MDQ segments with differing quantities',
433
433
  bucket: 'price',
434
434
  lands: 'Quantity is locked across ramp periods — stepped quantity does not carry',
435
- verdict: 'Degraded',
435
+ verdict: 'Partial path',
436
436
  unit: 'dimensions',
437
437
  mapping: 'price.mdq',
438
438
  }),
@@ -467,7 +467,7 @@ export const RULE_ROWS = [
467
467
  discriminator: 'ConstraintField populated',
468
468
  bucket: 'follows',
469
469
  lands: 'Roll-up conditions compare a line property to a static value, not to a quote property. Needs Update Line Items to stamp the quote value onto lines first, then filter on it',
470
- verdict: 'Degraded',
470
+ verdict: 'Partial path',
471
471
  mapping: 'price.mdq',
472
472
  }),
473
473
  row({
@@ -488,7 +488,7 @@ export const RULE_ROWS = [
488
488
  discriminator: 'TargetObject = Product Option',
489
489
  bucket: 'follows',
490
490
  lands: 'Aggregates catalog records at configuration time; roll-ups aggregate quote lines',
491
- verdict: 'Degraded',
491
+ verdict: 'Partial path',
492
492
  mapping: 'configuration.dependency',
493
493
  }),
494
494
  row({
@@ -498,7 +498,7 @@ export const RULE_ROWS = [
498
498
  discriminator: 'TargetObject = Asset or Subscription, or Scope = Assets',
499
499
  bucket: 'follows',
500
500
  lands: '— no asset model',
501
- verdict: 'No target',
501
+ verdict: 'No path',
502
502
  mapping: 'lifecycle.orders',
503
503
  }),
504
504
  // ── Discounting ─────────────────────────────────────────────────────────
@@ -531,7 +531,7 @@ export const RULE_ROWS = [
531
531
  discriminator: 'AggregationScope = Group',
532
532
  bucket: 'discounting',
533
533
  lands: 'Roll-ups aggregate quote-wide or per bundle. Line item grouping is a template display feature and carries no functional aggregate',
534
- verdict: 'Degraded',
534
+ verdict: 'Partial path',
535
535
  mapping: 'discounting.group',
536
536
  }),
537
537
  row({
@@ -541,7 +541,7 @@ export const RULE_ROWS = [
541
541
  discriminator: 'Type = Slab',
542
542
  bucket: 'discounting',
543
543
  lands: 'Volume tiers, converted to per-unit',
544
- verdict: 'Degraded',
544
+ verdict: 'Partial path',
545
545
  mapping: 'discounting.tiers',
546
546
  }),
547
547
  row({
@@ -653,7 +653,7 @@ export const RULE_ROWS = [
653
653
  discriminator: 'referenced by a discount schedule, rule or approval',
654
654
  bucket: 'guardrails',
655
655
  lands: 'Grouping is display-only; anything that aggregates or gates by group has to be rebuilt on a quote-wide or per-bundle roll-up',
656
- verdict: 'Degraded',
656
+ verdict: 'Partial path',
657
657
  mapping: 'discounting.group',
658
658
  }),
659
659
  // ── Approvals ───────────────────────────────────────────────────────────
@@ -675,7 +675,7 @@ export const RULE_ROWS = [
675
675
  discriminator: 'approver derived from a field',
676
676
  bucket: 'approvals',
677
677
  lands: 'Workflow routing',
678
- verdict: 'Degraded',
678
+ verdict: 'Partial path',
679
679
  mapping: 'approvals.derived',
680
680
  }),
681
681
  row({
@@ -696,7 +696,7 @@ export const RULE_ROWS = [
696
696
  discriminator: '—',
697
697
  bucket: 'approvals',
698
698
  lands: 'Rule conditions + roll-ups',
699
- verdict: 'Degraded',
699
+ verdict: 'Partial path',
700
700
  mapping: 'approvals.derived',
701
701
  }),
702
702
  row({
@@ -807,7 +807,7 @@ export const RULE_ROWS = [
807
807
  discriminator: '—',
808
808
  bucket: 'lifecycle',
809
809
  lands: '—',
810
- verdict: 'No target',
810
+ verdict: 'No path',
811
811
  mapping: 'lifecycle.orders',
812
812
  }),
813
813
  // ── Custom code & UI ────────────────────────────────────────────────────
package/lib/index.d.ts CHANGED
@@ -19,6 +19,7 @@ export * from './report/types.js';
19
19
  export { type RunningServer, type RunStatus, type ServerOptions, startServer } from './server.js';
20
20
  export * from './soql.js';
21
21
  export * from './types.js';
22
+ export * from './waitForExit.js';
22
23
  export interface RunOptions extends Omit<ExtractOptions, 'onProgress'> {
23
24
  version: string;
24
25
  runId?: string;
package/lib/index.js CHANGED
@@ -20,6 +20,7 @@ export * from './report/types.js';
20
20
  export { startServer } from './server.js';
21
21
  export * from './soql.js';
22
22
  export * from './types.js';
23
+ export * from './waitForExit.js';
23
24
  export function newRunId() {
24
25
  return randomBytes(2).toString('hex');
25
26
  }
package/lib/mapping.js CHANGED
@@ -28,7 +28,7 @@ export const MAPPING_ROWS = [
28
28
  bucket: 'catalog',
29
29
  from: 'Dated conversion rates, a maintained rate table',
30
30
  to: 'HubSpot holds one current rate per currency — a rate cannot be authored against a period',
31
- verdict: 'Degraded',
31
+ verdict: 'Partial path',
32
32
  },
33
33
  {
34
34
  id: 'configuration.bundles',
@@ -51,28 +51,28 @@ export const MAPPING_ROWS = [
51
51
  bucket: 'configuration',
52
52
  from: 'Option constraints — dependency ("requires")',
53
53
  to: 'Already satisfied when the required option is included with a default; otherwise Add add-on or Block',
54
- verdict: 'Degraded',
54
+ verdict: 'Partial path',
55
55
  },
56
56
  {
57
57
  id: 'configuration.minMax',
58
58
  bucket: 'configuration',
59
59
  from: 'Feature min/max option counts',
60
60
  to: '"Pick one" and "Pick one or more" are native; a numeric cap needs a roll-up + Block',
61
- verdict: 'Degraded',
61
+ verdict: 'Partial path',
62
62
  },
63
63
  {
64
64
  id: 'configuration.nested',
65
65
  bucket: 'configuration',
66
66
  from: 'Nested bundles',
67
67
  to: '—',
68
- verdict: 'No target',
68
+ verdict: 'No path',
69
69
  },
70
70
  {
71
71
  id: 'configuration.filter',
72
72
  bucket: 'configuration',
73
73
  from: 'Dynamic option sets (filter rules)',
74
74
  to: '—',
75
- verdict: 'No target',
75
+ verdict: 'No path',
76
76
  },
77
77
  {
78
78
  id: 'configuration.lookup',
@@ -118,14 +118,14 @@ export const MAPPING_ROWS = [
118
118
  bucket: 'price',
119
119
  from: 'MDQ / segmented subscription pricing',
120
120
  to: 'Ramp pricing; Update Bundle Members rules when bundled. Stepped quantity does not carry',
121
- verdict: 'Degraded',
121
+ verdict: 'Partial path',
122
122
  },
123
123
  {
124
124
  id: 'price.term',
125
125
  bucket: 'price',
126
126
  from: 'Subscription term and proration maths',
127
127
  to: '—',
128
- verdict: 'No target',
128
+ verdict: 'No path',
129
129
  },
130
130
  {
131
131
  id: 'price.lookup',
@@ -155,7 +155,7 @@ export const MAPPING_ROWS = [
155
155
  bucket: 'discounting',
156
156
  from: 'Schedules aggregating across a quote line group',
157
157
  to: 'Roll-up quote-wide or per bundle — grouping itself carries no functional aggregate',
158
- verdict: 'Degraded',
158
+ verdict: 'Partial path',
159
159
  },
160
160
  {
161
161
  id: 'discounting.contracted',
@@ -218,7 +218,7 @@ export const MAPPING_ROWS = [
218
218
  bucket: 'approvals',
219
219
  from: 'Derived approvers, escalation',
220
220
  to: 'Workflow routing',
221
- verdict: 'Degraded',
221
+ verdict: 'Partial path',
222
222
  },
223
223
  {
224
224
  id: 'output.groups',
@@ -249,7 +249,7 @@ export const MAPPING_ROWS = [
249
249
  bucket: 'lifecycle',
250
250
  from: 'Orders, assets',
251
251
  to: '—',
252
- verdict: 'No target',
252
+ verdict: 'No path',
253
253
  },
254
254
  {
255
255
  id: 'code.custom',
@@ -261,8 +261,8 @@ export const MAPPING_ROWS = [
261
261
  ];
262
262
  export const VERDICT_MEANINGS = {
263
263
  'Clear path': 'The behaviour survives and the route to it is known',
264
- Degraded: 'The behaviour survives in part. The report names what is lost',
265
- 'No target': 'The behaviour does not survive. Needs a redesign or a product change',
264
+ 'Partial path': 'The behaviour survives in part. The report names what is lost',
265
+ 'No path': 'The behaviour does not survive. Needs a redesign or a product change',
266
266
  };
267
267
  export const SHAPE_MEANINGS = {
268
268
  '1:1': 'One record becomes one record',
@@ -3,7 +3,7 @@ import { outboundConfig, SOURCE_URL } from '../config/outbound.js';
3
3
  import { MAPPING_ROWS, SHAPE_MEANINGS, VERDICT_MEANINGS } from '../mapping.js';
4
4
  import { bucketNote, summaryLines } from '../summary.js';
5
5
  import { BUCKETS, bucketDef, PHASES } from '../types.js';
6
- const ATTENTION = new Set(['Degraded', 'No target']);
6
+ const ATTENTION = new Set(['Partial path', 'No path']);
7
7
  /** Objects that feed each bucket, for unread and bulk-touch reporting. */
8
8
  const OBJECT_BUCKETS = {
9
9
  Product2: ['catalog', 'price', 'guardrails'],
@@ -249,7 +249,7 @@ export function assembleReport(ex, cl, opts) {
249
249
  count: mappingCounts.get(m.id) ?? 0,
250
250
  }));
251
251
  const verdicted = mapping.filter((m) => m.verdict !== 'Further review').length;
252
- const verdictScale = ['Clear path', 'Degraded', 'No target'].map((label) => {
252
+ const verdictScale = ['Clear path', 'Partial path', 'No path'].map((label) => {
253
253
  const count = mapping.filter((m) => m.verdict === label).length;
254
254
  return {
255
255
  label,
@@ -450,7 +450,7 @@ function buildNotes(ex, cl, rowCount) {
450
450
  },
451
451
  {
452
452
  head: 'Ramp pricing has hard edges, and a route around them',
453
- body: 'A ramp needs at least two periods, applies only to recurring lines, and cannot be applied to a bundle header or an option product. Stepped periods on a bundled product are still buildable with Update Bundle Members rules — what is lost is the native grouping, period badges and MRR handling. Quantity is also locked across ramp periods, which is the most common reason a segmented subscription lands on Degraded.',
453
+ body: 'A ramp needs at least two periods, applies only to recurring lines, and cannot be applied to a bundle header or an option product. Stepped periods on a bundled product are still buildable with Update Bundle Members rules — what is lost is the native grouping, period badges and MRR handling. Quantity is also locked across ramp periods, which is the most common reason a segmented subscription lands on Partial path.',
454
454
  },
455
455
  {
456
456
  head: 'Dated conversion rates: quoting carries, authoring does not',
package/lib/server.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface ServerOptions {
20
20
  }
21
21
  export interface RunningServer {
22
22
  server: Server;
23
+ /** The URL to open: http://localhost:<port>/. */
23
24
  url: string;
24
25
  port: number;
25
26
  close: () => Promise<void>;
package/lib/server.js CHANGED
@@ -144,7 +144,10 @@ export async function startServer(opts) {
144
144
  const server = createServer(handler);
145
145
  const port = await listen(server, host, opts.port ?? DEFAULT_PORT);
146
146
  const address = server.address();
147
- const url = `http://${host}:${address.port}/`;
147
+ // Browsers resolve "localhost" to ::1 or 127.0.0.1 depending on the machine. Bind the IPv6
148
+ // loopback as well, best effort, so http://localhost works either way. Still loopback only.
149
+ const v6 = host === HOST ? await listenLoopbackV6(handler, address.port) : null;
150
+ const url = host === HOST ? `http://localhost:${address.port}/` : `http://${host}:${address.port}/`;
148
151
  return {
149
152
  server,
150
153
  url,
@@ -153,11 +156,21 @@ export async function startServer(opts) {
153
156
  for (const s of subscribers)
154
157
  s.end();
155
158
  subscribers.clear();
159
+ v6?.close();
156
160
  server.close(() => resolveClose());
157
161
  }),
158
162
  state: () => ({ status, error }),
159
163
  };
160
164
  }
165
+ /** Best-effort second listener on ::1 for the same port; null when IPv6 loopback is unavailable. */
166
+ function listenLoopbackV6(handler, port) {
167
+ return new Promise((resolveV6) => {
168
+ const s = createServer(handler);
169
+ s.once('error', () => resolveV6(null));
170
+ s.once('listening', () => resolveV6(s));
171
+ s.listen(port, '::1');
172
+ });
173
+ }
161
174
  /** Binds 127.0.0.1 only. Detects a taken port and increments, up to 20 tries. */
162
175
  async function listen(server, host, start) {
163
176
  for (let port = start; port < start + 20; port++) {
package/lib/summary.js CHANGED
@@ -104,7 +104,7 @@ export function bucketNote(id, s) {
104
104
  case 'configuration': {
105
105
  const nested = s.rowCount('cfg.optionsNested');
106
106
  return nested
107
- ? `Nesting has no target — Quotivity bundles are one level deep, so ${n(nested)} option product${nested === 1 ? '' : 's'} ha${nested === 1 ? 's' : 've'} to be flattened or split.`
107
+ ? `Nesting has no path — Quotivity bundles are one level deep, so ${n(nested)} option product${nested === 1 ? '' : 's'} ha${nested === 1 ? 's' : 've'} to be flattened or split.`
108
108
  : 'No nested bundles. Every option product is a leaf, which is the shape Quotivity bundles expect.';
109
109
  }
110
110
  case 'discovery':
@@ -126,7 +126,7 @@ export function bucketNote(id, s) {
126
126
  case 'lifecycle':
127
127
  return f.amendRenewShareLastYear != null && f.amendRenewShareLastYear >= 0.5
128
128
  ? 'Renewals and amendments are the majority of quoting volume here, so this bucket carries more weight than its record count suggests.'
129
- : 'Contracts and subscriptions carry to HubSpot Contracts; orders and assets have no target.';
129
+ : 'Contracts and subscriptions carry to HubSpot Contracts; orders and assets have no path.';
130
130
  case 'code': {
131
131
  const seeded = Object.entries(ex.seeded)
132
132
  .filter(([o]) => /CustomAction|CustomScript/.test(o))
package/lib/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Verdict scale from the spec. "Further review" is not a verdict but travels in the same field. */
2
- export type Verdict = 'Clear path' | 'Degraded' | 'No target' | 'Further review';
2
+ export type Verdict = 'Clear path' | 'Partial path' | 'No path' | 'Further review';
3
3
  export type Shape = '1:1' | 'Fan-out' | 'Rebuild';
4
4
  export type ReviewReason = 'Unread' | 'Intent';
5
5
  export type BucketId = 'catalog' | 'configuration' | 'discovery' | 'price' | 'discounting' | 'guardrails' | 'approvals' | 'output' | 'lifecycle' | 'code';
@@ -0,0 +1,19 @@
1
+ export interface WaitForExitOptions {
2
+ /** Input stream to watch for Enter. Defaults to process.stdin. */
3
+ input?: NodeJS.ReadableStream & {
4
+ isTTY?: boolean;
5
+ };
6
+ /** Force Enter handling on even when the input is not a TTY (tests). */
7
+ interactive?: boolean;
8
+ }
9
+ export type ExitReason = 'enter' | 'signal';
10
+ /**
11
+ * Resolves when the user presses Enter, or on SIGINT / SIGTERM. Both paths resolve normally so the
12
+ * caller can close the server and return, which lets the process exit with status 0 instead of a
13
+ * thrown exit error and a stack trace.
14
+ */
15
+ export declare function waitForExit(opts?: WaitForExitOptions): Promise<ExitReason>;
16
+ /** The line printed once the server is up. */
17
+ export declare const EXIT_HINT = "Press ENTER to exit.";
18
+ export declare const STOPPED_LINE = "Stopped. The query results on this machine were never transmitted.";
19
+ //# sourceMappingURL=waitForExit.d.ts.map
@@ -0,0 +1,43 @@
1
+ const CTRL_C = String.fromCharCode(3);
2
+ /**
3
+ * Resolves when the user presses Enter, or on SIGINT / SIGTERM. Both paths resolve normally so the
4
+ * caller can close the server and return, which lets the process exit with status 0 instead of a
5
+ * thrown exit error and a stack trace.
6
+ */
7
+ export function waitForExit(opts = {}) {
8
+ const input = opts.input ?? process.stdin;
9
+ const interactive = opts.interactive ?? !!input.isTTY;
10
+ return new Promise((resolve) => {
11
+ let done = false;
12
+ const finish = (reason) => {
13
+ if (done)
14
+ return;
15
+ done = true;
16
+ process.removeListener('SIGINT', onSignal);
17
+ process.removeListener('SIGTERM', onSignal);
18
+ if (interactive) {
19
+ input.removeListener('data', onData);
20
+ input.pause();
21
+ }
22
+ resolve(reason);
23
+ };
24
+ const onSignal = () => finish('signal');
25
+ const onData = (chunk) => {
26
+ const text = chunk.toString();
27
+ if (text.includes(CTRL_C))
28
+ finish('signal');
29
+ else if (text.includes('\n') || text.includes('\r'))
30
+ finish('enter');
31
+ };
32
+ process.on('SIGINT', onSignal);
33
+ process.on('SIGTERM', onSignal);
34
+ if (interactive) {
35
+ input.on('data', onData);
36
+ input.resume();
37
+ }
38
+ });
39
+ }
40
+ /** The line printed once the server is up. */
41
+ export const EXIT_HINT = 'Press ENTER to exit.';
42
+ export const STOPPED_LINE = 'Stopped. The query results on this machine were never transmitted.';
43
+ //# sourceMappingURL=waitForExit.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quotivity/cpq-inventory-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Analysis core for the Quotivity CPQ Inventory & Migration Analysis: extraction, classification, mapping, report assembly and the local report server. Runs entirely on the customer's machine.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1 +0,0 @@
1
- @font-face{font-family:Lexend Deca;src:url(../fonts/LexendDeca-VariableFont_wght.ttf)format("truetype-variations");font-weight:100 900;font-style:normal;font-display:swap}:root{--font-sans:"Lexend Deca", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono:"Andale Mono", ui-monospace, "SF Mono", Menlo, monospace;--page:#fff9f4;--ink:#1c2a23;--ink-strong:#0f2e22;--ink-body:#3c4a42;--ink-soft:#5c6b62;--ink-muted:#6e7c73;--ink-faint:#8a9490;--ink-ghost:#a79c8e;--line:#e6ded4;--line-strong:#ded5c9;--line-soft:#efe8df;--line-faint:#f3ede5;--field-border:#c9bfb2;--dot:#d8cec1;--track:#e9e0d6;--white:#fff;--brand:#047251;--brand-accent:#07ae77;--brand-electric:#00db9a;--brand-tint:#f2faf5;--brand-tint-border:#cfe3d8;--banner-fg:#dcefe5;--clear-fg:#047251;--clear-accent:#07ae77;--degraded-fg:#a9611c;--degraded-accent:#d97230;--notarget-fg:#b23a2e;--notarget-accent:#c0392b;--review-fg:#1f6fa8;--review-accent:#3e8dc4;--review-border:#c5d9e8;--review-tint:#f4f8fb;--warn-fg:#8a4a17;--warn-body:#5c4a38;--warn-border:#e8c9a6;--warn-tint:#fdf3e8;--partial:#e0a11b}html,body{background:var(--page);margin:0}body{font-family:var(--font-sans);color:var(--ink);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-weight:300}a{color:var(--brand);text-underline-offset:2px;text-decoration:underline}a:hover{color:var(--ink-strong)}button{font-family:var(--font-sans)}*{box-sizing:border-box}.page{background:var(--page);min-height:100vh}.wrap{max-width:920px;margin:0 auto;padding-inline:32px}.banner{background:var(--ink-strong);color:var(--banner-fg)}.banner .wrap{flex-wrap:wrap;align-items:center;gap:10px;padding-block:7px 8px;display:flex}.banner-text{letter-spacing:.01em;font-size:12px;line-height:1.5}.banner-host{font-family:var(--font-mono);color:var(--white);font-weight:600}.header{z-index:5;background:var(--page);border-bottom:1px solid var(--line);position:sticky;top:0}.header-row{flex-wrap:wrap;align-items:center;gap:20px;padding-block:13px;display:flex}.brand{align-items:center;gap:9px;display:flex}.brand img{width:auto;height:22px;display:block}.brand-sub{color:var(--ink-muted);font-size:13px}.spacer{flex:1;min-width:24px}.learn{color:var(--brand);align-items:center;gap:5px;font-size:14px;font-weight:600;text-decoration:none;display:inline-flex}.rail{flex-wrap:wrap;gap:24px;display:flex}.rail button{cursor:pointer;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);background:0 0;border:0;border-bottom:2px solid #0000;margin:0;padding:0 0 9px;font-size:12px;font-weight:600}.rail button.active{color:var(--ink-strong);border-bottom-color:var(--brand-accent)}.eyebrow{letter-spacing:.1em;text-transform:uppercase;color:var(--brand);margin-bottom:18px;font-size:12px;font-weight:600}.eyebrow.tight{margin-bottom:14px}h1{color:var(--ink-strong);letter-spacing:-.01em;margin:0 0 18px;font-size:32px;font-weight:700;line-height:1.12}h1.hero{letter-spacing:-.015em;text-wrap:pretty;margin-bottom:24px;font-size:40px;line-height:1.08}h1.h28{letter-spacing:0;margin-bottom:14px;font-size:28px;line-height:1.15}h1.h32-14{margin-bottom:14px}h1.h32-20{margin-bottom:20px}.lede{color:var(--ink-body);font-size:16px;line-height:1.6}.lede-17{color:var(--ink-body);gap:13px;font-size:17px;line-height:1.55;display:grid}.lede-grid{color:var(--ink-body);gap:11px;font-size:16px;line-height:1.6;display:grid}.muted-13{color:var(--ink-muted);font-size:13px;line-height:1.65}.muted-14{color:var(--ink-muted);font-size:14px;line-height:1.6}.soft-14{color:var(--ink-soft);font-size:14px;line-height:1.65}.mono{font-family:var(--font-mono)}.btn{background:var(--ink-strong);color:var(--page);cursor:pointer;border:0;border-radius:3px;padding:14px 26px;font-size:15px;font-weight:600;transition:background .12s ease-out}.btn:hover{background:#1a4634}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-outline{border:1px solid var(--field-border);color:var(--ink-strong);cursor:pointer;background:0 0;border-radius:3px;padding:13px 22px;font-size:15px;font-weight:600}.btn-chip{border:1px solid var(--field-border);cursor:pointer;color:var(--ink-strong);background:0 0;border-radius:3px;padding:9px 14px;font-size:13px;font-weight:600}.btn-text{cursor:pointer;color:var(--brand);text-align:left;background:0 0;border:0;margin:0;padding:0;font-size:13px;font-weight:600}.btn-back{cursor:pointer;color:var(--ink-muted);background:0 0;border:0;margin:0;padding:0;font-size:14px;font-weight:600}.box-brand{border:1px solid var(--brand-tint-border);border-left:3px solid var(--brand-accent);background:var(--brand-tint);border-radius:3px;padding:18px 20px}.box-brand.lg{padding:20px 22px}.box-warn{border:1px solid var(--warn-border);border-left:3px solid var(--degraded-accent);background:var(--warn-tint);border-radius:3px;padding:16px 18px}.box-plain{border:1px solid var(--line);background:var(--white);border-radius:3px;padding:16px 18px}.box-plain.lg{padding:18px 20px}.box-plain.xl{padding:20px 22px}.box-error{border:1px solid #e8b4ae;border-left:3px solid var(--notarget-accent);background:#fdf0ee;border-radius:3px;padding:16px 18px}.checklist{gap:8px;display:grid}.checklist>div{color:var(--ink-body);align-items:baseline;gap:10px;font-size:14px;line-height:1.55;display:flex}.checklist .mark{width:12px;font-family:var(--font-mono);flex:none;font-weight:700}.mark.yes{color:var(--brand)}.mark.no{color:var(--notarget-accent)}.mark.partial{color:var(--partial)}.landing{padding-block:56px 96px}.landing-inner{flex-direction:column;min-height:540px;display:flex}.slide{flex:1}.cmp{border-top:1px solid var(--line-strong)}.cmp-head,.cmp-row{align-items:baseline;gap:16px;padding:9px 0;display:flex}.cmp-head{border-bottom:1px solid var(--line-strong);letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);font-size:11px;font-weight:600}.cmp-row{border-bottom:1px solid var(--line-faint)}.cmp-label{flex:1;min-width:170px}.cmp-row .cmp-label{color:var(--ink-body);font-size:14px;line-height:1.45}.cmp-group{letter-spacing:.08em;text-transform:uppercase;color:var(--ink-strong);padding:15px 0 7px;font-size:11px;font-weight:600}.cmp-hs{text-align:center;flex:none;width:104px;font-size:16px;font-weight:700}.cmp-head .cmp-hs{font-size:11px;font-weight:600}.cmp-q{text-align:center;width:118px;color:var(--brand);background:var(--brand-tint);flex:none;font-size:16px;font-weight:700}.cmp-head .cmp-q{background:0 0;font-size:11px;font-weight:600}.landing-nav{flex-wrap:wrap;align-items:center;gap:20px;margin-top:44px;display:flex}.dots{align-items:center;gap:9px;margin-right:4px;display:flex}.dots button{cursor:pointer;background:var(--dot);border:0;border-radius:4px;flex:none;width:8px;height:8px;margin:0;padding:0;transition:width .2s ease-out,background .2s ease-out}.dots button.active{background:var(--ink-strong);width:26px}.gate{padding-block:76px 104px}.label{color:var(--ink-strong);margin-bottom:6px;font-size:13px;font-weight:600;display:block}.input{border:1px solid var(--field-border);background:var(--white);width:100%;max-width:440px;font-family:var(--font-sans);color:var(--ink);border-radius:3px;outline:none;padding:12px;font-size:15px;font-weight:300}.input:focus{border-color:var(--brand);box-shadow:0 0 0 3px #07ae7726}.input.invalid{border-color:var(--notarget-accent)}.error-13{color:var(--notarget-accent);margin-top:7px;font-size:13px}.gate-foot{border-top:1px solid var(--line);margin-top:34px;padding-top:20px}.name-row{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;max-width:440px;display:grid}.name-row .input{max-width:none}@media (max-width:520px){.name-row{grid-template-columns:1fr}}.scanning{padding-block:72px 104px}.progress-track{background:var(--track);border-radius:3px;height:4px;overflow:hidden}.progress-bar{background:var(--brand-accent);height:4px;transition:width .24s ease-out}.scan-log{gap:10px;margin-top:28px;display:grid}.scan-row{align-items:baseline;gap:12px;font-size:14px;display:flex}.scan-row .mark{width:12px;font-family:var(--font-mono);flex:none;font-size:13px}.scan-row .detail{font-family:var(--font-mono);color:var(--ink-faint);font-size:12px}.scan-row.done{color:var(--ink-body)}.scan-row.now{color:var(--ink-strong)}.scan-row.queued{color:var(--ink-ghost)}.stage{padding-block:52px 104px}.chips{flex-wrap:wrap;align-items:center;gap:10px;margin-top:24px;display:flex}.chip{border:1px solid var(--line);background:var(--white);color:var(--ink-body);border-radius:3px;padding:9px 14px;font-size:13px}.chip b{color:var(--ink-strong);font-weight:600}.phases{gap:34px;margin-top:34px;display:grid}.phase-head{border-bottom:1px solid var(--line-strong);align-items:baseline;gap:12px;padding-bottom:9px;display:flex}.phase-name{letter-spacing:.08em;text-transform:uppercase;color:var(--ink-strong);font-size:13px;font-weight:600}.phase-note{color:var(--ink-faint);font-size:13px}.cards{grid-template-columns:repeat(auto-fill,minmax(316px,1fr));align-items:start;gap:14px;margin-top:14px;display:grid}.card{border:1px solid var(--line);background:var(--white);border-radius:3px;flex-direction:column;gap:14px;padding:18px 20px;display:flex}.card-name{color:var(--ink-strong);font-size:16px;font-weight:600}.card-q{color:var(--ink-faint);margin-top:4px;font-size:13px}.metrics{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px 22px;display:grid}.metric-v{font-family:var(--font-mono);color:var(--ink-strong);font-size:19px;font-weight:600;line-height:1.1}.metric-v.attention{color:var(--notarget-fg)}.metric-v.zero{color:var(--ink-faint)}.metric-v.review{color:var(--review-fg)}.metric-v.unreliable{color:var(--degraded-fg);font-size:13px}.metric-k{letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);margin-top:3px;font-size:11px;font-weight:600}.summary{color:var(--ink-body);gap:4px;font-size:14px;line-height:1.55;display:grid}.breakdown{border-top:1px solid var(--line-soft);gap:9px;padding-top:12px;display:grid}.brow{align-items:baseline;gap:10px;display:flex}.brow-label{color:var(--ink-body);font-size:13px}.brow-lands{color:var(--ink-faint);margin-top:2px;font-size:12px}.brow-cross{color:var(--ink-ghost);margin-top:2px;font-size:11px;font-style:italic}.brow-right{text-align:right;flex:none}.brow-count{font-family:var(--font-mono);color:var(--ink-strong);font-size:13px}.brow-count.zero{color:var(--ink-ghost)}.brow-tags{justify-content:flex-end;align-items:baseline;gap:6px;margin-top:2px;display:flex}.shape{letter-spacing:.04em;text-transform:uppercase;color:var(--ink-faint);font-size:10px;font-weight:600}.verdict{font-size:11px;font-weight:600}.v-clear{color:var(--clear-fg)}.v-degraded{color:var(--degraded-fg)}.v-notarget{color:var(--notarget-fg)}.v-review{color:var(--review-fg)}.bnote{color:var(--ink-faint);margin-top:2px;font-size:12px;line-height:1.55}.partial-flag{color:var(--degraded-fg);font-size:11px;font-weight:600}.dense-row{border-bottom:1px solid var(--line-soft);padding:16px 0}.dense-head{flex-wrap:wrap;align-items:baseline;gap:24px;display:flex}.dense-name{flex:none;width:196px}.dense-name b{color:var(--ink-strong);font-size:15px;font-weight:600;display:block}.dense-name span{color:var(--ink-faint);margin-top:3px;font-size:12px;display:block}.dense-summary{min-width:240px;color:var(--ink-body);flex:1;gap:3px;font-size:14px;line-height:1.5;display:grid}.dense-metrics{flex-wrap:wrap;flex:none;gap:16px;width:210px;display:flex}.dense-metrics .metric-v{font-size:15px}.dense-metrics .metric-k{margin-top:2px;font-size:10px}.dense-rows{gap:8px;margin-top:14px;margin-left:220px;display:grid}.drow{align-items:baseline;gap:14px;display:flex}.drow-label{color:var(--ink-body);flex:1;font-size:13px}.drow-label span{color:var(--ink-faint)}.drow-count{text-align:right;width:72px;font-family:var(--font-mono);color:var(--ink-strong);flex:none;font-size:13px}.drow-shape{text-align:right;flex:none;width:58px}.drow-verdict{text-align:right;flex:none;width:98px;font-size:12px;font-weight:600}.tiles{grid-template-columns:repeat(auto-fit,minmax(212px,1fr));gap:12px;margin-top:30px;display:grid}.tile{border:1px solid var(--line);border-left:3px solid var(--ink-strong);background:var(--white);border-radius:3px;padding:14px 16px}.tile-top{align-items:baseline;gap:8px;display:flex}.tile-count{color:var(--ink-strong);font-size:22px;font-weight:700;line-height:1.1}.tile-pct{font-family:var(--font-mono);color:var(--ink-faint);font-size:12px}.tile-label{margin-top:5px;font-size:13px;font-weight:600}.tile-meaning{color:var(--ink-muted);margin-top:3px;font-size:12px;line-height:1.5}.legend{border:1px solid var(--line);background:var(--white);border-radius:3px;flex-wrap:wrap;align-items:baseline;gap:26px;margin-top:12px;padding:14px 18px;display:flex}.legend-title{letter-spacing:.06em;text-transform:uppercase;color:var(--ink-strong);font-size:11px;font-weight:600}.legend-item{align-items:baseline;gap:7px;display:flex}.legend-count{font-family:var(--font-mono);color:var(--ink-strong);font-size:13px;font-weight:600}.legend-label{letter-spacing:.04em;text-transform:uppercase;color:var(--ink-body);font-size:12px;font-weight:600}.legend-meaning{color:var(--ink-faint);font-size:12px}.map{border-top:1px solid var(--line-strong);margin-top:36px}.map-head,.map-row{align-items:baseline;gap:20px;display:flex}.map-head{border-bottom:1px solid var(--line-strong);letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);padding:10px 0;font-size:11px;font-weight:600}.map-row{border-bottom:1px solid var(--line-soft);flex-wrap:wrap;padding:13px 0}.map-bucket{width:150px;color:var(--ink-faint);flex:none;font-size:12px;font-weight:600}.map-from{min-width:170px;color:var(--ink-strong);flex:1.1;font-size:14px;line-height:1.5}.map-count{font-family:var(--font-mono);color:var(--ink-faint);margin-left:6px;font-size:12px}.map-to{min-width:190px;color:var(--ink-body);flex:1.25;font-size:14px;line-height:1.5}.map-shape{text-align:right;flex:none;width:62px}.map-verdict{text-align:right;flex:none;width:104px;font-size:13px;font-weight:600}.panel{border:1px solid var(--line-strong);background:var(--white);border-radius:3px;margin-top:36px;overflow:hidden}.panel.review{border-color:var(--review-border)}.panel-head{border-bottom:1px solid var(--line-soft);padding:18px 22px}.panel.review .panel-head{background:var(--review-tint)}.panel-title{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}.panel-title b{color:var(--ink-strong);font-size:16px;font-weight:700}.panel-count{font-family:var(--font-mono);color:var(--review-fg);font-size:13px;font-weight:600}.panel-sub{color:var(--ink-soft);margin-top:5px;font-size:13px;line-height:1.6}.panel-body{padding:4px 22px 14px}.review-row{border-bottom:1px solid var(--line-faint);padding:16px 0}.review-row:last-child{border-bottom:0}.review-top{flex-wrap:wrap;align-items:baseline;gap:10px;display:flex}.reason{letter-spacing:.06em;text-transform:uppercase;color:var(--review-fg);border:1px solid var(--review-border);border-radius:3px;padding:2px 6px;font-size:10px;font-weight:600}.review-subject{color:var(--ink-strong);font-size:14px;font-weight:600}.review-q{color:var(--ink-body);margin-top:9px;font-size:14px;line-height:1.55}.outcomes{gap:5px;margin-top:9px;display:grid}.outcome{color:var(--ink-soft);align-items:baseline;gap:9px;font-size:13px;line-height:1.55;display:flex}.outcome .arrow{font-weight:700}.outcome.a .arrow{color:var(--brand)}.outcome.b .arrow{color:var(--degraded-fg)}.review-names{font-family:var(--font-mono);color:var(--ink-faint);margin-top:8px;font-size:12px;line-height:1.6}.notes{gap:14px;margin-top:28px;display:grid}.note-head{color:var(--ink-strong);margin-bottom:5px;font-size:14px;font-weight:600}.note-body{color:var(--ink-body);font-size:14px;line-height:1.65}.prereq-row{border-bottom:1px solid var(--line-faint);flex-wrap:wrap;align-items:baseline;gap:16px;padding:11px 0;display:flex}.prereq-field{min-width:200px;font-family:var(--font-mono);color:var(--ink-strong);flex:1;font-size:13px}.prereq-need{min-width:180px;color:var(--ink-soft);flex:1;font-size:13px}.prereq-flag{color:var(--review-fg);flex:none;font-size:11px;font-weight:600}.close{padding-block:76px 120px}.send-title{letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:12px;font-size:12px;font-weight:600}.actions{flex-wrap:wrap;align-items:center;gap:14px;margin-top:30px;display:flex}.filename{font-family:var(--font-mono);color:var(--ink-faint);margin-top:12px;font-size:12px}.footer{border-top:1px solid var(--line);font-family:var(--font-mono);color:var(--ink-faint);margin-top:48px;padding-top:22px;font-size:12px;line-height:1.7}.confirm-title{color:var(--ink-strong);margin-bottom:5px;font-size:14px;font-weight:600}.confirm-body{color:var(--ink-body);font-size:14px;line-height:1.65}.print-doc .stage{padding-block:32px 40px}.print-ident{font-family:var(--font-mono);color:var(--ink-faint);border-bottom:1px solid var(--line);flex-wrap:wrap;gap:18px;margin-bottom:24px;padding:10px 0;font-size:11px;display:flex}.print-ident b{color:var(--ink-strong);font-weight:600}@page{size:A4;margin:16mm 14mm 18mm}@media print{html,body{background:#fff}*,:before,:after{-webkit-print-color-adjust:exact;-moz-print-color-adjust:exact;print-color-adjust:exact;transition:none!important}.banner,.header,.rail,.no-print,.btn,.btn-outline,.btn-chip,.btn-text,.btn-back,.actions,.dots,.landing-nav{display:none!important}.page{min-height:0}.wrap{max-width:none;padding-inline:0}.stage,.close{padding-block:0 8mm}.breakdown,.dense-rows{display:grid!important}.card,.dense-row,.map-row,.brow,.drow,.review-row,.tile,.box-brand,.box-plain,.note,.prereq-row,.phase-head{break-inside:avoid;page-break-inside:avoid}.phase{break-inside:auto}.cards{grid-template-columns:1fr 1fr}thead{display:table-header-group}tr{break-inside:avoid}h1{break-after:avoid}.stage-break{break-before:page;page-break-before:always}.print-ident{display:flex}a{color:inherit;text-decoration:none}.learn:after{content:""}}