:root {
--bg: #f5f7fa;
--surface: #ffffff;
--surface2: #f0f2f5;
--border: rgba(0,0,0,0.1);
--border2: rgba(0,0,0,0.18);
--text: #1a1a1a;
--text2: #555;
--text3: #888;
--teal: #1D9E75;
--teal-dark: #0F6E56;
--teal-light: #E1F5EE;
--amber-bg: #FAEEDA;
--amber-text: #854F0B;
--amber-border: #EF9F27;
--green-bg: #EAF3DE;
--green-text: #3B6D11;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--surface: #1c1c1e;
--surface2: #2a2a2c;
--border: rgba(255,255,255,0.1);
--border2: rgba(255,255,255,0.2);
--text: #f0f0f0;
--text2: #aaa;
--text3: #666;
--teal-light: #042e22;
--amber-bg: #2e2000;
--amber-text: #EF9F27;
--amber-border: #854F0B;
--green-bg: #172200;
--green-text: #97C459;
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 1rem 4rem;
}
header {
text-align: center;
margin-bottom: 2rem;
}
header .logo {
display: inline-flex;
align-items: center;
gap: 10px;
margin-bottom: .75rem;
}
header .logo i {
font-size: 32px;
color: var(--teal);
}
header h1 {
font-size: 26px;
font-weight: 600;
color: var(--text);
}
header p {
font-size: 14px;
color: var(--text2);
margin-top: .25rem;
}
.card {
background: var(--surface);
border: 0.5px solid var(--border);
border-radius: var(--radius-lg);
padding: 1.5rem;
width: 100%;
max-width: 640px;
}
/* Drop Zone */
.drop-zone {
border: 1.5px dashed var(--border2);
border-radius: var(--radius-md);
padding: 2.5rem 1.5rem;
text-align: center;
cursor: pointer;
transition: background .15s, border-color .15s;
background: var(--surface2);
}
.drop-zone.drag-over {
border-color: var(--teal);
background: var(--teal-light);
}
.drop-zone .drop-icon {
font-size: 36px;
color: var(--teal);
display: block;
margin-bottom: .75rem;
}
.drop-zone .drop-title {
font-size: 15px;
font-weight: 500;
margin-bottom: .3rem;
}
.drop-zone .drop-sub {
font-size: 13px;
color: var(--text2);
}
.btn-browse {
display: inline-block;
margin-top: 1rem;
padding: 7px 20px;
border-radius: var(--radius-sm);
border: 1.5px solid var(--teal);
color: var(--teal);
font-size: 13px;
font-weight: 500;
cursor: pointer;
background: transparent;
transition: background .15s;
}
.btn-browse:hover { background: var(--teal-light); }
/* File List */
.file-list { margin-top: 1rem; display: flex; flex-direction: column; gap: 8px; }
.file-card {
background: var(--surface);
border: 0.5px solid var(--border);
border-radius: var(--radius-sm);
padding: .7rem 1rem;
display: flex;
align-items: center;
gap: 12px;
}
.file-card .file-icon { font-size: 22px; color: var(--teal); flex-shrink: 0; }
.file-info { flex: 1; min-width: 0; }
.file-name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-meta { font-size: 11px; color: var(--text3); margin-top: 2px; }
.badge {
font-size: 11px;
padding: 2px 9px;
border-radius: 20px;
font-weight: 500;
flex-shrink: 0;
}
.badge-primary { background: var(--teal-light); color: var(--teal-dark); }
.badge-ok { background: var(--green-bg); color: var(--green-text); }
.remove-btn {
background: none;
border: none;
cursor: pointer;
color: var(--text3);
font-size: 18px;
display: flex;
align-items: center;
padding: 2px;
border-radius: 4px;
flex-shrink: 0;
transition: color .15s;
}
.remove-btn:hover { color: #E24B4A; }
/* Warning Banner */
.warn-banner {
margin-top: .75rem;
background: var(--amber-bg);
border: 0.5px solid var(--amber-border);
border-radius: var(--radius-sm);
padding: .6rem .9rem;
font-size: 12px;
color: var(--amber-text);
display: none;
align-items: center;
gap: 8px;
}
/* Options Panel */
.options-panel {
margin-top: 1rem;
background: var(--surface2);
border: 0.5px solid var(--border);
border-radius: var(--radius-md);
padding: 1rem 1.25rem;
display: none;
}
.opt-title {
font-size: 11px;
font-weight: 600;
color: var(--text3);
text-transform: uppercase;
letter-spacing: .06em;
margin-bottom: .85rem;
}
.opt-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: .65rem;
}
.opt-label { font-size: 13px; }
.opt-label small { display: block; font-size: 11px; color: var(--text3); margin-top: 1px; }
.opt-row select {
font-size: 13px;
padding: 5px 10px;
border-radius: var(--radius-sm);
border: 0.5px solid var(--border2);
background: var(--surface);
color: var(--text);
cursor: pointer;
max-width: 200px;
}
.toggle-wrap {
display: flex;
align-items: center;
gap: 7px;
font-size: 13px;
cursor: pointer;
}
input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--teal); cursor: pointer; }
/* Merge Button */
.merge-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
margin-top: 1rem;
padding: 12px;
border-radius: var(--radius-sm);
background: var(--teal);
color: #fff;
font-size: 14px;
font-weight: 500;
border: none;
cursor: pointer;
transition: background .15s;
}
.merge-btn:hover:not(:disabled) { background: var(--teal-dark); }
.merge-btn:disabled { opacity: .5; cursor: not-allowed; }
/* Progress */
.progress-wrap { margin-top: 1rem; display: none; }
.progress-bar-bg {
height: 6px;
background: var(--surface2);
border-radius: 3px;
overflow: hidden;
margin-bottom: .5rem;
}
.progress-bar {
height: 100%;
background: var(--teal);
border-radius: 3px;
width: 0;
transition: width .3s;
}
.progress-label { font-size: 12px; color: var(--text2); text-align: center; }
/* Summary */
.summary { margin-top: 1rem; display: none; }
.summary-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: var(--teal);
margin-bottom: 1rem;
}
.summary-header i { font-size: 20px; }
.summary-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 1rem;
}
.stat-card {
background: var(--surface2);
border-radius: var(--radius-sm);
padding: .85rem;
text-align: center;
}
.stat-num { font-size: 24px; font-weight: 600; color: var(--teal); }
.stat-lbl { font-size: 11px; color: var(--text3); margin-top: 3px; }
.download-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 11px;
border-radius: var(--radius-sm);
background: transparent;
border: 1.5px solid var(--teal);
color: var(--teal);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background .15s;
}
.download-btn:hover { background: var(--teal-light); }
.reset-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
padding: 9px;
margin-top: 8px;
border-radius: var(--radius-sm);
background: transparent;
border: 0.5px solid var(--border2);
color: var(--text2);
font-size: 13px;
cursor: pointer;
transition: background .15s;
}
.reset-btn:hover { background: var(--surface2); }
footer {
margin-top: 2.5rem;
font-size: 12px;
color: var(--text3);
text-align: center;
}
Combine multiple CSV files into one — free, private, no upload needed.
<!-- Drop Zone -->
<div class="drop-zone" id="dropZone">
<i class="ti ti-file-plus drop-icon"></i>
<div class="drop-title">Drag & drop your CSV files here</div>
<div class="drop-sub">Supports multiple files at once</div>
<button class="btn-browse" onclick="document.getElementById('fileInput').click()">Browse files</button>
<input type="file" id="fileInput" accept=".csv" multiple style="display:none">
</div>
<!-- File List -->
<div class="file-list" id="fileList"></div>
<!-- Warning Banner -->
<div class="warn-banner" id="warnBanner">
<i class="ti ti-alert-triangle"></i>
Some files have mismatched column headers. Enable "Allow column mismatch" to merge anyway.
</div>
<!-- Options Panel -->
<div class="options-panel" id="optionsPanel">
<div class="opt-title">Merge options</div>
<div class="opt-row">
<div class="opt-label">
Primary file
<small>Its headers become the output template</small>
</div>
<select id="primarySelect"></select>
</div>
<div class="opt-row" style="margin-top:.25rem">
<label class="toggle-wrap">
<input type="checkbox" id="skipDupes"> Skip duplicate rows
</label>
<label class="toggle-wrap">
<input type="checkbox" id="allowMismatch"> Allow column mismatch
</label>
</div>
</div>
<!-- Merge Button -->
<button class="merge-btn" id="mergeBtn" disabled>
<i class="ti ti-git-merge"></i> Merge CSV files
</button>
<!-- Progress -->
<div class="progress-wrap" id="progressWrap">
<div class="progress-bar-bg"><div class="progress-bar" id="progressBar"></div></div>
<div class="progress-label" id="progressLabel">Merging...</div>
</div>
<!-- Summary -->
<div class="summary" id="summary">
<div class="summary-header"><i class="ti ti-circle-check"></i> Merge complete</div>
<div class="summary-grid">
<div class="stat-card"><div class="stat-num" id="statFiles">0</div><div class="stat-lbl">Files merged</div></div>
<div class="stat-card"><div class="stat-num" id="statRows">0</div><div class="stat-lbl">Total rows</div></div>
<div class="stat-card"><div class="stat-num" id="statCols">0</div><div class="stat-lbl">Columns</div></div>
</div>
<button class="download-btn" id="downloadBtn"><i class="ti ti-download"></i> Download merged.csv</button>
<button class="reset-btn" id="resetBtn"><i class="ti ti-refresh"></i> Merge more files</button>
</div>
${f.name}
${fmtSize(f.size)}