QT01 · T-shirt quotation worksheet
HTML for this example
Select the code and use your copy shortcut. Changes in this preview are temporary.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>QT01 · T-shirt quotation worksheet</title>
<style>
:root{color-scheme:light;--ink:#182936;--blue:#173f72;--paper:#fffef9;--line:#c6cdd1;--muted:#465666;--orange:#fae1b2}*{box-sizing:border-box}body{margin:0;padding:24px 16px;background:#e8edf0;color:var(--ink);font:16px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{max-width:960px;margin:auto;background:var(--paper);box-shadow:0 10px 36px #18293612;border:1px solid #b8c4cd;border-top:8px solid var(--blue)}header{padding:24px 28px 16px;border-bottom:1px solid var(--line)}.eyebrow{margin:0 0 6px;color:var(--blue);font-weight:750;letter-spacing:.06em}h1{font-size:clamp(26px,5vw,38px);line-height:1.15;margin:0 0 12px;letter-spacing:-.025em}p{margin:0 0 12px}header p:last-child{margin:0;color:var(--muted)}h2,legend{font-size:21px;line-height:1.3;font-weight:750;margin:0 0 16px}h3{font-size:18px;margin:0 0 8px}.summary{background:var(--blue);color:white;padding:22px 28px}.summary-head{display:flex;gap:16px;justify-content:space-between;align-items:baseline;flex-wrap:wrap}.summary-head p{margin:0}.summary-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-top:16px}.amount{display:block;font-size:clamp(24px,5vw,32px);font-weight:780;font-variant-numeric:tabular-nums}.summary small{font-size:16px;display:block;color:#e3ecf6}.scope-message{margin:16px 0 0;padding-top:12px;border-top:1px solid #6684aa}.worksheet{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}section.panel{padding:24px 28px;border-bottom:1px solid var(--line)}section.panel:nth-child(odd){border-right:1px solid var(--line)}fieldset{min-width:0;border:0;padding:0;margin:0}legend{padding:0}.sizes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:16px 0}label{display:block;font-weight:650}input{font:inherit;border:1px solid #8596a5;border-radius:4px;padding:10px 12px;background:white;color:var(--ink);width:100%;min-width:0;margin-top:6px}input:focus-visible,button:focus-visible,textarea:focus-visible{outline:3px solid #ce7900;outline-offset:3px}input[aria-invalid="true"]{border:2px solid #a13219;background:#fff2e9}.help{color:var(--muted);margin-top:10px}.ticket-line{display:flex;justify-content:space-between;gap:16px;border-bottom:1px solid #dbe0e2;padding:9px 0}.ticket-line strong{text-align:right;font-variant-numeric:tabular-nums;flex-shrink:0}.ticket-line.total{font-size:19px;border-bottom:3px double #8b9aa5;margin-top:6px;font-weight:750}.option{background:#f4eddf;padding:16px;margin-top:18px;border-left:4px solid #a35f07}.option p{margin:8px 0 12px}button{font:inherit;font-weight:700;cursor:pointer;border:2px solid var(--blue);border-radius:4px;padding:11px 18px;min-height:46px;background:var(--blue);color:white;white-space:normal}button[aria-pressed="true"]{background:#fff;color:var(--blue)}button.secondary{background:transparent;color:var(--blue)}.field-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}.full{grid-column:1/-1}.art-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.art{border-top:4px solid var(--blue);padding-top:12px}.art p:last-child{margin-bottom:0}.approval{border-bottom:1px solid #758491;min-height:36px;margin:4px 0 12px}.pending{display:inline-block;padding:3px 9px;background:var(--orange);font-weight:650;color:#623800}textarea{display:block;width:100%;min-height:176px;font:inherit;line-height:1.6;resize:vertical;background:white;border:1px solid #8596a5;border-radius:4px;padding:14px;margin-top:8px;color:var(--ink)}details{margin:0}summary{cursor:pointer;font-size:21px;font-weight:750;margin-bottom:16px}.original{margin-top:18px;padding:14px;background:#eef2f5}.original p{margin:0}.original strong{color:var(--blue)}footer{padding:20px 28px;display:flex;gap:20px;justify-content:space-between;align-items:center;background:#f5f4ed}footer p{margin:0;max-width:600px}.error{background:#ffead8;color:#792d0e}.summary.error small{color:#792d0e}.summary.error .scope-message{border-color:#b78469}#validation{margin:12px 0 0;font-weight:650}section,div,p,label{overflow-wrap:anywhere}@media(max-width:640px){body{padding:10px 8px}header,.summary,section.panel,footer{padding:20px}.worksheet{grid-template-columns:minmax(0,1fr)}section.panel:nth-child(odd){border-right:0}.summary-grid{grid-template-columns:1fr 1fr;gap:12px}.summary-grid>div:last-child{grid-column:1/-1;border-top:1px solid #6684aa;padding-top:10px}.art-grid{grid-template-columns:1fr}footer{flex-direction:column;align-items:stretch}.field-grid{gap:12px}.sizes{gap:10px}}@media(prefers-reduced-motion:no-preference){button{transition:background .15s}}
</style>
</head>
<body>
<main>
<header>
<p class="eyebrow">QT01 / QUOTE WORKSHEET</p>
<h1>T-shirt run, priced by location</h1>
<p>Fictional sample · USD · Illustrative rates, not market prices. Artwork approval is blank; no production is authorized.</p>
</header>
<section class="summary" id="quote-summary" aria-label="Current quote summary" aria-live="polite">
<div class="summary-head"><p><strong id="quantity-result">20 shirts</strong></p><p id="selection-result">Front only selected</p></div>
<div class="summary-grid">
<div><small>Front only</small><span class="amount" id="base-result" data-output>$205.00</span></div>
<div><small>Back addition</small><span class="amount" id="back-result" data-output>+$65.00</span></div>
<div><small>Selected known subtotal</small><span class="amount" id="selected-result" data-output>$205.00</span></div>
</div>
<p class="scope-message" id="match-state">Size sum matches the request for 20 shirts.</p>
<p id="validation" role="alert" hidden></p>
</section>
<div class="worksheet">
<section class="panel">
<fieldset><legend>01 / Reconcile sizes</legend>
<label for="requested">Requested total<input id="requested" type="number" min="1" max="10000" step="1" value="20" inputmode="numeric" aria-describedby="size-help"></label>
<div class="sizes">
<label for="size-s">Small<input id="size-s" type="number" min="0" max="10000" step="1" value="5" inputmode="numeric"></label>
<label for="size-m">Medium<input id="size-m" type="number" min="0" max="10000" step="1" value="8" inputmode="numeric"></label>
<label for="size-l">Large<input id="size-l" type="number" min="0" max="10000" step="1" value="7" inputmode="numeric"></label>
</div>
<p class="help" id="size-help">Try changing Large to 9. Prices use the size sum; the requested total stays separate until you reconcile it.</p>
</fieldset>
<div class="original"><p><strong>Original QT01, kept for comparison</strong><br>S5 / M8 / L7 = 20 shirts<br>Front $205 · Back +$65 · Both $270<br>Original rates: $6 blank, $3 front, $2 back; $25 setup at each location.</p></div>
</section>
<section class="panel" aria-labelledby="pricing-heading">
<h2 id="pricing-heading">02 / Price the run</h2>
<div class="ticket-line"><span>Blank garments</span><strong id="garment-line" data-output>$120.00</strong></div>
<div class="ticket-line"><span>Front printing</span><strong id="front-line" data-output>$60.00</strong></div>
<div class="ticket-line"><span>Front setup × 1</span><strong id="front-setup-line" data-output>$25.00</strong></div>
<div class="ticket-line total"><span>Front only</span><strong id="front-total-line" data-output>$205.00</strong></div>
<div class="option">
<h3>Optional back imprint</h3>
<p id="back-breakdown" data-output>20 × $2.00 + $25.00 setup = $65.00</p>
<button type="button" id="back-option" aria-pressed="false">Add back print</button>
</div>
<p class="help">Known subtotal only. Tax, fulfillment charges and order terms: TO CONFIRM.</p>
</section>
<section class="panel full">
<details><summary>Adjust the illustrative rates</summary>
<p>One garment rate across S, M and L. Each setup is charged once per location, not per size. Confirm your shop's rates and setup policy before quoting.</p>
<div class="field-grid">
<label for="blank-rate">Blank shirt ($/shirt)<input id="blank-rate" type="number" min="0" max="10000" step="0.01" value="6" inputmode="decimal"></label>
<label for="front-rate">Front print ($/shirt)<input id="front-rate" type="number" min="0" max="10000" step="0.01" value="3" inputmode="decimal"></label>
<label for="front-setup">Front setup ($ once)<input id="front-setup" type="number" min="0" max="10000" step="0.01" value="25" inputmode="decimal"></label>
<label for="back-rate">Back print ($/shirt)<input id="back-rate" type="number" min="0" max="10000" step="0.01" value="2" inputmode="decimal"></label>
<label for="back-setup">Back setup ($ once)<input id="back-setup" type="number" min="0" max="10000" step="0.01" value="25" inputmode="decimal"></label>
</div>
</details>
</section>
<section class="panel full" aria-labelledby="art-heading">
<h2 id="art-heading">03 / Match artwork to placement</h2>
<div class="art-grid">
<div class="art"><h3>Front · included</h3><p>ART01 mock reference · proposed one-color imprint</p><p>Actual revision, dimensions, placement and ink: TO CONFIRM</p><p>Artwork approval (not supplied)</p><div class="approval" role="img" aria-label="Front artwork approval: blank"></div></div>
<div class="art"><h3 id="back-art-heading">Back · optional, not selected</h3><p>ART01 mock reference · proposed one-color imprint at a separate location</p><p>Actual revision, dimensions, placement and ink: TO CONFIRM</p><p>Artwork approval (not supplied)</p><div class="approval" role="img" aria-label="Back artwork approval: blank"></div></div>
</div>
<p class="help">Garment SKU, color and size availability: TO CONFIRM. Production date, pickup/shipping and customer order decision: TO CONFIRM. A mock reference is not artwork approval.</p>
</section>
<section class="panel full">
<h2>04 / Clarify the revision</h2>
<p><span class="pending">Unsent draft</span> Review this text before using it in your usual customer conversation.</p>
<label for="clarification">Quantity and print-location question</label>
<textarea id="clarification" readonly></textarea>
</section>
</div>
<footer><p>Temporary worksheet: changes are not saved. Keep confirmed prices, artwork approvals and commercial terms in your maintained quote. This page does not send or accept orders.</p><button type="button" id="reset" class="secondary">Reset QT01</button></footer>
<noscript><p>Enable JavaScript to change quantities and calculate a revised quote. The displayed baseline is a fictional 20-shirt sample.</p></noscript>
</main>
<script>
(() => {
'use strict';
const find = id => document.getElementById(id);
const defaults = {'requested':'20','size-s':'5','size-m':'8','size-l':'7','blank-rate':'6','front-rate':'3','front-setup':'25','back-rate':'2','back-setup':'25'};
const quantityIds = ['requested','size-s','size-m','size-l'];
const rateIds = ['blank-rate','front-rate','front-setup','back-rate','back-setup'];
const currency = cents => '$' + (cents / 100).toFixed(2);
let includeBack = false;
function update() {
const values = {};
const problems = [];
for (const id of [...quantityIds, ...rateIds]) {
const node = find(id);
const raw = node.value.trim();
const isQuantity = quantityIds.includes(id);
const pattern = isQuantity ? /^\d+$/ : /^\d+(?:\.\d{1,2})?$/;
const number = Number(raw);
const valid = pattern.test(raw) && Number.isFinite(number) && number <= 10000 && (id !== 'requested' || number >= 1);
node.setAttribute('aria-invalid', String(!valid));
if (!valid) problems.push(id);
values[id] = isQuantity ? number : Math.round(number * 100);
}
const count = values['size-s'] + values['size-m'] + values['size-l'];
if (!problems.length && count === 0) {
problems.push('empty-run');
['size-s','size-m','size-l'].forEach(id => find(id).setAttribute('aria-invalid', 'true'));
}
const toggle = find('back-option');
toggle.textContent = includeBack ? 'Remove back print' : 'Add back print';
toggle.setAttribute('aria-pressed', String(includeBack));
find('back-art-heading').textContent = includeBack ? 'Back · selected for comparison' : 'Back · optional, not selected';
find('selection-result').textContent = includeBack ? 'Front + back selected' : 'Front only selected';
const invalid = problems.length > 0;
find('validation').hidden = !invalid;
find('quote-summary').classList.toggle('error', invalid);
if (invalid) {
const message = problems.includes('empty-run') ? 'Enter at least one shirt in the size list.' : 'Enter whole quantities from 0 to 10,000 (request at least 1), and nonnegative rates up to $10,000 with at most 2 decimals.';
find('validation').textContent = message;
find('quantity-result').textContent = 'Check the inputs';
find('match-state').textContent = 'No current quote calculation. Correct the highlighted inputs.';
document.querySelectorAll('[data-output]').forEach(node => { node.textContent = 'Unavailable'; });
find('clarification').value = 'Correct the quantity or rate inputs before preparing a clarification.';
return;
}
find('validation').textContent = '';
const garment = count * values['blank-rate'];
const front = count * values['front-rate'];
const base = garment + front + values['front-setup'];
const addition = count * values['back-rate'] + values['back-setup'];
const selected = base + (includeBack ? addition : 0);
find('quantity-result').textContent = count + ' shirts';
find('base-result').textContent = currency(base);
find('back-result').textContent = '+' + currency(addition);
find('selected-result').textContent = currency(selected);
find('garment-line').textContent = currency(garment);
find('front-line').textContent = currency(front);
find('front-setup-line').textContent = currency(values['front-setup']);
find('front-total-line').textContent = currency(base);
find('back-breakdown').textContent = count + ' × ' + currency(values['back-rate']) + ' + ' + currency(values['back-setup']) + ' setup = ' + currency(addition);
const requested = values.requested;
const matched = count === requested;
find('match-state').textContent = matched ? 'Size sum matches the request for ' + requested + ' shirts.' : count + ' shirts in the size list; request is ' + requested + '. Confirm the ' + Math.abs(count - requested) + (count > requested ? ' extra shirts' : ' missing shirts') + '. These prices are a conditional alternative.';
find('clarification').value = 'QT01 originally listed S5/M8/L7, totaling 20 shirts. The current size list is S' + values['size-s'] + '/M' + values['size-m'] + '/L' + values['size-l'] + ', totaling ' + count + ' against a request for ' + requested + '. Please confirm the intended size quantities and ' + (includeBack ? 'the front and optional back imprints' : 'whether the front-only option is wanted') + '. The current known subtotal is ' + currency(selected) + ', subject to shop pricing, artwork and fulfillment confirmation; tax and fulfillment charges are excluded. No production has been authorized.';
}
[...quantityIds, ...rateIds].forEach(id => find(id).addEventListener('input', update));
find('back-option').addEventListener('click', () => { includeBack = !includeBack; update(); });
find('reset').addEventListener('click', () => {
Object.entries(defaults).forEach(([id,value]) => { find(id).value = value; });
includeBack = false;
update();
});
update();
})();
</script>
</body>
</html>