1791 lines
107 KiB
HTML
1791 lines
107 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Oracle Autonomous AI Database — Flavor & Version Advisor</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,500;0,9..40,700;1,9..40,300&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--oracle-red: #C74634;
|
||
--surface-0: #F5F3F0;
|
||
--surface-1: #FFFFFF;
|
||
--surface-2: #F9F8F6;
|
||
--surface-3: #EDECEA;
|
||
--surface-4: #E2E0DD;
|
||
--border: #D6D3D0;
|
||
--border-light: #C5C2BF;
|
||
--text-primary: #1C1917;
|
||
--text-secondary: #57534E;
|
||
--text-muted: #A8A29E;
|
||
--shared-color: #2563EB;
|
||
--shared-bg: rgba(37,99,235,0.08);
|
||
--spool-color: #9333EA;
|
||
--spool-bg: rgba(147,51,234,0.08);
|
||
--pool-color: #D97706;
|
||
--pool-bg: rgba(217,119,6,0.08);
|
||
--dedicated-color: #059669;
|
||
--dedicated-bg: rgba(5,150,105,0.08);
|
||
--dedpool-color: #0891B2;
|
||
--dedpool-bg: rgba(8,145,178,0.08);
|
||
--accent-glow: rgba(199,70,52,0.08);
|
||
}
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
html { font-size: 18px; }
|
||
body { font-family: 'DM Sans', sans-serif; background: var(--surface-0); color: var(--text-primary); line-height: 1.6; min-height: 100vh; font-size: 1rem; }
|
||
|
||
.header { background: linear-gradient(135deg, var(--surface-1) 0%, var(--surface-0) 100%); border-bottom: 1px solid var(--border); padding: 2rem 2rem 1.5rem; position: relative; overflow: hidden; }
|
||
.header::before { content: ''; position: absolute; top: -50%; right: -10%; width: 500px; height: 500px; background: radial-gradient(circle, var(--accent-glow) 0%, transparent 70%); pointer-events: none; }
|
||
.header-content { max-width: 1400px; margin: 0 auto; position: relative; z-index: 1; }
|
||
.header-badge { display: inline-flex; align-items: center; gap: 0.5rem; background: var(--surface-3); border: 1px solid var(--border-light); padding: 0.25rem 0.75rem; border-radius: 100px; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-secondary); margin-bottom: 1rem; }
|
||
.header-badge .dot { width: 6px; height: 6px; background: var(--oracle-red); border-radius: 50%; animation: pulse 2s infinite; }
|
||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
||
.header h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.03em; margin-bottom: 0.25rem; }
|
||
.header h1 span { color: var(--oracle-red); }
|
||
.header p { color: var(--text-secondary); font-size: 1rem; }
|
||
.main-content { max-width: 1400px; margin: 0 auto; padding: 0 2rem 3rem; }
|
||
input[name="main-tab"] { display: none; }
|
||
.tab-nav { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 2rem; position: sticky; top: 0; background: var(--surface-0); z-index: 100; padding-top: 1rem; overflow-x: auto; transition: transform 0.25s ease; }
|
||
.tab-nav.nav-hidden { transform: translateY(-100%); }
|
||
.tab-nav label { padding: 0.75rem 1.25rem; font-size: 0.95rem; font-weight: 500; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; transition: all 0.2s; white-space: nowrap; user-select: none; }
|
||
.charts-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin: 1.5rem 0; }
|
||
.chart-card { background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px; padding: 1rem; display: flex; flex-direction: column; align-items: center; }
|
||
.chart-title { font-size: 0.82rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.75rem; text-align: center; }
|
||
.chart-card canvas { max-width: 100%; height: auto; }
|
||
@media (max-width: 900px) { .charts-row { grid-template-columns: 1fr; } .matrix-body { flex-direction: column; } .matrix-score-panel { position: static; flex: none; width: 100%; display: flex; gap: 0.5rem; flex-wrap: wrap; } .matrix-score-panel .score-chip { width: auto; flex: 1; min-width: 150px; } }
|
||
.tab-nav label:hover { color: var(--text-secondary); }
|
||
.share-link { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; color: var(--text-muted); cursor: pointer; padding: 0.2rem 0.5rem; border-radius: 5px; border: 1px solid transparent; transition: all 0.15s; background: none; font-family: inherit; }
|
||
.share-link:hover { color: var(--oracle-red); border-color: var(--border); background: var(--surface-2); }
|
||
.share-link svg { width: 13px; height: 13px; }
|
||
.share-link.copied { color: #10B981; border-color: rgba(16,185,129,0.3); }
|
||
.section-header-row { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 0.25rem; }
|
||
#tab-overview:checked ~ .tab-nav label[for="tab-overview"],
|
||
#tab-scenarios:checked ~ .tab-nav label[for="tab-scenarios"],
|
||
#tab-matrix:checked ~ .tab-nav label[for="tab-matrix"],
|
||
#tab-cost:checked ~ .tab-nav label[for="tab-cost"] { color: var(--oracle-red); border-bottom-color: var(--oracle-red); }
|
||
.tab-panel { display: none; animation: fadeIn 0.3s ease; }
|
||
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
|
||
#tab-overview:checked ~ .tab-panels #panel-overview,
|
||
#tab-scenarios:checked ~ .tab-panels #panel-scenarios,
|
||
#tab-matrix:checked ~ .tab-panels #panel-matrix,
|
||
#tab-cost:checked ~ .tab-panels #panel-cost { display: block; }
|
||
|
||
/* Cost table */
|
||
.cost-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; margin-bottom: 1.5rem; }
|
||
.cost-table thead th { background: var(--surface-2); padding: 0.75rem 1rem; text-align: right; font-weight: 600; font-size: 0.85rem; color: var(--text-secondary); border-bottom: 1px solid var(--border); }
|
||
.cost-table thead th:first-child { text-align: left; border-radius: 12px 0 0 0; }
|
||
.cost-table thead th:last-child { border-radius: 0 12px 0 0; }
|
||
.cost-table thead th.shared { color: var(--shared-color); }
|
||
.cost-table thead th.pool { color: var(--pool-color); }
|
||
.cost-table thead th.dedicated { color: var(--dedicated-color); }
|
||
.cost-table tbody td { padding: 0.75rem 1rem; vertical-align: middle; border-bottom: 1px solid var(--border); color: var(--text-secondary); text-align: right; font-family: 'JetBrains Mono', monospace; font-size: 0.95rem; }
|
||
.cost-table tbody tr:last-child td { border-bottom: none; }
|
||
.cost-table tbody tr:hover td { background: var(--surface-2); }
|
||
.cost-table tbody td:first-child { text-align: left; font-family: 'DM Sans', sans-serif; font-weight: 600; color: var(--text-primary); }
|
||
.cost-table .best-price { color: #10B981; font-weight: 700; }
|
||
.cost-table .pct-badge { display: inline-block; font-size: 0.72rem; padding: 0.1rem 0.4rem; border-radius: 4px; margin-left: 0.4rem; font-weight: 600; }
|
||
.cost-table .pct-badge.more { background: rgba(239,68,68,0.15); color: #F87171; }
|
||
.cost-table .pct-badge.best { background: rgba(16,185,129,0.12); color: #10B981; }
|
||
|
||
.cost-input-group { display: flex; flex-direction: column; gap: 0.3rem; }
|
||
.cost-input-group label { font-size: 0.78rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.04em; }
|
||
.cost-input-group input { background: var(--surface-3); border: 1px solid var(--border-light); color: var(--text-primary); padding: 0.55rem 0.75rem; border-radius: 8px; font-family: 'JetBrains Mono', monospace; font-size: 0.95rem; width: 100%; transition: border-color 0.2s; }
|
||
.cost-input-group input:focus { outline: none; border-color: var(--oracle-red); box-shadow: 0 0 0 2px rgba(199,70,52,0.2); }
|
||
.cost-input-hint { font-size: 0.7rem; color: var(--pool-color); font-style: italic; min-height: 1rem; }
|
||
.cost-result-cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.85rem; }
|
||
.cost-result-card { background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px; padding: 1.1rem; position: relative; overflow: hidden; min-width: 0; }
|
||
.cost-result-card.winner { border-color: #10B981; box-shadow: 0 0 12px rgba(16,185,129,0.15); }
|
||
.cost-result-card .flavor-tag { font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; font-weight: 600; padding: 0.15rem 0.5rem; border-radius: 5px; display: inline-block; margin-bottom: 0.5rem; }
|
||
.cost-result-card .flavor-tag.shared { background: var(--shared-bg); color: var(--shared-color); border: 1px solid rgba(59,130,246,0.25); }
|
||
.cost-result-card .flavor-tag.spool { background: var(--spool-bg); color: var(--spool-color); border: 1px solid rgba(168,85,247,0.25); }
|
||
.cost-result-card .flavor-tag.pool { background: var(--pool-bg); color: var(--pool-color); border: 1px solid rgba(245,158,11,0.25); }
|
||
.cost-result-card .flavor-tag.dedicated { background: var(--dedicated-bg); color: var(--dedicated-color); border: 1px solid rgba(16,185,129,0.25); }
|
||
.cost-result-card .flavor-tag.dedpool { background: var(--dedpool-bg); color: var(--dedpool-color); border: 1px solid rgba(6,182,212,0.25); }
|
||
.cost-result-card .total-price { font-family: 'JetBrains Mono', monospace; font-size: 1.5rem; font-weight: 700; margin-bottom: 0.35rem; }
|
||
.cost-result-card .total-price.shared { color: var(--shared-color); }
|
||
.cost-result-card .total-price.spool { color: var(--spool-color); }
|
||
.cost-result-card .total-price.pool { color: var(--pool-color); }
|
||
.cost-result-card .total-price.dedicated { color: var(--dedicated-color); }
|
||
.cost-result-card .total-price.dedpool { color: var(--dedpool-color); }
|
||
.cost-result-card .pct-vs { font-size: 0.75rem; font-weight: 600; margin-bottom: 0.6rem; }
|
||
.cost-result-card .breakdown-line { display: flex; justify-content: space-between; font-size: 0.78rem; color: var(--text-muted); padding: 0.15rem 0; }
|
||
.cost-result-card .breakdown-line span:last-child { font-family: 'JetBrains Mono', monospace; color: var(--text-secondary); }
|
||
.cost-result-card .winner-label { display: block; text-align: center; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; background: #10B981; color: #fff; padding: 0.25rem 0.6rem; border-radius: 6px; margin-bottom: 0.5rem; }
|
||
.cost-result-card .ecpu-note { font-size: 0.72rem; color: var(--pool-color); font-style: italic; margin-top: 0.4rem; padding-top: 0.4rem; border-top: 1px solid var(--border); }
|
||
.cost-totals-chip { display: inline-flex; align-items: center; gap: 0.4rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 0.35rem 0.7rem; font-size: 0.78rem; color: var(--text-secondary); }
|
||
.cost-totals-chip strong { color: var(--text-primary); font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; }
|
||
.cost-totals-chip.highlight strong { color: var(--pool-color); }
|
||
#ci_usage_pct { -webkit-appearance: none; appearance: none; width: 100%; height: 6px; background: var(--surface-4); border-radius: 3px; outline: none; cursor: pointer; }
|
||
#ci_usage_pct::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--oracle-red); cursor: pointer; border: 2px solid var(--surface-1); box-shadow: 0 0 6px rgba(199,70,52,0.4); }
|
||
#ci_usage_pct::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--oracle-red); cursor: pointer; border: 2px solid var(--surface-1); }
|
||
@media (max-width: 1200px) { .cost-result-cards { grid-template-columns: repeat(3, 1fr); } }
|
||
@media (max-width: 900px) { .cost-result-cards { grid-template-columns: repeat(2, 1fr); } }
|
||
@media (max-width: 600px) { .cost-result-cards { grid-template-columns: 1fr; } }
|
||
|
||
/* ── OCI Cost Estimator Style ── */
|
||
.oci-header-bar { background: linear-gradient(135deg, #2D6B3F 0%, #1A4528 100%); border-radius: 12px 12px 0 0; padding: 1rem 1.5rem; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem; }
|
||
.oci-header-left { display: flex; align-items: center; gap: 0.75rem; color: #fff; }
|
||
.oci-header-title { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.01em; }
|
||
.oci-header-sub { font-size: 0.72rem; color: rgba(255,255,255,0.65); margin-top: 0.15rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||
.oci-header-right { display: flex; align-items: center; gap: 1rem; }
|
||
.oci-header-cost-label { font-size: 0.72rem; color: rgba(255,255,255,0.7); text-transform: uppercase; letter-spacing: 0.04em; text-align: right; }
|
||
.oci-header-cost-value { font-family: 'JetBrains Mono', monospace; font-size: 1.6rem; font-weight: 700; color: #fff; text-align: right; }
|
||
|
||
.oci-config-card { background: var(--surface-1); border: 1px solid var(--border); border-top: none; border-radius: 0 0 12px 12px; padding: 1.25rem 1.5rem 1.5rem; margin-bottom: 1.5rem; }
|
||
.oci-config-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.75rem; padding-bottom: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--border); }
|
||
.oci-config-title { display: flex; align-items: center; gap: 0.5rem; font-size: 1rem; font-weight: 600; color: var(--text-primary); }
|
||
.oci-config-title svg { color: var(--text-muted); }
|
||
.oci-util-badge { display: flex; align-items: center; gap: 0.5rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 0.35rem 0.75rem; }
|
||
.oci-util-label { font-size: 0.72rem; color: var(--oracle-red); font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; }
|
||
.oci-util-value { font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; color: var(--text-primary); font-weight: 600; }
|
||
|
||
.oci-preset-row { margin-bottom: 1.25rem; }
|
||
.oci-field-label { display: block; font-size: 0.75rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.35rem; }
|
||
.oci-select { background: var(--surface-3); border: 1px solid var(--border-light); color: var(--text-primary); padding: 0.5rem 2.2rem 0.5rem 0.75rem; border-radius: 8px; font-family: 'DM Sans', sans-serif; font-size: 0.88rem; cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23A8A29E' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.7rem center; min-width: 280px; transition: border-color 0.2s; }
|
||
.oci-select:focus { outline: none; border-color: var(--oracle-red); }
|
||
|
||
.oci-fields-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 1.25rem; margin-bottom: 1.25rem; }
|
||
.oci-field { display: flex; flex-direction: column; }
|
||
.oci-input { background: var(--surface-3); border: 1px solid var(--border-light); color: var(--text-primary); padding: 0.6rem 0.85rem; border-radius: 8px; font-family: 'JetBrains Mono', monospace; font-size: 1rem; width: 100%; transition: border-color 0.2s; }
|
||
.oci-input:focus { outline: none; border-color: #2D6B3F; box-shadow: 0 0 0 2px rgba(45,107,63,0.2); }
|
||
.oci-field-hint { font-size: 0.7rem; color: var(--pool-color); font-style: italic; margin-top: 0.25rem; min-height: 1rem; }
|
||
.oci-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 6px; background: var(--surface-4); border-radius: 3px; outline: none; cursor: pointer; margin-top: 0.35rem; }
|
||
.oci-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%; background: #2D6B3F; cursor: pointer; border: 2px solid var(--surface-1); box-shadow: 0 0 6px rgba(45,107,63,0.4); }
|
||
.oci-slider::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%; background: #2D6B3F; cursor: pointer; border: 2px solid var(--surface-1); }
|
||
|
||
.oci-totals-row { display: flex; flex-wrap: wrap; gap: 0.75rem; padding-top: 1rem; border-top: 1px solid var(--border); }
|
||
|
||
.oci-billing-details { background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px; margin: 1.5rem 0; }
|
||
.oci-billing-details summary { padding: 0.85rem 1.25rem; cursor: pointer; font-size: 0.88rem; color: var(--text-secondary); font-weight: 600; }
|
||
.oci-billing-details summary:hover { color: var(--text-primary); }
|
||
.oci-billing-details[open] summary { border-bottom: 1px solid var(--border); }
|
||
.oci-billing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; padding: 1.25rem; }
|
||
|
||
.oci-verify-link { text-align: center; font-size: 0.82rem; color: var(--text-muted); margin-top: 0.5rem; }
|
||
.oci-verify-link a { color: #2D6B3F; text-decoration: underline; font-weight: 600; }
|
||
.oci-verify-link a:hover { color: #3A8C52; }
|
||
|
||
@media (max-width: 700px) {
|
||
.oci-header-bar { padding: 0.85rem 1rem; border-radius: 10px 10px 0 0; flex-direction: column; align-items: flex-start; }
|
||
.oci-header-cost-value { font-size: 1.3rem; }
|
||
.oci-config-card { padding: 1rem; }
|
||
.oci-fields-grid { grid-template-columns: 1fr 1fr; gap: 0.85rem; }
|
||
.oci-select { min-width: unset; width: 100%; }
|
||
}
|
||
@media (max-width: 480px) {
|
||
.oci-fields-grid { grid-template-columns: 1fr; }
|
||
.oci-billing-grid { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
/* Unified collapsible marker for all details */
|
||
details summary { list-style: none; cursor: pointer; }
|
||
details summary::-webkit-details-marker { display: none; }
|
||
details summary::marker { content: ""; }
|
||
details summary::before { content: "\2295"; font-size: 1.1rem; color: var(--oracle-red); margin-right: 0.5rem; font-weight: 400; vertical-align: middle; }
|
||
details[open] summary::before { content: "\2296"; }
|
||
.feature-matrix-details { background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.25rem; margin-bottom: 1.25rem; }
|
||
.feature-matrix-details[open] summary { margin-bottom: 0.5rem; }
|
||
.cloud-selector-label { font-size: 0.78rem; font-weight: 600; color: var(--text-secondary); margin-right: 0.25rem; }
|
||
.cloud-btn { font-size: 0.75rem; padding: 0.3rem 0.7rem; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-1); color: var(--text-secondary); cursor: pointer; transition: all 0.15s; font-weight: 500; }
|
||
.cloud-btn:hover { border-color: var(--text-muted); }
|
||
.cloud-btn.active { background: var(--oracle-red); color: #fff; border-color: var(--oracle-red); }
|
||
.cloud-doc-link { font-size: 0.7rem; text-decoration: none; opacity: 0.5; transition: opacity 0.15s; margin-right: 0.25rem; }
|
||
.cloud-doc-link:hover { opacity: 1; }
|
||
|
||
/* ── Feature Matrix Table ── */
|
||
.feature-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin-bottom: 1rem; }
|
||
.feature-table th { background: var(--surface-2); padding: 0.5rem 0.6rem; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 2px solid var(--border); text-align: center; position: sticky; top: 0; z-index: 2; }
|
||
.feature-table td { padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--border); }
|
||
.feat-cat-row td { background: var(--surface-3); font-weight: 700; font-size: 0.78rem; color: var(--text-primary); padding: 0.5rem 0.6rem; letter-spacing: 0.02em; }
|
||
.feat-cat-label { border-left: 3px solid var(--oracle-red); }
|
||
.feat-name { font-weight: 500; color: var(--text-primary); }
|
||
.feat-doc-link { color: var(--oracle-red); text-decoration: none; font-size: 0.7rem; }
|
||
.feat-ver { text-align: center; }
|
||
.ver-badge { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 4px; }
|
||
.ver-badge.v26 { background: rgba(147,51,234,0.12); color: #9333EA; }
|
||
.ver-badge.v19 { background: rgba(37,99,235,0.1); color: #2563EB; }
|
||
.ver-badge.vboth { background: rgba(100,116,139,0.1); color: #64748B; }
|
||
.feat-avail { text-align: center; font-size: 0.9rem; }
|
||
.feat-avail.yes { color: #059669; }
|
||
.feat-avail.no { color: #DC2626; opacity: 0.5; }
|
||
.avail-yes { font-weight: 700; }
|
||
.avail-no { font-weight: 400; }
|
||
.feat-req { text-align: center; }
|
||
.feat-checkbox { width: 16px; height: 16px; cursor: pointer; accent-color: var(--oracle-red); }
|
||
.feat-row.feat-required { background: rgba(199,70,52,0.06); }
|
||
.feat-row.feat-required .feat-name { color: var(--oracle-red); font-weight: 600; }
|
||
.score-chip.filtered-out { opacity: 0.2; pointer-events: none; }
|
||
.ver-rec { margin-left: 0.25rem; }
|
||
.ver-rec-tag { font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 4px; vertical-align: middle; }
|
||
.ver-rec-tag.v26 { background: rgba(147,51,234,0.12); color: #9333EA; }
|
||
.ver-rec-tag.vany { background: rgba(100,116,139,0.1); color: #64748B; }
|
||
.ver-rec-tag.excluded { background: rgba(220,38,38,0.08); color: #DC2626; font-style: italic; }
|
||
.filter-status-bar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; padding: 0.6rem 1rem; background: rgba(199,70,52,0.06); border: 1px solid rgba(199,70,52,0.15); border-radius: 8px; font-size: 0.82rem; color: var(--text-primary); margin-bottom: 1rem; }
|
||
.filter-status-left { display: flex; align-items: center; gap: 0.5rem; }
|
||
.filter-status-right { display: flex; align-items: center; gap: 0.3rem; flex-wrap: wrap; }
|
||
|
||
/* ── Deployment Hierarchy ── */
|
||
.hierarchy-container { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-bottom: 2rem; }
|
||
.hierarchy-world { border: 1px solid var(--border); border-radius: 12px; overflow: hidden; background: var(--surface-1); }
|
||
.hierarchy-world-header { padding: 0.85rem 1.15rem; display: flex; align-items: center; gap: 0.6rem; }
|
||
.hierarchy-world-header.serverless-header { background: linear-gradient(135deg, rgba(37,99,235,0.08), rgba(147,51,234,0.06)); border-bottom: 2px solid var(--shared-color); }
|
||
.hierarchy-world-header.dedicated-header { background: linear-gradient(135deg, rgba(5,150,105,0.08), rgba(8,145,178,0.06)); border-bottom: 2px solid var(--dedicated-color); }
|
||
.hierarchy-world-icon { font-size: 1.2rem; }
|
||
.hierarchy-world-title { font-size: 1rem; font-weight: 700; color: var(--text-primary); }
|
||
.hierarchy-world-sub { font-size: 0.72rem; color: var(--text-muted); margin-left: auto; }
|
||
.hierarchy-children { padding: 0.85rem 1rem; display: flex; flex-direction: column; gap: 0.5rem; }
|
||
.hierarchy-child { display: flex; align-items: center; gap: 0.6rem; padding: 0.45rem 0.7rem; border-radius: 8px; background: var(--surface-2); border: 1px solid var(--border); }
|
||
.hierarchy-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
||
.hierarchy-child.shared .hierarchy-dot { background: var(--shared-color); }
|
||
.hierarchy-child.spool .hierarchy-dot { background: var(--spool-color); }
|
||
.hierarchy-child.pool .hierarchy-dot { background: var(--pool-color); }
|
||
.hierarchy-child.dedicated .hierarchy-dot { background: var(--dedicated-color); }
|
||
.hierarchy-child.dedpool .hierarchy-dot { background: var(--dedpool-color); }
|
||
.hierarchy-child-name { font-size: 0.85rem; font-weight: 600; color: var(--text-primary); }
|
||
.hierarchy-child-desc { font-size: 0.72rem; color: var(--text-muted); margin-left: auto; }
|
||
@media (max-width: 700px) { .hierarchy-container { grid-template-columns: 1fr; } .hierarchy-world-sub { display: none; } }
|
||
|
||
.section-title { font-size: 1.35rem; font-weight: 700; margin-bottom: 0.25rem; letter-spacing: -0.02em; }
|
||
.section-subtitle { color: var(--text-muted); font-size: 0.95rem; margin-bottom: 1.5rem; }
|
||
.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
||
.driver-card { background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem; transition: border-color 0.2s; }
|
||
.driver-card:hover { border-color: var(--border-light); }
|
||
.driver-card h3 { font-size: 1.05rem; font-weight: 600; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
||
.driver-card h3 .icon { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 8px; font-size: 1rem; flex-shrink: 0; }
|
||
.flavor-row { display: grid; grid-template-columns: 140px 1fr; gap: 0.5rem; margin-bottom: 0.6rem; align-items: start; }
|
||
.flavor-label { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; font-weight: 600; padding: 0.25rem 0.5rem; border-radius: 6px; text-align: center; }
|
||
.flavor-label.shared { background: var(--shared-bg); color: var(--shared-color); border: 1px solid rgba(59,130,246,0.25); }
|
||
.flavor-label.spool { background: var(--spool-bg); color: var(--spool-color); border: 1px solid rgba(168,85,247,0.25); }
|
||
.flavor-label.pool { background: var(--pool-bg); color: var(--pool-color); border: 1px solid rgba(245,158,11,0.25); }
|
||
.flavor-label.dedicated { background: var(--dedicated-bg); color: var(--dedicated-color); border: 1px solid rgba(16,185,129,0.25); }
|
||
.flavor-label.dedpool { background: var(--dedpool-bg); color: var(--dedpool-color); border: 1px solid rgba(6,182,212,0.25); }
|
||
.flavor-desc { font-size: 0.92rem; color: var(--text-secondary); line-height: 1.5; }
|
||
.flavor-stars { margin-bottom: 2px; }
|
||
.score-bar-wrap { display: flex; align-items: center; gap: 0.5rem; }
|
||
.score-bar-num { font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; font-weight: 700; color: var(--text-secondary); min-width: 28px; }
|
||
.score-bar-track { flex: 1; height: 6px; background: var(--surface-4); border-radius: 3px; min-width: 60px; max-width: 120px; }
|
||
.score-bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s ease; }
|
||
.score-bar-fill.shared { background: var(--shared-color); }
|
||
.score-bar-fill.spool { background: var(--spool-color); }
|
||
.score-bar-fill.pool { background: var(--pool-color); }
|
||
.score-bar-fill.dedicated { background: var(--dedicated-color); }
|
||
.score-bar-fill.dedpool { background: var(--dedpool-color); }
|
||
.flavor-row-wrap { display: flex; flex-direction: column; gap: 0.15rem; }
|
||
|
||
.scenario-table { width: 100%; border-collapse: separate; border-spacing: 0; background: var(--surface-1); border: 1px solid var(--border); border-radius: 12px; margin-bottom: 2rem; font-size: 0.95rem; }
|
||
.scenario-table thead th { background: var(--surface-2); padding: 0.75rem 1rem; text-align: left; font-weight: 600; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-secondary); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10; }
|
||
.scenario-table thead th:first-child { border-radius: 12px 0 0 0; }
|
||
.scenario-table thead th:last-child { border-radius: 0 12px 0 0; }
|
||
.scenario-table tbody td { padding: 0.85rem 1rem; vertical-align: top; border-bottom: 1px solid var(--border); color: var(--text-secondary); line-height: 1.55; }
|
||
.scenario-table tbody tr:last-child td { border-bottom: none; }
|
||
.scenario-table tbody tr:hover td { background: var(--surface-2); }
|
||
.scenario-table td:first-child { font-weight: 600; color: var(--text-primary); min-width: 140px; }
|
||
.scenario-table .takeaway { font-weight: 500; color: var(--text-primary); font-style: italic; }
|
||
|
||
.matrix-container { background: var(--surface-1); border: 1px solid var(--border); border-radius: 16px; padding: 1.5rem; margin-bottom: 2rem; }
|
||
.matrix-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem; }
|
||
.matrix-header h2 { font-size: 1.2rem; font-weight: 700; }
|
||
.matrix-header p { color: var(--text-muted); font-size: 0.92rem; }
|
||
.scenario-selector { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
|
||
.scenario-selector label { font-size: 0.9rem; color: var(--text-secondary); font-weight: 500; }
|
||
.scenario-selector select { background: var(--surface-3); border: 1px solid var(--border-light); color: var(--text-primary); padding: 0.5rem 2rem 0.5rem 0.75rem; border-radius: 8px; font-family: 'DM Sans', sans-serif; font-size: 0.92rem; cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23A8A29E' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 0.6rem center; }
|
||
.scenario-selector select:focus { outline: 1px solid var(--oracle-red); border-color: var(--oracle-red); }
|
||
.reset-btn { background: var(--surface-3); border: 1px solid var(--border-light); color: var(--text-secondary); padding: 0.45rem 1rem; border-radius: 8px; font-size: 0.85rem; cursor: pointer; font-family: 'DM Sans', sans-serif; transition: all 0.2s; }
|
||
.reset-btn:hover { background: var(--surface-4); color: var(--text-primary); }
|
||
|
||
.score-summary { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-bottom: 1.5rem; flex-direction: column; }
|
||
|
||
/* Decision Matrix split layout — score panel fixed left */
|
||
.matrix-body { display: flex; gap: 1.5rem; align-items: flex-start; }
|
||
.matrix-score-panel { position: sticky; top: 0.5rem; flex: 0 0 240px; }
|
||
.matrix-score-panel .score-chip { min-width: unset; width: 100%; margin-bottom: 0.5rem; }
|
||
.matrix-main-content { flex: 1; min-width: 0; max-height: 75vh; overflow-y: auto; overflow-x: auto; padding-right: 0.25rem; }
|
||
.matrix-main-content::-webkit-scrollbar { width: 6px; }
|
||
.matrix-main-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||
.matrix-main-content::-webkit-scrollbar-track { background: transparent; }
|
||
.score-chip { display: flex; align-items: center; gap: 0.5rem; background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; padding: 0.6rem 0.9rem; min-width: 210px; }
|
||
.score-chip .label { font-size: 0.8rem; font-weight: 500; color: var(--text-secondary); }
|
||
.score-chip .value { font-family: 'JetBrains Mono', monospace; font-size: 1.3rem; font-weight: 700; }
|
||
.score-chip.shared .value { color: var(--shared-color); }
|
||
.score-chip.spool .value { color: var(--spool-color); }
|
||
.score-chip.pool .value { color: var(--pool-color); }
|
||
.score-chip.dedicated .value { color: var(--dedicated-color); }
|
||
.score-chip.dedpool .value { color: var(--dedpool-color); }
|
||
.score-chip .bar-bg { flex: 1; height: 5px; background: var(--surface-4); border-radius: 2px; min-width: 30px; }
|
||
.score-chip .bar-fill { height: 100%; border-radius: 2px; transition: width 0.4s ease; }
|
||
.score-chip.shared .bar-fill { background: var(--shared-color); }
|
||
.score-chip.spool .bar-fill { background: var(--spool-color); }
|
||
.score-chip.pool .bar-fill { background: var(--pool-color); }
|
||
.score-chip.dedicated .bar-fill { background: var(--dedicated-color); }
|
||
.score-chip.dedpool .bar-fill { background: var(--dedpool-color); }
|
||
.winner-badge { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.15rem 0.55rem; border-radius: 6px; margin-left: 0.5rem; animation: fadeIn 0.3s; }
|
||
|
||
.how-to-box { background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.25rem; margin-bottom: 1.5rem; font-size: 0.92rem; color: var(--text-secondary); line-height: 1.6; }
|
||
.how-to-box strong { color: var(--text-primary); }
|
||
.how-to-box .formula-example { font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; color: var(--text-muted); background: var(--surface-3); padding: 0.5rem 0.75rem; border-radius: 6px; margin-top: 0.6rem; display: block; line-height: 1.7; }
|
||
|
||
.formula-panel { background: var(--surface-2); border: 1px solid var(--border); border-radius: 10px; padding: 0.85rem 1rem; }
|
||
.formula-panel summary { cursor: pointer; font-size: 0.9rem; font-weight: 600; color: var(--text-secondary); user-select: none; }
|
||
.formula-panel summary:hover { color: var(--text-primary); }
|
||
.formula-flavor { margin-top: 0.75rem; padding: 0.6rem 0.75rem; background: var(--surface-3); border-radius: 8px; margin-bottom: 0.5rem; }
|
||
.formula-flavor-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem; }
|
||
.formula-flavor-name { font-size: 0.88rem; font-weight: 600; }
|
||
.formula-flavor-name.shared { color: var(--shared-color); }
|
||
.formula-flavor-name.spool { color: var(--spool-color); }
|
||
.formula-flavor-name.pool { color: var(--pool-color); }
|
||
.formula-flavor-name.dedicated { color: var(--dedicated-color); }
|
||
.formula-steps { font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--text-muted); line-height: 1.8; }
|
||
.formula-steps .step-line { display: flex; gap: 0.3rem; }
|
||
.formula-steps .step-op { color: var(--text-muted); min-width: 12px; }
|
||
.formula-total { font-weight: 700; color: var(--text-primary); border-top: 1px solid var(--border); padding-top: 0.3rem; margin-top: 0.3rem; }
|
||
.winner-badge.shared { background: var(--shared-bg); color: var(--shared-color); }
|
||
.winner-badge.spool { background: var(--spool-bg); color: var(--spool-color); }
|
||
.winner-badge.pool { background: var(--pool-bg); color: var(--pool-color); }
|
||
.winner-badge.dedicated { background: var(--dedicated-bg); color: var(--dedicated-color); }
|
||
.winner-badge.dedpool { background: var(--dedpool-bg); color: var(--dedpool-color); }
|
||
|
||
.criteria-grid { display: grid; gap: 0; }
|
||
.criteria-row { display: grid; grid-template-columns: 1fr 1.5fr; border-bottom: 1px solid var(--border); transition: background 0.15s; }
|
||
.criteria-row:last-child { border-bottom: none; }
|
||
.criteria-row:hover { background: var(--surface-2); }
|
||
.criteria-left { padding: 1rem; border-right: 1px solid var(--border); }
|
||
.criteria-name-row { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.6rem; }
|
||
.criteria-name { font-weight: 600; font-size: 1rem; }
|
||
|
||
.info-trigger { width: 20px; height: 20px; border-radius: 50%; background: var(--surface-4); border: 1px solid var(--border-light); display: inline-flex; align-items: center; justify-content: center; font-size: 0.7rem; font-weight: 700; color: var(--text-muted); cursor: pointer; flex-shrink: 0; transition: all 0.2s; }
|
||
.info-trigger:hover { background: var(--oracle-red); color: #fff; border-color: var(--oracle-red); }
|
||
.info-trigger.active { background: var(--oracle-red); color: #fff; border-color: var(--oracle-red); }
|
||
.info-panel { display: none; grid-column: 1 / -1; background: var(--surface-3); border-bottom: 1px solid var(--border); padding: 1rem 1.25rem; animation: fadeIn 0.2s ease; }
|
||
.info-panel.open { display: block; }
|
||
.info-panel .info-desc { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 0.75rem; font-style: italic; line-height: 1.55; }
|
||
.info-panel .info-flavor { margin-bottom: 0.5rem; display: grid; grid-template-columns: 120px 1fr; gap: 0.4rem; align-items: start; }
|
||
.info-panel .info-flavor:last-child { margin-bottom: 0; }
|
||
.info-panel .info-flavor-label { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; font-weight: 600; padding: 0.2rem 0.45rem; border-radius: 4px; text-align: center; }
|
||
.info-panel .info-flavor-label.shared { background: var(--shared-bg); color: var(--shared-color); }
|
||
.info-panel .info-flavor-label.spool { background: var(--spool-bg); color: var(--spool-color); }
|
||
.info-panel .info-flavor-label.pool { background: var(--pool-bg); color: var(--pool-color); }
|
||
.info-panel .info-flavor-label.dedicated { background: var(--dedicated-bg); color: var(--dedicated-color); }
|
||
.info-panel .info-flavor-label.dedpool { background: var(--dedpool-bg); color: var(--dedpool-color); }
|
||
.info-panel .info-flavor-text { font-size: 0.88rem; color: var(--text-secondary); line-height: 1.5; }
|
||
|
||
.weight-control { display: flex; align-items: center; gap: 0.5rem; }
|
||
.weight-control label { font-size: 0.78rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; min-width: 62px; }
|
||
.weight-control input[type="range"] { flex: 1; -webkit-appearance: none; appearance: none; height: 5px; background: var(--surface-4); border-radius: 2px; outline: none; cursor: pointer; }
|
||
.weight-control input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--oracle-red); cursor: pointer; border: 2px solid var(--surface-1); box-shadow: 0 0 6px rgba(199,70,52,0.4); }
|
||
.weight-control input[type="range"]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--oracle-red); cursor: pointer; border: 2px solid var(--surface-1); }
|
||
.weight-pct { font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; color: var(--oracle-red); min-width: 30px; text-align: right; font-weight: 600; }
|
||
.criteria-right { padding: 1rem; }
|
||
.scores-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.4rem; }
|
||
.score-cell { text-align: center; }
|
||
.score-cell .score-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 0.3rem; }
|
||
.score-cell.shared .score-label { color: var(--shared-color); }
|
||
.score-cell.pool .score-label { color: var(--pool-color); }
|
||
.score-cell.dedicated .score-label { color: var(--dedicated-color); }
|
||
.score-dots { display: flex; gap: 4px; justify-content: center; }
|
||
.score-dot { width: 20px; height: 20px; border-radius: 5px; border: 1px solid var(--border); background: var(--surface-3); transition: all 0.2s; cursor: pointer; }
|
||
.score-dot.filled.shared { background: var(--shared-color); border-color: var(--shared-color); }
|
||
.score-dot.filled.spool { background: var(--spool-color); border-color: var(--spool-color); }
|
||
.score-dot.filled.pool { background: var(--pool-color); border-color: var(--pool-color); }
|
||
.score-dot.filled.dedicated { background: var(--dedicated-color); border-color: var(--dedicated-color); }
|
||
.score-dot.filled.dedpool { background: var(--dedpool-color); border-color: var(--dedpool-color); }
|
||
|
||
.ref-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
||
.ref-item { display: flex; align-items: center; gap: 0.75rem; background: var(--surface-1); border: 1px solid var(--border); border-radius: 10px; padding: 0.9rem 1rem; text-decoration: none; color: var(--text-primary); transition: all 0.2s; }
|
||
.ref-item:hover { border-color: var(--oracle-red); background: var(--surface-2); }
|
||
.ref-item .ref-icon { width: 34px; height: 34px; background: var(--surface-3); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 1rem; flex-shrink: 0; }
|
||
.ref-item .ref-text { flex: 1; }
|
||
.ref-item .ref-text strong { display: block; font-size: 0.95rem; font-weight: 600; }
|
||
.ref-item .ref-text span { font-size: 0.82rem; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; word-break: break-all; }
|
||
|
||
.notes-callout { background: var(--surface-2); border: 1px solid var(--border); border-left: 3px solid var(--oracle-red); border-radius: 0 10px 10px 0; padding: 1rem 1.25rem; margin-bottom: 2rem; }
|
||
.notes-callout h3 { font-size: 1rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||
.notes-callout ul { list-style: none; display: flex; flex-direction: column; gap: 0.4rem; }
|
||
.notes-callout li { font-size: 0.92rem; color: var(--text-secondary); padding-left: 1rem; position: relative; line-height: 1.55; }
|
||
.notes-callout li::before { content: '\203A'; position: absolute; left: 0; color: var(--oracle-red); font-weight: 700; }
|
||
|
||
/* Billing description cards */
|
||
.billing-card { background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px; padding: 1rem 1.15rem; }
|
||
.billing-card h4 { font-size: 0.95rem; font-weight: 700; margin-bottom: 0.5rem; }
|
||
.billing-card.shared h4 { color: var(--shared-color); }
|
||
.billing-card.spool h4 { color: var(--spool-color); }
|
||
.billing-card.pool h4 { color: var(--pool-color); }
|
||
.billing-card.dedicated h4 { color: var(--dedicated-color); }
|
||
.billing-card.dedpool h4 { color: var(--dedpool-color); }
|
||
.billing-card p { font-size: 0.82rem; color: var(--text-secondary); line-height: 1.5; margin-bottom: 0.5rem; }
|
||
.doc-link { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; color: var(--oracle-red); text-decoration: none; font-weight: 500; margin-top: 0.35rem; padding: 0.2rem 0.5rem; background: rgba(199,70,52,0.08); border-radius: 5px; transition: background 0.15s; }
|
||
.doc-link:hover { background: rgba(199,70,52,0.18); text-decoration: underline; }
|
||
.doc-link svg { width: 12px; height: 12px; flex-shrink: 0; }
|
||
.overview-doc-link { display: inline-flex; align-items: center; gap: 0.25rem; font-size: 0.68rem; color: var(--oracle-red); text-decoration: none; opacity: 0.8; transition: opacity 0.15s; }
|
||
.overview-doc-link:hover { opacity: 1; text-decoration: underline; }
|
||
.billing-formula { background: var(--surface-3); border-radius: 8px; padding: 0.6rem 0.75rem; margin: 0.5rem 0; display: flex; flex-direction: column; gap: 0.2rem; }
|
||
.billing-formula .billing-line { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--text-secondary); line-height: 1.6; }
|
||
.billing-formula .billing-line em { font-style: normal; color: var(--text-primary); font-weight: 600; min-width: 5.5rem; display: inline-block; }
|
||
.billing-card .billing-source { font-size: 0.75rem; color: var(--text-muted, #78716C); margin-bottom: 0; font-style: italic; }
|
||
.billing-details-toggle summary:hover { color: var(--oracle-red); }
|
||
|
||
@media (max-width: 900px) {
|
||
html { font-size: 17px; }
|
||
.overview-grid { grid-template-columns: 1fr; }
|
||
.criteria-row { grid-template-columns: 1fr; }
|
||
.criteria-left { border-right: none; border-bottom: 1px solid var(--border); }
|
||
.score-summary { flex-direction: column; }
|
||
.header h1 { font-size: 1.4rem; }
|
||
.header p { font-size: 0.95rem; }
|
||
.scenario-table { font-size: 0.82rem; display: block; overflow-x: auto; }
|
||
.main-content { padding: 0 1rem 2rem; }
|
||
.matrix-container { padding: 1rem; }
|
||
.matrix-header { flex-direction: column; }
|
||
.scenario-selector { width: 100%; }
|
||
.scenario-selector select { width: 100%; font-size: 0.85rem; }
|
||
.score-chip { min-width: unset; flex: 1; }
|
||
.info-panel .info-flavor { grid-template-columns: 1fr; }
|
||
.info-panel .info-flavor-label { justify-self: start; }
|
||
.cost-table { display: block; overflow-x: auto; }
|
||
.tab-nav label { font-size: 0.88rem; }
|
||
.section-title { font-size: 1.2rem; }
|
||
.driver-card h3 { font-size: 1rem; }
|
||
.flavor-desc { font-size: 0.92rem; }
|
||
.billing-card p { font-size: 0.85rem; }
|
||
.billing-formula .billing-line { font-size: 0.78rem; }
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
html { font-size: 16px; }
|
||
.header { padding: 1.25rem 1rem 1rem; }
|
||
.header h1 { font-size: 1.2rem; }
|
||
.header p { font-size: 0.85rem; }
|
||
.main-content { padding: 0 0.75rem 2rem; }
|
||
.tab-nav { padding-top: 0.5rem; }
|
||
.tab-nav label { padding: 0.6rem 0.75rem; font-size: 0.82rem; }
|
||
|
||
/* Section titles */
|
||
.section-title { font-size: 1.1rem; }
|
||
.section-subtitle { font-size: 0.88rem; }
|
||
|
||
/* Overview cards */
|
||
.driver-card { padding: 1rem; }
|
||
.driver-card h3 { font-size: 0.95rem; }
|
||
.flavor-row { grid-template-columns: 1fr; gap: 0.25rem; }
|
||
.flavor-label { justify-self: start; font-size: 0.7rem; }
|
||
.flavor-desc { font-size: 0.85rem; }
|
||
.score-bar-track { max-width: 100px; }
|
||
|
||
/* Scenario table → cards on mobile */
|
||
.scenario-table { font-size: 0.82rem; }
|
||
.scenario-table thead th { padding: 0.5rem 0.6rem; font-size: 0.75rem; }
|
||
.scenario-table tbody td { padding: 0.6rem; }
|
||
.scenario-table .takeaway { font-size: 0.82rem; }
|
||
|
||
/* Matrix */
|
||
.matrix-container { padding: 0.75rem; border-radius: 12px; }
|
||
.matrix-header h2 { font-size: 1.05rem; }
|
||
.how-to-box { font-size: 0.82rem; padding: 0.75rem; }
|
||
.how-to-box .formula-example { font-size: 0.75rem; padding: 0.4rem 0.6rem; }
|
||
|
||
/* Score chips stack */
|
||
.score-summary { gap: 0.5rem; }
|
||
.score-chip { padding: 0.5rem 0.65rem; }
|
||
.score-chip .label { font-size: 0.75rem; }
|
||
.score-chip .value { font-size: 1.1rem; }
|
||
.winner-badge { font-size: 0.68rem; padding: 0.15rem 0.4rem; }
|
||
|
||
/* Criteria rows */
|
||
.criteria-row { grid-template-columns: 1fr; }
|
||
.criteria-left { padding: 0.75rem; border-right: none; border-bottom: 1px solid var(--border); }
|
||
.criteria-name { font-size: 0.9rem; }
|
||
.weight-control label { font-size: 0.75rem; min-width: 55px; }
|
||
.weight-pct { font-size: 0.82rem; }
|
||
.criteria-right { padding: 0.75rem; }
|
||
.scores-row { grid-template-columns: 1fr; gap: 0.6rem; }
|
||
.score-cell { text-align: left; display: flex; align-items: center; gap: 0.5rem; }
|
||
.score-cell .score-label { margin-bottom: 0; min-width: 100px; font-size: 0.75rem; }
|
||
.score-dots { justify-content: flex-start; }
|
||
.score-dot { width: 26px; height: 26px; border-radius: 6px; }
|
||
|
||
/* Info panel */
|
||
.info-panel { padding: 0.75rem; }
|
||
.info-panel .info-desc { font-size: 0.82rem; }
|
||
.info-panel .info-flavor { grid-template-columns: 1fr; gap: 0.2rem; }
|
||
.info-panel .info-flavor-label { justify-self: start; font-size: 0.7rem; }
|
||
.info-panel .info-flavor-text { font-size: 0.82rem; }
|
||
|
||
/* Formula detail */
|
||
.formula-panel { padding: 0.65rem; }
|
||
.formula-panel summary { font-size: 0.82rem; }
|
||
.formula-flavor { padding: 0.5rem; }
|
||
.formula-steps { font-size: 0.72rem; line-height: 1.7; }
|
||
.formula-steps .step-line { flex-wrap: wrap; }
|
||
|
||
/* Cost estimator */
|
||
.cost-result-card .total-price { font-size: 1.25rem; }
|
||
.cost-result-card .breakdown-line { font-size: 0.82rem; }
|
||
.cost-result-card .flavor-tag { font-size: 0.72rem; }
|
||
.cost-result-card .ecpu-note { font-size: 0.78rem; }
|
||
.cost-totals-chip { font-size: 0.82rem; }
|
||
.cost-input-hint { font-size: 0.78rem; }
|
||
|
||
/* References */
|
||
.ref-item { padding: 0.65rem 0.75rem; }
|
||
.ref-item .ref-text strong { font-size: 0.85rem; }
|
||
.ref-item .ref-text span { font-size: 0.72rem; }
|
||
|
||
/* Notes */
|
||
.notes-callout { padding: 0.75rem 1rem; }
|
||
.notes-callout li { font-size: 0.82rem; }
|
||
.billing-cards { grid-template-columns: 1fr; }
|
||
.billing-card h4 { font-size: 0.95rem; }
|
||
.billing-card p, .billing-card .billing-source { font-size: 0.82rem; }
|
||
.billing-formula .billing-line { font-size: 0.75rem; }
|
||
.doc-link { font-size: 0.78rem; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="header">
|
||
<div class="header-content">
|
||
<div class="header-badge"><span class="dot"></span> Interactive Decision Tool</div>
|
||
<h1><span>Oracle</span> Autonomous AI Database — Flavor & Version Advisor</h1>
|
||
<p style="font-size:0.84rem;line-height:1.7;color:var(--text-secondary);">
|
||
<strong style="color:var(--text-primary);">Serverless</strong> — Standalone · Elastic Pool · Dedicated Elastic Pool<br>
|
||
<strong style="color:var(--text-primary);">Dedicated Exadata</strong> — ADB Dedicated · Elastic Pool on Dedicated<br>
|
||
<span style="font-size:0.76rem;color:var(--text-muted);">Oracle Database 19c & Oracle AI Database 26ai</span>
|
||
</p>
|
||
<div style="margin-top:1rem;padding:0.6rem 0.9rem;background:rgba(199,70,52,0.08);border:1px solid rgba(199,70,52,0.2);border-radius:8px;font-size:0.72rem;color:var(--text-secondary);line-height:1.5;">
|
||
<strong style="color:var(--oracle-red);">Disclaimer:</strong> This tool is <strong>not an official Oracle document</strong>. It was created to support internal business-case discussions. For official product documentation, pricing, and specifications please refer to the doc links in each flavor card on the <em>When to Choose</em> tab.
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-content">
|
||
<input type="radio" name="main-tab" id="tab-overview" checked>
|
||
<input type="radio" name="main-tab" id="tab-scenarios">
|
||
<input type="radio" name="main-tab" id="tab-matrix">
|
||
<input type="radio" name="main-tab" id="tab-cost">
|
||
<nav class="tab-nav">
|
||
<label for="tab-overview">When to Choose</label>
|
||
<label for="tab-scenarios">Scenarios</label>
|
||
<label for="tab-matrix">Decision Matrix</label>
|
||
<label for="tab-cost">Cost Estimator</label>
|
||
</nav>
|
||
<div class="tab-panels">
|
||
|
||
<!-- ════════════════ TAB 1: OVERVIEW ════════════════ -->
|
||
<div class="tab-panel" id="panel-overview">
|
||
<div class="notes-callout">
|
||
<h3>Assumptions & Notes</h3>
|
||
<ul>
|
||
<li>Pricing assumptions: ECPUs, BYOL, ATP, list price. No discounts/commits included.</li>
|
||
<li>Portability is treated as a single criterion (not split). Within Serverless, joining/leaving an Elastic Pool is click-based; moving to Dedicated Exadata is typically a migration exercise.</li>
|
||
<li>Scores are relative (1–5); importance (0–100) can be set independently per criterion to model stakeholder priorities.</li>
|
||
<li>For a concrete cost estimate, use the official <a href="https://www.oracle.com/cloud/costestimator.html" target="_blank" style="color:var(--oracle-red);text-decoration:underline;">OCI Cost Estimator</a>.</li>
|
||
</ul>
|
||
</div>
|
||
<div class="section-header-row">
|
||
<h2 class="section-title" style="margin-bottom:0">When to Choose Each Option</h2>
|
||
<button class="share-link" onclick="copyTabLink('overview')" title="Copy link to this tab">
|
||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/></svg>
|
||
<span>Share</span>
|
||
</button>
|
||
</div>
|
||
<p class="section-subtitle">Business driver comparison across the five deployment flavors</p>
|
||
|
||
<!-- Deployment Hierarchy -->
|
||
<div class="hierarchy-container">
|
||
<div class="hierarchy-world">
|
||
<div class="hierarchy-world-header serverless-header">
|
||
<span class="hierarchy-world-icon">☁️</span>
|
||
<span class="hierarchy-world-title">Serverless</span>
|
||
<span class="hierarchy-world-sub">Oracle-managed shared infrastructure</span>
|
||
</div>
|
||
<div class="hierarchy-children">
|
||
<div class="hierarchy-child shared">
|
||
<span class="hierarchy-dot"></span>
|
||
<span class="hierarchy-child-name">Serverless</span>
|
||
<span class="hierarchy-child-desc">Standalone pay-per-use</span>
|
||
</div>
|
||
<div class="hierarchy-child spool">
|
||
<span class="hierarchy-dot"></span>
|
||
<span class="hierarchy-child-name">Elastic Pool</span>
|
||
<span class="hierarchy-child-desc">Pooled compute, shared infra</span>
|
||
</div>
|
||
<div class="hierarchy-child pool">
|
||
<span class="hierarchy-dot"></span>
|
||
<span class="hierarchy-child-name">Ded. Elastic Pool</span>
|
||
<span class="hierarchy-child-desc">Pooled compute+storage, co-located</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="hierarchy-world">
|
||
<div class="hierarchy-world-header dedicated-header">
|
||
<span class="hierarchy-world-icon">🏗️</span>
|
||
<span class="hierarchy-world-title">Dedicated</span>
|
||
<span class="hierarchy-world-sub">Customer-exclusive Exadata infrastructure</span>
|
||
</div>
|
||
<div class="hierarchy-children">
|
||
<div class="hierarchy-child dedicated">
|
||
<span class="hierarchy-dot"></span>
|
||
<span class="hierarchy-child-name">ADB Dedicated</span>
|
||
<span class="hierarchy-child-desc">Standalone Exadata</span>
|
||
</div>
|
||
<div class="hierarchy-child dedpool">
|
||
<span class="hierarchy-dot"></span>
|
||
<span class="hierarchy-child-name">EP (Dedicated)</span>
|
||
<span class="hierarchy-child-desc">Elastic Pool on Exadata</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="overview-grid" id="overviewGrid"></div>
|
||
</div>
|
||
|
||
<!-- ════════════════ TAB 2: SCENARIOS ════════════════ -->
|
||
<div class="tab-panel" id="panel-scenarios">
|
||
<div class="section-header-row">
|
||
<h2 class="section-title" style="margin-bottom:0">Scenario Analysis</h2>
|
||
<button class="share-link" onclick="copyTabLink('scenarios')" title="Copy link to this tab">
|
||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/></svg>
|
||
<span>Share</span>
|
||
</button>
|
||
</div>
|
||
<p class="section-subtitle">When each flavor makes the most business sense</p>
|
||
<div id="scenariosContainer"></div>
|
||
</div>
|
||
|
||
<!-- ════════════════ TAB 3: DECISION MATRIX ════════════════ -->
|
||
<div class="tab-panel" id="panel-matrix">
|
||
<div class="matrix-container">
|
||
<div class="matrix-header">
|
||
<div>
|
||
<div class="section-header-row">
|
||
<h2 id="matrixTitle" style="margin-bottom:0">Decision Matrix</h2>
|
||
<button class="share-link" onclick="copyTabLink('matrix')" title="Copy link to this tab">
|
||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/></svg>
|
||
<span>Share</span>
|
||
</button>
|
||
</div>
|
||
<p id="matrixDesc">Set importance (0–100) per criterion to find your best fit</p>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
|
||
<div class="scenario-selector">
|
||
<label for="matrixSelect">Scenario:</label>
|
||
<select id="matrixSelect" onchange="switchMatrix(this.value)">
|
||
<optgroup label="By Scale">
|
||
<option value="small">Small — 5 DBs, low steady usage</option>
|
||
<option value="consolidated">Consolidated — ~25 medium DBs</option>
|
||
<option value="saas">High-consolidation SaaS — ~200 tenants / many DBs</option>
|
||
<option value="large">Large workloads — ~10 DBs, >100 ECPUs + >100 TB</option>
|
||
<option value="ultra">SaaS Ultra Multi-Tenant — ~1,000 DBs</option>
|
||
</optgroup>
|
||
<optgroup label="By Business Criticality">
|
||
<option value="crit_low">🟢 Low Impact — Dev/test, sandbox, PoC</option>
|
||
<option value="crit_medium">🟡 Medium Impact — Departmental apps, staging</option>
|
||
<option value="crit_high">🔴 High Impact — Mission-critical, ERP, core banking</option>
|
||
</optgroup>
|
||
</select>
|
||
</div>
|
||
<button class="reset-btn" onclick="resetMatrix()">↺ Reset</button>
|
||
</div>
|
||
</div>
|
||
<details class="how-to-box">
|
||
<summary style="cursor:pointer;font-weight:600;color:var(--text-primary);font-size:0.88rem;">How to use this matrix</summary>
|
||
<div style="margin-top:0.6rem;">Each criterion has an <strong>Importance</strong> slider (0–100) representing how critical that factor is for your scenario — these are independent, not percentages.
|
||
Each flavor receives a <strong>Score</strong> (1–5) for how well it meets that criterion. You can click the score dots to adjust them.
|
||
The weighted result is calculated by normalizing each importance against the total, then multiplying by the score:
|
||
<span class="formula-example">Weighted Score = Σ (importance<sub>i</sub> / Σimportances) × score<sub>i</sub></span></div>
|
||
</details>
|
||
|
||
<!-- Feature Requirements Filter -->
|
||
<details class="feature-matrix-details">
|
||
<summary style="cursor:pointer;font-weight:600;color:var(--text-primary);font-size:0.92rem;margin-bottom:0.5rem;">
|
||
Feature & Capability Matrix
|
||
<span style="font-weight:400;font-size:0.75rem;color:var(--text-muted);margin-left:0.5rem;">Features with differences across flavors or versions — check "Req." to filter</span>
|
||
<span style="font-weight:400;font-size:0.68rem;color:var(--text-muted);margin-left:auto;float:right;">Updated: Mar/2026</span>
|
||
</summary>
|
||
<div id="featureFilterStatus"></div>
|
||
<div class="cloud-selector" id="cloudSelector"></div>
|
||
<div id="featureMatrixContainer" style="overflow-x:auto;margin-top:0.5rem;"></div>
|
||
</details>
|
||
|
||
<div class="matrix-body">
|
||
<div class="matrix-score-panel" id="scorePanel">
|
||
<div class="score-summary" id="summary"></div>
|
||
</div>
|
||
<div class="matrix-main-content">
|
||
<div class="criteria-grid" id="grid"></div>
|
||
<div class="charts-row" id="chartsRow">
|
||
<div class="chart-card">
|
||
<div class="chart-title">Flavor Radar — Scores per Criterion</div>
|
||
<canvas id="radarChart" width="580" height="520"></canvas>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-title">Importance Distribution</div>
|
||
<canvas id="pieChart" width="520" height="480"></canvas>
|
||
</div>
|
||
</div>
|
||
<div id="formulaDetail" style="margin-bottom:1.5rem;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ════════════════ TAB: COST ESTIMATOR ════════════════ -->
|
||
<div class="tab-panel" id="panel-cost">
|
||
|
||
<!-- OCI-style green header bar -->
|
||
<div class="oci-header-bar">
|
||
<div class="oci-header-left">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" style="flex-shrink:0"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5"/><path d="M7 12h10M12 7v10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||
<div>
|
||
<div class="oci-header-title">Autonomous AI Database — Cost Comparison</div>
|
||
<div class="oci-header-sub">BYOL · Transaction Processing · List Price</div>
|
||
</div>
|
||
</div>
|
||
<div class="oci-header-right">
|
||
<div class="oci-header-cost-label">Estimated Monthly Cost</div>
|
||
<div class="oci-header-cost-value" id="ociHeaderCost">$0</div>
|
||
<button class="share-link" onclick="copyTabLink('cost')" title="Copy link to this tab" style="color:#fff;border-color:rgba(255,255,255,0.3);">
|
||
<svg viewBox="0 0 16 16" fill="currentColor"><path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/></svg>
|
||
<span>Share</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Configuration Card -->
|
||
<div class="oci-config-card">
|
||
<div class="oci-config-header">
|
||
<div class="oci-config-title">
|
||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><rect x="2" y="3" width="16" height="14" rx="2" stroke="currentColor" stroke-width="1.3"/><path d="M6 7h8M6 10h5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
|
||
Database Configuration
|
||
</div>
|
||
<div class="oci-util-badge">
|
||
<span class="oci-util-label">Utilization</span>
|
||
<span class="oci-util-value">744 hrs/month</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preset selector -->
|
||
<div class="oci-preset-row">
|
||
<label class="oci-field-label">Load Preset</label>
|
||
<select id="costPreset" onchange="loadCostPreset(this.value)" class="oci-select">
|
||
<option value="">— Custom —</option>
|
||
<option value="small" selected>Small — 5 DBs, 80 ECPUs</option>
|
||
<option value="consolidated">Consolidated — 25 DBs, 200 ECPUs</option>
|
||
<option value="saas">SaaS — 200 DBs, 400 ECPUs</option>
|
||
<option value="large">Large — 10 DBs, 640 ECPUs</option>
|
||
<option value="ultra">Ultra Multi-Tenant — 1,000 DBs, 150 real ECPUs</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Input fields -->
|
||
<div class="oci-fields-grid">
|
||
<div class="oci-field">
|
||
<label class="oci-field-label"># of Databases</label>
|
||
<input type="number" id="ci_dbs" min="1" max="5000" value="5" onchange="calcCosts(true)" oninput="calcCosts(true)" class="oci-input">
|
||
</div>
|
||
<div class="oci-field">
|
||
<label class="oci-field-label">ECPUs per DB</label>
|
||
<input type="number" id="ci_ecpu_per_db" min="2" max="5000" value="16" onchange="calcCosts(true)" oninput="calcCosts(true)" class="oci-input">
|
||
<span class="oci-field-hint">Min 2 per DB</span>
|
||
</div>
|
||
<div class="oci-field">
|
||
<label class="oci-field-label">Storage per DB (TB)</label>
|
||
<input type="number" id="ci_storage_per_db" min="0.02" max="500" value="5" step="0.5" onchange="calcCosts(true)" oninput="calcCosts(true)" class="oci-input">
|
||
</div>
|
||
<div class="oci-field">
|
||
<label class="oci-field-label">Real ECPU Usage (%)</label>
|
||
<input type="range" id="ci_usage_pct" min="1" max="100" value="100" oninput="calcCosts(false)" class="oci-slider">
|
||
<span class="oci-field-hint" id="usagePctLabel" style="color:var(--pool-color);font-weight:600;">100% — all provisioned ECPUs active</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Computed totals -->
|
||
<div class="oci-totals-row" id="costTotals"></div>
|
||
</div>
|
||
|
||
<!-- Results cards -->
|
||
<div id="costResults"></div>
|
||
|
||
<!-- Billing model details (collapsible) -->
|
||
<details class="oci-billing-details">
|
||
<summary>Billing Model Details & Formulas</summary>
|
||
<div class="oci-billing-grid">
|
||
<div class="billing-card shared">
|
||
<h4>ATP Serverless</h4>
|
||
<p>Each DB billed independently for <strong>provisioned ECPUs × hours running</strong>. Min 2 ECPUs/DB. Auto-scaling bursts up to 3× base.</p>
|
||
<div class="billing-formula">
|
||
<span class="billing-line"><em>Compute</em> = Σ ECPUs/DB × $0.0807/hr × 744 h</span>
|
||
<span class="billing-line"><em>Storage</em> = GB allocated × $0.1156/mo</span>
|
||
<span class="billing-line"><em>Backup</em> = GB used × $0.0244/mo</span>
|
||
</div>
|
||
<a class="doc-link" href="https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/autonomous-features-billing.html" target="_blank"><svg viewBox="0 0 16 16" fill="currentColor"><path d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/><path d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/></svg> Billing Docs</a>
|
||
</div>
|
||
<div class="billing-card spool">
|
||
<h4>Elastic Pool (Serverless)</h4>
|
||
<p>Compute billed to <strong>pool leader</strong>. Members can have 1 ECPU min. Storage billed per-DB.</p>
|
||
<div class="billing-formula">
|
||
<span class="billing-line"><em>Compute</em> = Pool shape × tier × $0.0807/hr × 744 h</span>
|
||
<span class="billing-line"><em>Tiers</em> peak ≤ 1× → 1×; ≤ 2× → 2×; > 2× → 4×</span>
|
||
<span class="billing-line"><em>Shapes</em> 128, 256, 512, 1024, 2048, 4096</span>
|
||
</div>
|
||
<a class="doc-link" href="https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/elastic-pools-about-billing.html" target="_blank"><svg viewBox="0 0 16 16" fill="currentColor"><path d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/><path d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/></svg> Billing Docs</a>
|
||
</div>
|
||
<div class="billing-card pool">
|
||
<h4>Dedicated Elastic Pool</h4>
|
||
<p>Both <strong>compute + storage billed to leader</strong>. Same tiered billing. Min: 256 ECPUs / 256 TB.</p>
|
||
<div class="billing-formula">
|
||
<span class="billing-line"><em>Compute</em> = Pool shape × tier × $0.0807/hr × 744 h</span>
|
||
<span class="billing-line"><em>Shapes</em> 256, 512, 1024</span>
|
||
<span class="billing-line"><em>Storage</em> = Pool total TB × $0.1156/GB/mo</span>
|
||
</div>
|
||
<a class="doc-link" href="https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/Dedicated-EP-Billing.html" target="_blank"><svg viewBox="0 0 16 16" fill="currentColor"><path d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/><path d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/></svg> Billing Docs</a>
|
||
</div>
|
||
<div class="billing-card dedicated">
|
||
<h4>ADB Dedicated X11M</h4>
|
||
<p>Compute per <strong>provisioned ECPUs</strong> + <strong>Exadata infra</strong> (DB + Storage servers). Backup via Object Storage.</p>
|
||
<div class="billing-formula">
|
||
<span class="billing-line"><em>Compute</em> = ECPUs × $0.0807/hr × 744 h</span>
|
||
<span class="billing-line"><em>DB srvs</em> = max(2, ⌈ECPU÷760⌉) × $2.90/h</span>
|
||
<span class="billing-line"><em>ST srvs</em> = max(3, ⌈TB÷80⌉) × $2.90/h</span>
|
||
</div>
|
||
<a class="doc-link" href="https://docs.oracle.com/en-us/iaas/autonomous-database/doc/billing-autonomous-database-dedicated-exadata-infrastructure.html" target="_blank"><svg viewBox="0 0 16 16" fill="currentColor"><path d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/><path d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/></svg> Billing Docs</a>
|
||
</div>
|
||
<div class="billing-card dedpool">
|
||
<h4>Elastic Pool (Dedicated)</h4>
|
||
<p>Elastic Pool on <strong>Dedicated Exadata</strong>. Pool compute billing + Exadata servers. Storage billed per instance.</p>
|
||
<div class="billing-formula">
|
||
<span class="billing-line"><em>Compute</em> = Pool shape × tier × $0.0807/hr</span>
|
||
<span class="billing-line"><em>Tiers</em> same as Serverless EP (1×/2×/4×)</span>
|
||
<span class="billing-line"><em>Infra</em> = DB + ST servers × $2.90/h</span>
|
||
</div>
|
||
<a class="doc-link" href="https://docs.oracle.com/en/cloud/paas/autonomous-database/dedicated/catiu/index.html" target="_blank"><svg viewBox="0 0 16 16" fill="currentColor"><path d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/><path d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/></svg> Billing Docs</a>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<div class="oci-verify-link">
|
||
Verify estimates with <a href="https://www.oracle.com/cloud/costestimator.html" target="_blank">OCI Cost Estimator ↗</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ════════════════ TAB: REFERENCES ════════════════ -->
|
||
|
||
|
||
<script src="adb-data.js"></script>
|
||
<script>
|
||
Object.keys(matrices).forEach(k => {
|
||
matrices[k].origWeights = matrices[k].criteria.map(c => c.weight);
|
||
matrices[k].weights = matrices[k].criteria.map(c => c.weight);
|
||
});
|
||
|
||
let currentMatrix = 'A';
|
||
|
||
function computeScores(m) {
|
||
const tw = m.weights.reduce((a,b) => a+b, 0);
|
||
if (tw === 0) return [0,0,0,0];
|
||
const s = Array(FLAVORS.length).fill(0);
|
||
m.criteria.forEach((c,i) => { for(let f=0;f<FLAVORS.length;f++) s[f] += (m.weights[i]/tw)*c.scores[f]; });
|
||
return s.map(v => Math.round(v*100)/100);
|
||
}
|
||
function getWinners(scores) { const mx=Math.max(...scores); return scores.reduce((a,s,i)=>s===mx?[...a,i]:a,[]); }
|
||
|
||
function renderSummary(m) {
|
||
const tw = m.weights.reduce((a,b)=>a+b,0);
|
||
const scores = computeScores(m), winners = getWinners(scores);
|
||
document.getElementById('summary').innerHTML = scores.map((s,i) => {
|
||
const pct = (s/5*100).toFixed(0), win = winners.includes(i);
|
||
return `<div class="score-chip ${FLAVORS[i].cls}" data-flavor-idx="${i}"><div style="flex:1">
|
||
<div class="label">${FLAVORS[i].name}<span class="ver-rec" id="verRec${i}"></span></div>
|
||
<div style="display:flex;align-items:center;gap:0.5rem">
|
||
<span class="value">${s.toFixed(2)}</span>
|
||
${win ? `<span class="winner-badge ${FLAVORS[i].cls}">\u2605 Best fit</span>` : ''}
|
||
</div>
|
||
<div class="bar-bg"><div class="bar-fill" style="width:${pct}%"></div></div>
|
||
</div></div>`;
|
||
}).join('');
|
||
|
||
// Build formula breakdown
|
||
if (tw === 0) { document.getElementById('formulaDetail').innerHTML = ''; return; }
|
||
const flavorsHTML = Array.from({length:FLAVORS.length},(_,f)=>f).map(f => {
|
||
const lines = m.criteria.map((c,i) => {
|
||
const norm = (m.weights[i]/tw);
|
||
const contrib = norm * c.scores[f];
|
||
return `<div class="step-line"><span class="step-op">+</span><span>(${m.weights[i]}/${tw}) \u00d7 ${c.scores[f]} = ${contrib.toFixed(3)}</span><span style="color:var(--text-muted);margin-left:0.5rem;font-style:italic">${c.name}</span></div>`;
|
||
}).join('');
|
||
return `<div class="formula-flavor">
|
||
<div class="formula-flavor-header"><span class="formula-flavor-name ${FLAVORS[f].cls}">${FLAVORS[f].name}</span></div>
|
||
<div class="formula-steps">
|
||
${lines}
|
||
<div class="step-line formula-total"><span class="step-op">=</span><span>${scores[f].toFixed(2)}</span></div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
document.getElementById('formulaDetail').innerHTML = `
|
||
<details class="formula-panel">
|
||
<summary>Show calculation details</summary>
|
||
${flavorsHTML}
|
||
</details>`;
|
||
|
||
// Render charts
|
||
renderRadar(m, scores);
|
||
renderPie(m);
|
||
// Re-apply feature filters (checkboxes survive in separate DOM)
|
||
if (typeof applyFeatureFilters === 'function') applyFeatureFilters();
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// RADAR CHART — Flavor scores per criterion (like spider/web chart)
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
function renderRadar(m, scores) {
|
||
const canvas = document.getElementById('radarChart');
|
||
if (!canvas) return;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const W = 580, H = 520;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
canvas.style.width = W + 'px';
|
||
canvas.style.height = H + 'px';
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
const cx = W / 2, cy = H / 2 - 8;
|
||
const maxR = Math.min(W, H) * 0.27; // smaller polygon = more label room
|
||
const n = m.criteria.length;
|
||
if (n < 3) return;
|
||
const angleStep = (2 * Math.PI) / n;
|
||
const startAngle = -Math.PI / 2;
|
||
|
||
const flavorColors = ['#3B82F6','#A855F7','#F59E0B','#10B981','#06B6D4'];
|
||
const flavorAlphas = ['#3B82F619','#A855F719','#F59E0B19','#10B98119','#06B6D419'];
|
||
|
||
// Grid rings (1–5)
|
||
for (let ring = 1; ring <= 5; ring++) {
|
||
const r = (ring / 5) * maxR;
|
||
ctx.beginPath();
|
||
for (let i = 0; i <= n; i++) {
|
||
const a = startAngle + i * angleStep;
|
||
const x = cx + r * Math.cos(a), y = cy + r * Math.sin(a);
|
||
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
||
}
|
||
ctx.closePath();
|
||
ctx.strokeStyle = ring === 5 ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.06)';
|
||
ctx.lineWidth = ring === 5 ? 1.2 : 0.7;
|
||
ctx.stroke();
|
||
// Ring number
|
||
if (ring % 2 === 0 || ring === 5) {
|
||
ctx.fillStyle = 'rgba(255,255,255,0.18)';
|
||
ctx.font = '400 9px "JetBrains Mono", monospace';
|
||
ctx.textAlign = 'left';
|
||
ctx.textBaseline = 'bottom';
|
||
ctx.fillText(ring, cx + 3, cy - r - 2);
|
||
}
|
||
}
|
||
|
||
// Axis lines
|
||
for (let i = 0; i < n; i++) {
|
||
const a = startAngle + i * angleStep;
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, cy);
|
||
ctx.lineTo(cx + maxR * Math.cos(a), cy + maxR * Math.sin(a));
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.07)';
|
||
ctx.lineWidth = 0.7;
|
||
ctx.stroke();
|
||
}
|
||
|
||
// Flavor polygons
|
||
for (let f = 0; f < FLAVORS.length; f++) {
|
||
ctx.beginPath();
|
||
for (let i = 0; i <= n; i++) {
|
||
const idx = i % n;
|
||
const a = startAngle + idx * angleStep;
|
||
const val = m.criteria[idx].scores[f] / 5;
|
||
const x = cx + val * maxR * Math.cos(a), y = cy + val * maxR * Math.sin(a);
|
||
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
||
}
|
||
ctx.closePath();
|
||
ctx.fillStyle = flavorAlphas[f];
|
||
ctx.fill();
|
||
ctx.strokeStyle = flavorColors[f];
|
||
ctx.lineWidth = 2.2;
|
||
ctx.stroke();
|
||
// Dots
|
||
for (let i = 0; i < n; i++) {
|
||
const a = startAngle + i * angleStep;
|
||
const val = m.criteria[i].scores[f] / 5;
|
||
const x = cx + val * maxR * Math.cos(a), y = cy + val * maxR * Math.sin(a);
|
||
ctx.beginPath();
|
||
ctx.arc(x, y, 3.5, 0, 2 * Math.PI);
|
||
ctx.fillStyle = flavorColors[f];
|
||
ctx.fill();
|
||
}
|
||
}
|
||
|
||
// Labels — generous spacing, multi-line wrap, never clipped
|
||
const labelFont = '500 12.5px "DM Sans", sans-serif';
|
||
ctx.fillStyle = '#57534E';
|
||
for (let i = 0; i < n; i++) {
|
||
const a = startAngle + i * angleStep;
|
||
const cosA = Math.cos(a), sinA = Math.sin(a);
|
||
const labelR = maxR + 30;
|
||
let lx = cx + labelR * cosA;
|
||
let ly = cy + labelR * sinA;
|
||
|
||
// Alignment
|
||
if (cosA < -0.15) ctx.textAlign = 'right';
|
||
else if (cosA > 0.15) ctx.textAlign = 'left';
|
||
else ctx.textAlign = 'center';
|
||
if (sinA < -0.6) ctx.textBaseline = 'bottom';
|
||
else if (sinA > 0.6) ctx.textBaseline = 'top';
|
||
else ctx.textBaseline = 'middle';
|
||
|
||
ctx.font = labelFont;
|
||
const name = m.criteria[i].name;
|
||
|
||
// Word wrap into lines ≤ 14 chars
|
||
const words = name.split(/[\s-]+/);
|
||
const rawParts = [];
|
||
// Preserve hyphens
|
||
let pos = 0;
|
||
words.forEach(w => {
|
||
const idx = name.indexOf(w, pos);
|
||
const sep = idx > 0 && name[idx - 1] === '-' ? '-' : '';
|
||
rawParts.push(sep + w);
|
||
pos = idx + w.length;
|
||
});
|
||
|
||
const lines = [];
|
||
let cur = '';
|
||
rawParts.forEach(w => {
|
||
const test = cur ? cur + ' ' + w : w;
|
||
if (test.length > 14 && cur) { lines.push(cur.trim()); cur = w; }
|
||
else { cur = test; }
|
||
});
|
||
if (cur) lines.push(cur.trim());
|
||
|
||
const lineH = 15;
|
||
const totalH = lines.length * lineH;
|
||
let baseY = ly;
|
||
if (ctx.textBaseline === 'middle') baseY = ly - (totalH - lineH) / 2;
|
||
else if (ctx.textBaseline === 'bottom') baseY = ly - totalH + lineH;
|
||
|
||
lines.forEach((line, li) => {
|
||
ctx.fillStyle = '#57534E';
|
||
ctx.fillText(line, lx, baseY + li * lineH);
|
||
});
|
||
}
|
||
|
||
// Legend — centered at bottom
|
||
ctx.textAlign = 'left';
|
||
ctx.textBaseline = 'middle';
|
||
const legY = H - 16;
|
||
const legItemW = 128;
|
||
const legTotalW = 4 * legItemW;
|
||
const legStartX = (W - legTotalW) / 2;
|
||
for (let f = 0; f < FLAVORS.length; f++) {
|
||
const lx = legStartX + f * legItemW;
|
||
ctx.fillStyle = flavorColors[f];
|
||
ctx.fillRect(lx, legY - 6, 14, 14);
|
||
ctx.strokeStyle = 'rgba(0,0,0,0.3)';
|
||
ctx.lineWidth = 1;
|
||
ctx.strokeRect(lx, legY - 6, 14, 14);
|
||
ctx.fillStyle = '#57534E';
|
||
ctx.font = '500 11.5px "DM Sans", sans-serif';
|
||
ctx.fillText(FLAVORS[f].short, lx + 19, legY + 1);
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// PIE / DONUT CHART — Importance weight distribution
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
function renderPie(m) {
|
||
const canvas = document.getElementById('pieChart');
|
||
if (!canvas) return;
|
||
const dpr = window.devicePixelRatio || 1;
|
||
const W = 520, H = 480;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
canvas.style.width = W + 'px';
|
||
canvas.style.height = H + 'px';
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
const cx = W / 2, cy = H / 2 - 5;
|
||
const outerR = Math.min(W, H) * 0.30;
|
||
const innerR = outerR * 0.52;
|
||
const total = m.weights.reduce((a, b) => a + b, 0);
|
||
if (total === 0) return;
|
||
|
||
// Color palette — muted blues/teals matching dark theme
|
||
const palette = [
|
||
'#3B82F6','#10B981','#F59E0B','#A855F7','#EC4899',
|
||
'#06B6D4','#F87171','#84CC16','#D4A85C','#818CF8'
|
||
];
|
||
|
||
// Sort by weight descending for better visual
|
||
const sorted = m.criteria.map((c, i) => ({ name: c.name, weight: m.weights[i], idx: i }))
|
||
.sort((a, b) => b.weight - a.weight);
|
||
|
||
let angle = -Math.PI / 2;
|
||
const slices = [];
|
||
|
||
sorted.forEach((item, si) => {
|
||
const sliceAngle = (item.weight / total) * 2 * Math.PI;
|
||
const midAngle = angle + sliceAngle / 2;
|
||
const color = palette[si % palette.length];
|
||
|
||
// Draw slice
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, outerR, angle, angle + sliceAngle);
|
||
ctx.arc(cx, cy, innerR, angle + sliceAngle, angle, true);
|
||
ctx.closePath();
|
||
ctx.fillStyle = color;
|
||
ctx.fill();
|
||
ctx.strokeStyle = 'rgba(15,14,13,0.6)';
|
||
ctx.lineWidth = 2;
|
||
ctx.stroke();
|
||
|
||
slices.push({ name: item.name, weight: item.weight, pct: ((item.weight / total) * 100).toFixed(0), midAngle, color });
|
||
angle += sliceAngle;
|
||
});
|
||
|
||
// Labels — outside with leader lines, anti-collision
|
||
const labelFont = '500 11.5px "DM Sans", sans-serif';
|
||
const pctFont = '700 11.5px "JetBrains Mono", monospace';
|
||
ctx.font = labelFont;
|
||
const labelR = outerR + 16;
|
||
const lineR = outerR + 5;
|
||
const elbowR = labelR + 18;
|
||
|
||
slices.forEach(s => {
|
||
if (parseFloat(s.pct) < 3) return; // skip tiny slices
|
||
const cosM = Math.cos(s.midAngle), sinM = Math.sin(s.midAngle);
|
||
const isLeft = cosM < 0;
|
||
|
||
// Leader line: radial → elbow → horizontal
|
||
const startX = cx + lineR * cosM, startY = cy + lineR * sinM;
|
||
const elbowX = cx + elbowR * cosM, elbowY = cy + elbowR * sinM;
|
||
const endX = elbowX + (isLeft ? -20 : 20);
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(startX, startY);
|
||
ctx.lineTo(elbowX, elbowY);
|
||
ctx.lineTo(endX, elbowY);
|
||
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
|
||
ctx.lineWidth = 0.9;
|
||
ctx.stroke();
|
||
|
||
// Dot at start
|
||
ctx.beginPath();
|
||
ctx.arc(startX, startY, 2, 0, 2 * Math.PI);
|
||
ctx.fillStyle = s.color;
|
||
ctx.fill();
|
||
|
||
// Text
|
||
const tx = endX + (isLeft ? -5 : 5);
|
||
ctx.textAlign = isLeft ? 'right' : 'left';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillStyle = '#1C1917';
|
||
ctx.font = labelFont;
|
||
ctx.fillText(s.name + ',', tx, elbowY - 1);
|
||
ctx.fillStyle = s.color;
|
||
ctx.font = pctFont;
|
||
// Measure name width to place pct inline
|
||
ctx.font = labelFont;
|
||
const nameW = ctx.measureText(s.name + ', ').width;
|
||
ctx.font = pctFont;
|
||
if (isLeft) {
|
||
ctx.textAlign = 'right';
|
||
ctx.fillText(' ' + s.pct + '%', tx - nameW, elbowY - 1);
|
||
} else {
|
||
ctx.textAlign = 'left';
|
||
ctx.fillText(' ' + s.pct + '%', tx + nameW, elbowY - 1);
|
||
}
|
||
});
|
||
|
||
// Center text
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
ctx.fillStyle = '#1C1917';
|
||
ctx.font = '700 24px "JetBrains Mono", monospace';
|
||
ctx.fillText(total, cx, cy - 7);
|
||
ctx.fillStyle = '#A8A29E';
|
||
ctx.font = '500 12px "DM Sans", sans-serif';
|
||
ctx.fillText('Total Weight', cx, cy + 14);
|
||
}
|
||
|
||
function buildInfoTrigger(name, idx) {
|
||
const info = INFO[name];
|
||
if (!info) return '';
|
||
return `<span class="info-trigger" onclick="toggleInfo(${idx})" id="infoBtn-${idx}">i</span>`;
|
||
}
|
||
|
||
function buildInfoPanel(name, idx) {
|
||
const info = INFO[name];
|
||
if (!info) return '';
|
||
const flavorHTML = info.flavors.map((t,f) => `<div class="info-flavor">
|
||
<span class="info-flavor-label ${FLAVORS[f].cls}">${FLAVORS[f].short}</span>
|
||
<span class="info-flavor-text">${t}</span>
|
||
</div>`).join('');
|
||
return `<div class="info-panel" id="infoPanel-${idx}">
|
||
<div class="info-desc">${info.desc}</div>
|
||
${flavorHTML}
|
||
</div>`;
|
||
}
|
||
|
||
function toggleInfo(idx) {
|
||
const panel = document.getElementById('infoPanel-' + idx);
|
||
const btn = document.getElementById('infoBtn-' + idx);
|
||
const isOpen = panel.classList.contains('open');
|
||
// Close all others
|
||
document.querySelectorAll('.info-panel.open').forEach(p => p.classList.remove('open'));
|
||
document.querySelectorAll('.info-trigger.active').forEach(b => b.classList.remove('active'));
|
||
if (!isOpen) {
|
||
panel.classList.add('open');
|
||
btn.classList.add('active');
|
||
}
|
||
}
|
||
|
||
function renderGrid(m) {
|
||
document.getElementById('grid').innerHTML = m.criteria.map((c,i) => {
|
||
const scoresHTML = c.scores.map((s,f) => {
|
||
const dots = Array.from({length:5},(_,d) =>
|
||
`<div class="score-dot ${d<s?'filled '+FLAVORS[f].cls:''}" data-c="${i}" data-f="${f}" data-d="${d+1}" onclick="setScore(${i},${f},${d+1})"></div>`
|
||
).join('');
|
||
return `<div class="score-cell ${FLAVORS[f].cls}"><div class="score-label">${FLAVORS[f].short}</div><div class="score-dots">${dots}</div></div>`;
|
||
}).join('');
|
||
return `<div class="criteria-row">
|
||
<div class="criteria-left">
|
||
<div class="criteria-name-row"><span class="criteria-name">${c.name}</span>${buildInfoTrigger(c.name, i)}</div>
|
||
<div class="weight-control">
|
||
<label>Importance</label>
|
||
<input type="range" min="0" max="100" value="${m.weights[i]}" oninput="updateWeight(${i},this.value)">
|
||
<span class="weight-pct" id="wpct-${i}">${m.weights[i]}</span>
|
||
</div>
|
||
</div>
|
||
<div class="criteria-right"><div class="scores-row">${scoresHTML}</div></div>
|
||
</div>${buildInfoPanel(c.name, i)}`;
|
||
}).join('');
|
||
}
|
||
|
||
function updateWeight(idx,val) {
|
||
const m = matrices[currentMatrix]; m.weights[idx]=parseInt(val);
|
||
document.getElementById(`wpct-${idx}`).textContent=val; renderSummary(m);
|
||
}
|
||
function setScore(ci,fi,ns) {
|
||
const m = matrices[currentMatrix]; m.criteria[ci].scores[fi]=ns;
|
||
document.querySelectorAll(`.score-dot[data-c="${ci}"][data-f="${fi}"]`).forEach(d => {
|
||
d.className = parseInt(d.dataset.d)<=ns ? `score-dot filled ${FLAVORS[fi].cls}` : 'score-dot';
|
||
});
|
||
renderSummary(m);
|
||
}
|
||
function switchMatrix(key) {
|
||
currentMatrix=key; const m=matrices[key];
|
||
document.getElementById('matrixTitle').textContent=m.title;
|
||
document.getElementById('matrixDesc').textContent=m.desc+' \u2022 Set importance (0\u2013100) per criterion to find your best fit';
|
||
renderGrid(m); renderSummary(m);
|
||
}
|
||
function resetMatrix() {
|
||
const m=matrices[currentMatrix];
|
||
m.origWeights.forEach((w,i)=>{m.weights[i]=w;});
|
||
m.origScores.forEach((s,i)=>{m.criteria[i].scores=[...s];});
|
||
renderGrid(m); renderSummary(m);
|
||
}
|
||
switchMatrix('small');
|
||
|
||
|
||
function renderStars(n, flavorCls) {
|
||
const pct = (n / 5 * 100).toFixed(0);
|
||
return `<div class="score-bar-wrap"><span class="score-bar-num">${n}/5</span><div class="score-bar-track"><div class="score-bar-fill ${flavorCls || ''}" style="width:${pct}%"></div></div></div>`;
|
||
}
|
||
|
||
function renderOverview() {
|
||
const flavorLabels = FLAVORS.map(fl => fl.name);
|
||
const flavorCls = FLAVORS.map(fl => fl.cls);
|
||
const flavorDocKeys = ["shared","spool","pool","dedicated"];
|
||
document.getElementById('overviewGrid').innerHTML = OVERVIEW_CARDS.map(card => {
|
||
const rows = Array.from({length:FLAVORS.length},(_,f)=>f).map(f => `
|
||
<div class="flavor-row">
|
||
<span class="flavor-label ${flavorCls[f]}">${flavorLabels[f]}</span>
|
||
<div class="flavor-row-wrap">
|
||
<div class="flavor-stars">${renderStars(card.stars[f], flavorCls[f])}</div>
|
||
<span class="flavor-desc">${card.descs[f]}</span>
|
||
<a class="overview-doc-link" href="${DOC_LINKS[flavorDocKeys[f]]}" target="_blank" title="Oracle Docs">\uD83D\uDD17</a>
|
||
</div>
|
||
</div>`).join('');
|
||
return `<div class="driver-card">
|
||
<h3><span class="icon" style="background:${card.iconBg};color:${card.iconColor};">${card.icon}</span> ${card.title}</h3>
|
||
${rows}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
renderOverview();
|
||
|
||
|
||
function loadCostPreset(key) {
|
||
if (!key || !COST_PRESETS[key]) return;
|
||
const p = COST_PRESETS[key];
|
||
document.getElementById('ci_dbs').value = p.dbs;
|
||
document.getElementById('ci_ecpu_per_db').value = p.ecpuPerDb;
|
||
document.getElementById('ci_storage_per_db').value = p.storPerDb;
|
||
document.getElementById('ci_usage_pct').value = p.usagePct;
|
||
calcCosts(false);
|
||
}
|
||
|
||
function calcCosts(updateBackupDefault) {
|
||
const P = PRICING;
|
||
const dbs = Math.max(1, parseInt(document.getElementById('ci_dbs').value) || 1);
|
||
const ecpuPerDb = Math.max(2, parseInt(document.getElementById('ci_ecpu_per_db').value) || 2);
|
||
const storPerDb = Math.max(0.02, parseFloat(document.getElementById('ci_storage_per_db').value) || 0.02);
|
||
const usagePct = Math.max(1, Math.min(100, parseInt(document.getElementById('ci_usage_pct').value) || 100));
|
||
|
||
// Computed totals
|
||
const provEcpu = dbs * ecpuPerDb;
|
||
const storageTB = dbs * storPerDb;
|
||
const backupTB = Math.round(storageTB / 3 * 10) / 10; // 1/3 of storage
|
||
const realEcpu = Math.max(1, Math.round(provEcpu * usagePct / 100));
|
||
|
||
// Usage % label
|
||
const pctLabel = document.getElementById('usagePctLabel');
|
||
if (usagePct < 100) {
|
||
pctLabel.innerHTML = `${usagePct}% \u2014 <strong>${realEcpu.toLocaleString()}</strong> real ECPUs of ${provEcpu.toLocaleString()} provisioned`;
|
||
pctLabel.style.color = 'var(--dedicated-color)';
|
||
} else {
|
||
pctLabel.textContent = `100% \u2014 all ${provEcpu.toLocaleString()} provisioned ECPUs active`;
|
||
pctLabel.style.color = 'var(--pool-color)';
|
||
}
|
||
|
||
// Totals chips
|
||
document.getElementById('costTotals').innerHTML = [
|
||
`<span class="cost-totals-chip">Total ECPUs: <strong>${provEcpu.toLocaleString()}</strong></span>`,
|
||
usagePct < 100 ? `<span class="cost-totals-chip highlight">Real Usage: <strong>${realEcpu.toLocaleString()} ECPUs</strong> (${usagePct}%)</span>` : '',
|
||
`<span class="cost-totals-chip">Storage: <strong>${storageTB.toLocaleString()} TB</strong></span>`,
|
||
`<span class="cost-totals-chip">Backup: <strong>${backupTB.toLocaleString()} TB</strong> (\u2153 storage)</span>`,
|
||
].filter(Boolean).join('');
|
||
|
||
// ── SERVERLESS ──
|
||
const sharedCompute = provEcpu * P.hrsMonth * P.ecpuHr;
|
||
const sharedStorage = storageTB * 1024 * P.storageGbMo;
|
||
const sharedBackup = backupTB * 1024 * P.backupGbMo;
|
||
const sharedTotal = sharedCompute + sharedStorage + sharedBackup;
|
||
|
||
// ── ELASTIC POOL (Serverless) ──
|
||
// Pool shapes: 128, 256, 512, 1024, 2048, 4096 ECPUs
|
||
// Pool capacity = 4× pool shape (max provisionable ECPUs in pool)
|
||
// Billing tiers per hour based on aggregate peak usage:
|
||
// usage ≤ 1× shape → billed 1× shape
|
||
// usage > 1× and ≤ 2× shape → billed 2× shape
|
||
// usage > 2× and ≤ 4× shape → billed 4× shape
|
||
// Inside pool: min 1 ECPU per DB (vs 2 outside)
|
||
// Storage billed per-DB individually (same as Serverless).
|
||
const POOL_SHAPES = [128, 256, 512, 1024, 2048, 4096];
|
||
|
||
// Total ECPUs that need to fit in pool capacity (4× shape)
|
||
// Inside pool each DB can be 1 ECPU min, but user specified ecpuPerDb
|
||
const totalPoolEcpus = dbs * Math.max(1, ecpuPerDb);
|
||
|
||
// Find smallest shape whose capacity (4×) can hold all provisioned ECPUs
|
||
let spoolShape = POOL_SHAPES.find(s => s * 4 >= totalPoolEcpus) || 4096;
|
||
// If even 4096×4=16384 is not enough, need multiple pools
|
||
const spoolPoolCount = spoolShape * 4 >= totalPoolEcpus ? 1 : Math.ceil(totalPoolEcpus / (4096 * 4));
|
||
if (spoolPoolCount > 1) spoolShape = 4096;
|
||
|
||
// Determine billing tier: what gets billed per hour based on real usage
|
||
const spoolRealPerPool = Math.ceil(realEcpu / spoolPoolCount);
|
||
let spoolBilledEcpus;
|
||
if (spoolRealPerPool <= spoolShape) spoolBilledEcpus = spoolShape; // 1× tier
|
||
else if (spoolRealPerPool <= spoolShape * 2) spoolBilledEcpus = spoolShape * 2; // 2× tier
|
||
else spoolBilledEcpus = spoolShape * 4; // 4× tier
|
||
spoolBilledEcpus *= spoolPoolCount;
|
||
|
||
const spoolTierLabel = spoolRealPerPool <= spoolShape ? '1×' :
|
||
spoolRealPerPool <= spoolShape * 2 ? '2×' : '4×';
|
||
|
||
const spoolCompute = spoolBilledEcpus * P.hrsMonth * P.ecpuHr;
|
||
const spoolStorage = storageTB * 1024 * P.storageGbMo;
|
||
const spoolBackup = backupTB * 1024 * P.backupGbMo;
|
||
const spoolTotal = spoolCompute + spoolStorage + spoolBackup;
|
||
|
||
// ── DEDICATED ELASTIC POOL ──
|
||
// Shapes: 256 ECPUs min (with 256 TB storage)
|
||
// Same tiered billing as Serverless pool, plus storage consolidated to leader
|
||
const DED_POOL_SHAPES = [256, 512, 1024];
|
||
let poolShape = DED_POOL_SHAPES.find(s => s * 4 >= totalPoolEcpus) || 1024;
|
||
const poolPoolCount = poolShape * 4 >= totalPoolEcpus ? 1 : Math.ceil(totalPoolEcpus / (1024 * 4));
|
||
if (poolPoolCount > 1) poolShape = 1024;
|
||
|
||
const poolRealPerPool = Math.ceil(realEcpu / poolPoolCount);
|
||
let poolBilledEcpus;
|
||
if (poolRealPerPool <= poolShape) poolBilledEcpus = poolShape;
|
||
else if (poolRealPerPool <= poolShape * 2) poolBilledEcpus = poolShape * 2;
|
||
else poolBilledEcpus = poolShape * 4;
|
||
poolBilledEcpus *= poolPoolCount;
|
||
|
||
const poolTierLabel = poolRealPerPool <= poolShape ? '1×' :
|
||
poolRealPerPool <= poolShape * 2 ? '2×' : '4×';
|
||
|
||
const poolStorTB = Math.max(P.poolMinStorTB, storageTB);
|
||
const poolCompute = poolBilledEcpus * P.hrsMonth * P.ecpuHr;
|
||
const poolStorage = poolStorTB * 1024 * P.storageGbMo;
|
||
const poolBackup = backupTB * 1024 * P.backupGbMo;
|
||
const poolTotal = poolCompute + poolStorage + poolBackup;
|
||
|
||
// ── DEDICATED X11M ──
|
||
const dedEcpu = provEcpu;
|
||
const dedCompute = dedEcpu * P.hrsMonth * P.ecpuHr;
|
||
const dbServers = Math.max(P.minDbServers, Math.ceil(dedEcpu / P.dbSrvEcpuCap));
|
||
const stServers = Math.max(P.minStServers, Math.ceil(storageTB / P.stSrvStorCap));
|
||
const dedInfra = (dbServers * P.x11mDbSrvHr + stServers * P.x11mStSrvHr) * P.hrsMonth;
|
||
const dedBackup = Math.max(0, backupTB * 1024 - 10) * P.objStorageGbMo;
|
||
const dedTotal = dedCompute + dedInfra + dedBackup;
|
||
|
||
// ── DEDICATED ELASTIC POOL (Exadata) ──
|
||
// Same Exadata infra as ADB Dedicated + pool billing (same shapes/tiers as Serverless EP)
|
||
// Pool shapes: 128, 256, 512, 1024, 2048, 4096
|
||
let dedEpShape = POOL_SHAPES.find(s => s * 4 >= totalPoolEcpus) || 4096;
|
||
const dedEpPoolCount = dedEpShape * 4 >= totalPoolEcpus ? 1 : Math.ceil(totalPoolEcpus / (4096 * 4));
|
||
if (dedEpPoolCount > 1) dedEpShape = 4096;
|
||
const dedEpRealPerPool = Math.ceil(realEcpu / dedEpPoolCount);
|
||
let dedEpBilledEcpus;
|
||
if (dedEpRealPerPool <= dedEpShape) dedEpBilledEcpus = dedEpShape;
|
||
else if (dedEpRealPerPool <= dedEpShape * 2) dedEpBilledEcpus = dedEpShape * 2;
|
||
else dedEpBilledEcpus = dedEpShape * 4;
|
||
dedEpBilledEcpus *= dedEpPoolCount;
|
||
const dedEpTierLabel = dedEpRealPerPool <= dedEpShape ? '1×' :
|
||
dedEpRealPerPool <= dedEpShape * 2 ? '2×' : '4×';
|
||
const dedEpCompute = dedEpBilledEcpus * P.hrsMonth * P.ecpuHr;
|
||
// Infra: same Exadata servers as Dedicated, but sized for pool capacity
|
||
const dedEpDbServers = Math.max(P.minDbServers, Math.ceil((dedEpShape * 4 * dedEpPoolCount) / P.dbSrvEcpuCap));
|
||
const dedEpStServers = Math.max(P.minStServers, Math.ceil(storageTB / P.stSrvStorCap));
|
||
const dedEpInfra = (dedEpDbServers * P.x11mDbSrvHr + dedEpStServers * P.x11mStSrvHr) * P.hrsMonth;
|
||
const dedEpBackup = Math.max(0, backupTB * 1024 - 10) * P.objStorageGbMo;
|
||
const dedEpTotal = dedEpCompute + dedEpInfra + dedEpBackup;
|
||
|
||
// Find winner
|
||
const totals = [sharedTotal, spoolTotal, poolTotal, dedTotal, dedEpTotal];
|
||
const minTotal = Math.min(...totals);
|
||
// Update OCI-style green header with lowest cost
|
||
const hdrEl = document.getElementById('ociHeaderCost');
|
||
if (hdrEl) hdrEl.textContent = '$' + Math.round(minTotal).toLocaleString('en-US');
|
||
const spoolSavingVsProv = `Shape ${spoolShape.toLocaleString()} ECPUs (cap ${(spoolShape*4).toLocaleString()})${spoolPoolCount > 1 ? ' × '+spoolPoolCount+' pools' : ''} — billed ${spoolTierLabel} = ${spoolBilledEcpus.toLocaleString()} ECPUs`;
|
||
const poolSavingVsProv = `Shape ${poolShape.toLocaleString()} ECPUs (cap ${(poolShape*4).toLocaleString()})${poolPoolCount > 1 ? ' × '+poolPoolCount+' pools' : ''} — billed ${poolTierLabel} = ${poolBilledEcpus.toLocaleString()} ECPUs`;
|
||
const dedEpSavingVsProv = `Shape ${dedEpShape.toLocaleString()} ECPUs (cap ${(dedEpShape*4).toLocaleString()})${dedEpPoolCount > 1 ? ' × '+dedEpPoolCount+' pools' : ''} — billed ${dedEpTierLabel} = ${dedEpBilledEcpus.toLocaleString()} ECPUs`;
|
||
const flavors = [
|
||
{ name: 'ATP Serverless', cls: 'shared', total: sharedTotal, compute: sharedCompute, storage: sharedStorage, backup: sharedBackup, infra: 0,
|
||
ecpuLabel: `${provEcpu.toLocaleString()} prov. ECPUs`,
|
||
ecpuNote: null },
|
||
{ name: 'Elastic Pool', cls: 'spool', total: spoolTotal, compute: spoolCompute, storage: spoolStorage, backup: spoolBackup, infra: 0,
|
||
ecpuLabel: `Compute (${spoolBilledEcpus.toLocaleString()} billed ECPUs)`,
|
||
ecpuNote: spoolSavingVsProv },
|
||
{ name: 'Ded. Elastic Pool', cls: 'pool', total: poolTotal, compute: poolCompute, storage: poolStorage, backup: poolBackup, infra: 0,
|
||
ecpuLabel: `Compute (${poolBilledEcpus.toLocaleString()} billed ECPUs)`,
|
||
ecpuNote: poolSavingVsProv },
|
||
{ name: 'ADB Dedicated X11M', cls: 'dedicated', total: dedTotal, compute: dedCompute, storage: 0, backup: dedBackup, infra: dedInfra,
|
||
ecpuLabel: `${dedEcpu.toLocaleString()} prov. ECPUs`,
|
||
ecpuNote: `${dbServers} DB + ${stServers} Storage servers (X11M)` },
|
||
{ name: 'EP (Dedicated)', cls: 'dedpool', total: dedEpTotal, compute: dedEpCompute, storage: 0, backup: dedEpBackup, infra: dedEpInfra,
|
||
ecpuLabel: `Compute (${dedEpBilledEcpus.toLocaleString()} billed ECPUs)`,
|
||
ecpuNote: dedEpSavingVsProv + ` | ${dedEpDbServers} DB + ${dedEpStServers} ST servers` },
|
||
];
|
||
|
||
// Render result cards
|
||
const fmt = v => '$' + Math.round(v).toLocaleString('en-US');
|
||
document.getElementById('costResults').innerHTML = '<div class="cost-result-cards">' + flavors.map(f => {
|
||
const isWinner = Math.abs(f.total - minTotal) < 1;
|
||
const pct = isWinner ? '' : `<div class="pct-vs" style="color:#F87171">+${Math.round(((f.total - minTotal) / minTotal) * 100)}% vs lowest</div>`;
|
||
const winLabel = isWinner ? '<span class="winner-label">\u2714 Lowest</span>' : '';
|
||
const winCls = isWinner ? ' winner' : '';
|
||
const infraLine = f.infra > 0 ? `<div class="breakdown-line"><span>Exadata infra</span><span>${fmt(f.infra)}</span></div>` : '';
|
||
const ecpuNote = f.ecpuNote ? `<div class="ecpu-note">${f.ecpuNote}</div>` : '';
|
||
return `<div class="cost-result-card${winCls}">
|
||
${winLabel}
|
||
<span class="flavor-tag ${f.cls}">${f.name}</span>
|
||
<div class="total-price ${f.cls}">${fmt(f.total)}<span style="font-size:0.65rem;font-weight:400;color:var(--text-muted)">/mo</span></div>
|
||
${pct}
|
||
<div class="breakdown-line"><span>Compute (${f.ecpuLabel})</span><span>${fmt(f.compute)}</span></div>
|
||
${infraLine}
|
||
<div class="breakdown-line"><span>Storage</span><span>${f.storage > 0 ? fmt(f.storage) : '\u2014 (incl. in infra)'}</span></div>
|
||
<div class="breakdown-line"><span>Backup</span><span>${fmt(f.backup)}</span></div>
|
||
${ecpuNote}
|
||
</div>`;
|
||
}).join('') + '</div>';
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// SCENARIO TABLES — rendered from SCENARIOS data
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// FEATURE MATRIX — interactive table + requirement filters
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
function renderFeatureMatrix() {
|
||
if (typeof FEATURES === 'undefined') return;
|
||
|
||
// Render cloud platform selector
|
||
if (typeof CLOUD_PLATFORMS !== 'undefined') {
|
||
const cs = document.getElementById('cloudSelector');
|
||
if (cs) {
|
||
cs.innerHTML = '<span class="cloud-selector-label">Cloud platform:</span>' +
|
||
CLOUD_PLATFORMS.map((cp, i) =>
|
||
`<button class="cloud-btn${i === 0 ? ' active' : ''}" data-cidx="${i}" onclick="toggleCloud(this)">${cp.icon} ${cp.name}</button>` +
|
||
(cp.doc ? `<a href="${cp.doc}" target="_blank" class="cloud-doc-link" title="Regional availability for Oracle Database@${cp.name}">🔗</a>` : '')
|
||
).join('');
|
||
}
|
||
}
|
||
|
||
const container = document.getElementById('featureMatrixContainer');
|
||
if (!container) return;
|
||
|
||
const categories = [...new Set(FEATURES.map(f => f.category))];
|
||
const flavorNames = FLAVORS.map(fl => fl.short);
|
||
const flavorCls = FLAVORS.map(fl => fl.cls);
|
||
|
||
const versionBadge = v => v === '26ai' ? '<span class="ver-badge v26">26ai</span>' : v === '19c' ? '<span class="ver-badge v19">19c</span>' : v === 'both' ? '<span class="ver-badge vboth">19c</span> <span class="ver-badge vboth">26ai</span>' : '';
|
||
const availIcon = ok => ok ? '<span class="avail-yes">✓</span>' : '<span class="avail-no">✗</span>';
|
||
|
||
let html = '<table class="feature-table"><thead><tr>';
|
||
html += '<th style="text-align:left;min-width:200px">Feature / Capability</th>';
|
||
html += '<th style="width:60px">Version</th>';
|
||
flavorNames.forEach((n, i) => {
|
||
html += `<th style="color:var(--${flavorCls[i]}-color);min-width:70px">${n}</th>`;
|
||
});
|
||
html += '<th style="width:40px;text-align:center" title="Check to require this feature">Req.</th>';
|
||
html += '</tr></thead><tbody>';
|
||
|
||
categories.forEach(cat => {
|
||
html += `<tr class="feat-cat-row"><td colspan="${FLAVORS.length + 3}" class="feat-cat-label">${cat}</td></tr>`;
|
||
FEATURES.filter(f => f.category === cat).forEach((feat, idx) => {
|
||
const fIdx = FEATURES.indexOf(feat);
|
||
html += `<tr class="feat-row" data-fidx="${fIdx}">`;
|
||
html += `<td class="feat-name">${feat.name}${feat.doc ? ` <a href="${feat.doc}" target="_blank" class="feat-doc-link">↗</a>` : ''}</td>`;
|
||
html += `<td class="feat-ver">${versionBadge(feat.version)}</td>`;
|
||
feat.avail.forEach((ok, fi) => {
|
||
html += `<td class="feat-avail ${ok ? 'yes' : 'no'}">${availIcon(ok)}</td>`;
|
||
});
|
||
html += `<td class="feat-req"><input type="checkbox" class="feat-checkbox" data-fidx="${fIdx}" onchange="applyFeatureFilters()"></td>`;
|
||
html += '</tr>';
|
||
});
|
||
});
|
||
html += '</tbody></table>';
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function toggleCloud(btn) {
|
||
btn.classList.toggle('active');
|
||
applyFeatureFilters();
|
||
}
|
||
|
||
function applyFeatureFilters() {
|
||
if (typeof FEATURES === 'undefined') return;
|
||
const checkboxes = document.querySelectorAll('.feat-checkbox:checked');
|
||
const required = Array.from(checkboxes).map(cb => parseInt(cb.dataset.fidx));
|
||
|
||
// Determine cloud platform constraints
|
||
const activeCloudBtns = document.querySelectorAll('.cloud-btn.active');
|
||
const selectedClouds = Array.from(activeCloudBtns).map(b => parseInt(b.dataset.cidx));
|
||
const hasCloudFilter = typeof CLOUD_PLATFORMS !== 'undefined' && selectedClouds.length > 0 && selectedClouds.length < CLOUD_PLATFORMS.length;
|
||
|
||
// Determine which flavors satisfy ALL required features
|
||
let flavorOk = FLAVORS.map((_, fi) => {
|
||
return required.every(fIdx => FEATURES[fIdx].avail[fi]);
|
||
});
|
||
|
||
// Apply cloud platform filter
|
||
if (hasCloudFilter) {
|
||
flavorOk = flavorOk.map((ok, fi) => {
|
||
if (!ok) return false;
|
||
return selectedClouds.every(ci => CLOUD_PLATFORMS[ci].avail[fi]);
|
||
});
|
||
}
|
||
|
||
// Determine version requirement
|
||
const needs26ai = required.some(fIdx => FEATURES[fIdx].version === '26ai');
|
||
|
||
// Update version recommendation on each score chip
|
||
FLAVORS.forEach((_, i) => {
|
||
const el = document.getElementById('verRec' + i);
|
||
if (!el) return;
|
||
if (required.length === 0 && !hasCloudFilter) {
|
||
el.innerHTML = '';
|
||
} else if (!flavorOk[i]) {
|
||
el.innerHTML = ' <span class="ver-rec-tag excluded">not compatible</span>';
|
||
} else if (needs26ai) {
|
||
el.innerHTML = ' <span class="ver-rec-tag v26">26ai</span>';
|
||
} else {
|
||
el.innerHTML = ' <span class="ver-rec-tag vany">19c / 26ai</span>';
|
||
}
|
||
});
|
||
|
||
// Update filter status display
|
||
const statusEl = document.getElementById('featureFilterStatus');
|
||
if (statusEl) {
|
||
const hasAnyFilter = required.length > 0 || hasCloudFilter;
|
||
if (!hasAnyFilter) {
|
||
statusEl.innerHTML = '';
|
||
} else {
|
||
const okFlavors = FLAVORS.filter((_, i) => flavorOk[i]).map(fl => `<span class="flavor-label ${fl.cls}" style="font-size:0.65rem;padding:0.1rem 0.4rem;">${fl.short}</span>`);
|
||
const blockedFlavors = FLAVORS.filter((_, i) => !flavorOk[i]).map(fl => `<span class="flavor-label ${fl.cls}" style="font-size:0.65rem;padding:0.1rem 0.4rem;opacity:0.3;text-decoration:line-through;">${fl.short}</span>`);
|
||
const parts = [];
|
||
if (required.length > 0) parts.push(`<strong>${required.length}</strong> feature${required.length > 1 ? 's' : ''}`);
|
||
if (hasCloudFilter) parts.push(selectedClouds.map(ci => CLOUD_PLATFORMS[ci].name).join(' + '));
|
||
const verLabel = needs26ai ? ' · <span class="ver-badge v26">26ai required</span>' : required.length > 0 ? ' · <span class="ver-badge vboth">19c / 26ai</span>' : '';
|
||
statusEl.innerHTML = `
|
||
<div class="filter-status-bar">
|
||
<div class="filter-status-left">
|
||
${parts.join(' · ')}${verLabel}
|
||
</div>
|
||
<div class="filter-status-right">
|
||
${okFlavors.join(' ')} ${blockedFlavors.join(' ')}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
}
|
||
|
||
// Apply visual dimming to the Decision Matrix score chips
|
||
const hasAnyFilter = required.length > 0 || hasCloudFilter;
|
||
document.querySelectorAll('.score-chip').forEach(chip => {
|
||
const fIdx = parseInt(chip.dataset.flavorIdx);
|
||
if (!isNaN(fIdx)) {
|
||
if (hasAnyFilter && !flavorOk[fIdx]) {
|
||
chip.classList.add('filtered-out');
|
||
} else {
|
||
chip.classList.remove('filtered-out');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Dim feature rows that are checked
|
||
document.querySelectorAll('.feat-row').forEach(row => {
|
||
const fIdx = parseInt(row.dataset.fidx);
|
||
if (required.includes(fIdx)) {
|
||
row.classList.add('feat-required');
|
||
} else {
|
||
row.classList.remove('feat-required');
|
||
}
|
||
});
|
||
|
||
// Dim cost cards
|
||
document.querySelectorAll('.cost-result-card').forEach(card => {
|
||
const cls = card.querySelector('.flavor-tag');
|
||
if (cls) {
|
||
const flavorClass = FLAVORS.map(fl => fl.cls).find(c => cls.classList.contains(c));
|
||
const fi = FLAVORS.findIndex(fl => fl.cls === flavorClass);
|
||
if (fi >= 0 && hasAnyFilter && !flavorOk[fi]) {
|
||
card.style.opacity = '0.35';
|
||
card.style.pointerEvents = 'none';
|
||
} else {
|
||
card.style.opacity = '';
|
||
card.style.pointerEvents = '';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
renderFeatureMatrix();
|
||
|
||
function renderScenarios() {
|
||
const flavorColors = ['var(--shared-color)','var(--spool-color)','var(--pool-color)','var(--dedicated-color)','var(--dedpool-color)'];
|
||
const flavorNames = FLAVORS.map(f => f.short);
|
||
document.getElementById('scenariosContainer').innerHTML = SCENARIOS.map((group, gi) => {
|
||
const heading = gi > 0 ? `<h3 style="font-size:1.05rem;margin:2rem 0 0.75rem;color:var(--text-primary);">${group.title}</h3>` : '';
|
||
const rows = group.rows.map(r => `<tr>
|
||
<td>${r.label}<br><span style="font-weight:400;color:var(--text-muted);font-size:0.7rem">${r.sub}</span></td>
|
||
${r.flavors.map(f => `<td>${f}</td>`).join('')}
|
||
<td class="takeaway">${r.takeaway}</td>
|
||
</tr>`).join('');
|
||
return `${heading}
|
||
<table class="scenario-table">
|
||
<thead><tr>
|
||
<th>${group.firstCol}</th>
|
||
${flavorNames.map((n,i) => `<th style="color:${flavorColors[i]}">${n}</th>`).join('')}
|
||
<th>Takeaway</th>
|
||
</tr></thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>`;
|
||
}).join('');
|
||
}
|
||
renderScenarios();
|
||
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
// TAB DEEP LINKING — #overview, #scenarios, #matrix, #cost
|
||
// ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
// Auto-hide tab nav on page scroll down, reveal on scroll up
|
||
(function(){
|
||
var last=0, nav=document.querySelector('.tab-nav');
|
||
if(!nav) return;
|
||
window.addEventListener('scroll',function(){
|
||
var y=window.scrollY;
|
||
if(y>200 && y>last+5) nav.classList.add('nav-hidden');
|
||
else if(y<last-5) nav.classList.remove('nav-hidden');
|
||
last=y;
|
||
},{passive:true});
|
||
})();
|
||
|
||
const TAB_IDS = ['overview','scenarios','matrix','cost'];
|
||
|
||
function activateTab(tabName) {
|
||
if (!TAB_IDS.includes(tabName)) return;
|
||
const radio = document.getElementById('tab-' + tabName);
|
||
if (radio) radio.checked = true;
|
||
}
|
||
|
||
function copyTabLink(tabName) {
|
||
const url = window.location.href.split('#')[0] + '#' + tabName;
|
||
navigator.clipboard.writeText(url).then(() => {
|
||
// Find the clicked button by the tab name
|
||
const btns = document.querySelectorAll('.share-link');
|
||
btns.forEach(btn => {
|
||
if (btn.getAttribute('onclick') && btn.getAttribute('onclick').includes(tabName)) {
|
||
btn.classList.add('copied');
|
||
btn.querySelector('span').textContent = 'Copied!';
|
||
setTimeout(() => {
|
||
btn.classList.remove('copied');
|
||
btn.querySelector('span').textContent = 'Share';
|
||
}, 2000);
|
||
}
|
||
});
|
||
}).catch(() => {
|
||
// Fallback: select the URL in a prompt
|
||
prompt('Copy this link:', url);
|
||
});
|
||
// Also update the hash without scrolling
|
||
try { history.replaceState(null, '', '#' + tabName); } catch(e) { /* sandboxed iframe */ }
|
||
}
|
||
|
||
// On page load: read hash and activate tab
|
||
function initTabFromHash() {
|
||
try {
|
||
const hash = window.location.hash.replace('#','');
|
||
if (hash && TAB_IDS.includes(hash)) {
|
||
activateTab(hash);
|
||
}
|
||
} catch(e) { /* sandboxed */ }
|
||
}
|
||
|
||
// Update hash when tab is clicked via label
|
||
document.querySelectorAll('.tab-nav label').forEach(label => {
|
||
label.addEventListener('click', () => {
|
||
const forId = label.getAttribute('for'); // e.g. "tab-scenarios"
|
||
const tabName = forId.replace('tab-','');
|
||
try { history.replaceState(null, '', '#' + tabName); } catch(e) {}
|
||
});
|
||
});
|
||
|
||
// Listen for back/forward
|
||
window.addEventListener('hashchange', () => initTabFromHash());
|
||
|
||
// Initialize
|
||
initTabFromHash();
|
||
loadCostPreset('small');
|
||
</script>
|
||
</body>
|
||
</html>
|