├── .gitignore
├── CNAME
├── images
├── 4ad.svg
├── 4ae.svg
├── 4x.svg
├── 5a.svg
├── 5b.svg
├── 6a.png
├── 7a.jpg
├── Figure 0100 horizontal.svg
├── Figure 0100.svg
├── Figure 0200.svg
├── Figure 0300.svg
├── Figure 0700 Spreadsheet.png
├── Figure 0800.svg
├── Figure 1300.svg
├── Figure 1600.jpg
├── Figure 1900.svg
├── Figure 2000-2Percent-DelaySimulation.png
├── Figure 2000-4Percent-DelaySimulation.png
├── Figure 2100.svg
├── Figure 2200.svg
├── Figure 2300.svg
├── Figure 2400.svg
├── Figure 2500 Figure 4.w.svg
├── Figure 3000 General Costs.png
├── favicon.ico
├── meta.jpg
└── pipecat.svg
├── index.html
├── script
├── binary-numbers.css
├── fetch-contributors.py
├── footnotes.js
├── pagination.js
└── position-footnotes.js
└── styles.css
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | voiceaiandvoiceagents.com
--------------------------------------------------------------------------------
/images/4ae.svg:
--------------------------------------------------------------------------------
1 |
43 |
--------------------------------------------------------------------------------
/images/6a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/6a.png
--------------------------------------------------------------------------------
/images/7a.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/7a.jpg
--------------------------------------------------------------------------------
/images/Figure 0100 horizontal.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/images/Figure 0100.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/images/Figure 0300.svg:
--------------------------------------------------------------------------------
1 |
53 |
--------------------------------------------------------------------------------
/images/Figure 0700 Spreadsheet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/Figure 0700 Spreadsheet.png
--------------------------------------------------------------------------------
/images/Figure 1600.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/Figure 1600.jpg
--------------------------------------------------------------------------------
/images/Figure 2000-2Percent-DelaySimulation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/Figure 2000-2Percent-DelaySimulation.png
--------------------------------------------------------------------------------
/images/Figure 2000-4Percent-DelaySimulation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/Figure 2000-4Percent-DelaySimulation.png
--------------------------------------------------------------------------------
/images/Figure 2200.svg:
--------------------------------------------------------------------------------
1 |
72 |
--------------------------------------------------------------------------------
/images/Figure 3000 General Costs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/Figure 3000 General Costs.png
--------------------------------------------------------------------------------
/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/favicon.ico
--------------------------------------------------------------------------------
/images/meta.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pipecat-ai/voice-ai-primer-web/534dca469508f1ca12f4403e42b676e20c67f230/images/meta.jpg
--------------------------------------------------------------------------------
/images/pipecat.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/script/binary-numbers.css:
--------------------------------------------------------------------------------
1 | /* Binary paragraph numbers */
2 | .binary-paragraph-number {
3 | font-family: monospace;
4 | font-size: 0.8rem;
5 | color: var(--muted);
6 | opacity: 0.3;
7 | text-align: right;
8 | letter-spacing: 1px;
9 | white-space: nowrap;
10 | padding-right: 15px; /* Space between binary text and content */
11 | width: 60px; /* Fixed width for alignment */
12 | }
13 |
14 | .binary-container {
15 | position: absolute;
16 | left: -120px; /* Position outside the content area */
17 | width: auto; /* Allow natural width */
18 | text-align: right; /* Align text to the right */
19 | pointer-events: none;
20 | z-index: 5;
21 | transition: opacity 0.2s ease; /* Add smooth transition */
22 | }
23 |
24 | /* Adjust positioning for smaller screens but not mobile */
25 | @media (max-width: 1200px) and (min-width: 769px) {
26 | .binary-container {
27 | left: -60px; /* Slightly closer on smaller screens */
28 | }
29 | }
30 |
31 | /* Media query for mobile */
32 | @media (max-width: 768px) {
33 | .binary-container {
34 | display: none; /* Hide binary numbers on mobile */
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/script/fetch-contributors.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 |
4 | def fetch_contributors_by_commits(owner, repo, include_anonymous=False):
5 | """
6 | Fetches contributors to the specified GitHub repository, sorted by the number of commits in descending order.
7 |
8 | Parameters:
9 | - owner (str): The username or organization that owns the repository.
10 | - repo (str): The name of the repository.
11 | - include_anonymous (bool): Whether to include anonymous contributors.
12 |
13 | Returns:
14 | - str: A comma-separated list of contributor usernames.
15 | """
16 | contributors = []
17 | page = 1
18 | per_page = 100 # Maximum allowed per GitHub API
19 |
20 | while True:
21 | url = f"https://api.github.com/repos/{owner}/{repo}/contributors"
22 | params = {"per_page": per_page, "page": page}
23 | if include_anonymous:
24 | params["anon"] = "true"
25 | response = requests.get(url, params=params)
26 |
27 | if response.status_code != 200:
28 | raise Exception(f"GitHub API error: {response.status_code} - {response.text}")
29 |
30 | data = response.json()
31 | if not data:
32 | break
33 |
34 | # Extract usernames
35 | for user in data:
36 | if "login" in user:
37 | contributors.append(user["login"])
38 | elif include_anonymous and "name" in user:
39 | contributors.append(user["name"])
40 | page += 1
41 |
42 | return ", ".join(contributors)
43 |
44 |
45 | # Example usage:
46 | if __name__ == "__main__":
47 | owner = "pipecat-ai"
48 | repo = "pipecat"
49 | usernames = fetch_contributors_by_commits(owner, repo)
50 | print(usernames)
51 |
--------------------------------------------------------------------------------
/script/footnotes.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('DOMContentLoaded', () => {
2 | // Get all superscript elements
3 | const superscripts = document.querySelectorAll('sup');
4 |
5 | superscripts.forEach(sup => {
6 | sup.style.cursor = 'pointer';
7 |
8 | sup.addEventListener('click', (e) => {
9 | e.preventDefault();
10 |
11 | // Get the footnote number from the superscript text
12 | const footnoteNum = sup.textContent.replace(/[\[\]]/g, '');
13 |
14 | // Find the footnote by matching the text content
15 | const footnotes = document.querySelectorAll('.footnote p');
16 | const footnote = Array.from(footnotes).find(p =>
17 | p.textContent.startsWith(`[${footnoteNum}]`)
18 | )?.closest('.footnote');
19 |
20 | if (footnote) {
21 | footnote.scrollIntoView({
22 | behavior: 'smooth',
23 | block: 'center'
24 | });
25 |
26 | // Add a brief highlight effect
27 | footnote.style.transition = 'background-color 0.8s';
28 | footnote.style.backgroundColor = 'rgba(0,0,0,0.1)';
29 | footnote.style.padding = '0.1rem';
30 | footnote.style.paddingLeft = '0.2rem';
31 | footnote.style.paddingRight = '0.2rem';
32 | footnote.style.borderRadius = '0.1rem';
33 | setTimeout(() => {
34 | footnote.style.backgroundColor = 'transparent';
35 | }, 1000);
36 | }
37 | });
38 | });
39 | });
--------------------------------------------------------------------------------
/script/pagination.js:
--------------------------------------------------------------------------------
1 | function setupBinaryParagraphNumbers() {
2 | // Remove any existing page indicator
3 | const existingIndicator = document.getElementById('page-indicator');
4 | if (existingIndicator) {
5 | existingIndicator.remove();
6 | }
7 |
8 | // Clear any existing binary numbers first
9 | document.querySelectorAll('.binary-container').forEach(el => el.remove());
10 |
11 | // Get all paragraphs in the chapter content
12 | const paragraphs = document.querySelectorAll('.chapter-content p');
13 |
14 | // Function to convert decimal to 12-bit binary with "/" for 0 and "\" for 1
15 | function decimalToBinary(decimal) {
16 | let binary = decimal.toString(2).padStart(12, '0');
17 | return binary.replace(/0/g, '/').replace(/1/g, '\\');
18 | }
19 |
20 | // Process each paragraph
21 | paragraphs.forEach((paragraph, index) => {
22 | // Get the corresponding chapter row
23 | const chapterRow = paragraph.closest('.chapter-row');
24 | if (!chapterRow) return;
25 |
26 | // Create binary number element
27 | const binaryNumber = document.createElement('div');
28 | binaryNumber.className = 'binary-paragraph-number';
29 | binaryNumber.textContent = decimalToBinary(index + 1);
30 |
31 | // Create a container for the binary number that will be positioned
32 | const binaryContainer = document.createElement('div');
33 | binaryContainer.className = 'binary-container';
34 | binaryContainer.style.opacity = '0'; // Start hidden
35 | binaryContainer.appendChild(binaryNumber);
36 |
37 | // Add the binary container to the chapter row instead of chapter notes
38 | chapterRow.appendChild(binaryContainer);
39 |
40 | // Set initial position
41 | updateBinaryPosition(paragraph, binaryContainer);
42 |
43 | // Add a data attribute to link the paragraph and its binary number
44 | paragraph.dataset.binaryIndex = index;
45 | binaryContainer.dataset.binaryIndex = index;
46 |
47 | // Add hover event listeners
48 | paragraph.addEventListener('mouseenter', () => {
49 | binaryContainer.style.opacity = '1';
50 | });
51 |
52 | paragraph.addEventListener('mouseleave', () => {
53 | binaryContainer.style.opacity = '0';
54 | });
55 | });
56 | }
57 |
58 | // Function to update the position of a binary number container
59 | function updateBinaryPosition(paragraph, binaryContainer) {
60 | const chapterRow = paragraph.closest('.chapter-row');
61 | if (!chapterRow) return;
62 |
63 | const paragraphRect = paragraph.getBoundingClientRect();
64 | const rowRect = chapterRow.getBoundingClientRect();
65 |
66 | // Calculate the top position relative to the chapter row
67 | // Add a small offset to align with the first line of text
68 | const topPosition = paragraphRect.top - rowRect.top + 3;
69 |
70 | // Set the top position only - left is handled by CSS
71 | binaryContainer.style.top = `${topPosition}px`;
72 | }
73 |
74 | // Update all binary positions on scroll
75 | function updateAllBinaryPositions() {
76 | const paragraphs = document.querySelectorAll('.chapter-content p');
77 |
78 | paragraphs.forEach(paragraph => {
79 | const index = paragraph.dataset.binaryIndex;
80 | if (index !== undefined) {
81 | const binaryContainer = document.querySelector(`.binary-container[data-binary-index="${index}"]`);
82 | if (binaryContainer) {
83 | updateBinaryPosition(paragraph, binaryContainer);
84 | }
85 | }
86 | });
87 | }
88 |
89 | // Run on load and whenever the window is resized or scrolled
90 | document.addEventListener('DOMContentLoaded', () => {
91 | setupBinaryParagraphNumbers();
92 | // Initial update after a short delay to ensure everything is rendered
93 | setTimeout(updateAllBinaryPositions, 100);
94 | });
95 | window.addEventListener('resize', () => {
96 | setupBinaryParagraphNumbers();
97 | // Update after a short delay to ensure everything is rendered
98 | setTimeout(updateAllBinaryPositions, 100);
99 | });
100 | window.addEventListener('scroll', updateAllBinaryPositions);
--------------------------------------------------------------------------------
/script/position-footnotes.js:
--------------------------------------------------------------------------------
1 | function positionFootnotes() {
2 | const anchors = document.querySelectorAll('[data-footnote-ref]');
3 | anchors.forEach(anchor => {
4 | const footnoteId = anchor.getAttribute('data-footnote-ref');
5 | const footnote = document.querySelector(`#${footnoteId}`);
6 | if (footnote) {
7 | const anchorRect = anchor.getBoundingClientRect();
8 | const containerRect = document.querySelector('.chapter-row').getBoundingClientRect();
9 | const offsetTop = anchorRect.top - containerRect.top;
10 | footnote.style.top = offsetTop + 'px';
11 | }
12 | });
13 | }
14 | window.addEventListener('load', positionFootnotes);
15 | window.addEventListener('resize', positionFootnotes);
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --font-dm-sans: "DM Sans", sans-serif;
3 | --font-source-serif: "source-serif-4-subhead", serif;
4 | --foreground: #000000;
5 | --background: #f4f5f6;
6 | --border: #000000;
7 | --muted: #6b7280;
8 | }
9 |
10 | * {
11 | box-sizing: border-box;
12 | padding: 0;
13 | margin: 0;
14 | }
15 |
16 | html,
17 | body {
18 | max-width: 140vw;
19 | overflow-x: hidden;
20 | font-family: var(--font-source-serif);
21 | background-color: var(--background);
22 | color: var(--foreground);
23 | line-height: 1.6;
24 | font-size: 1.1rem;
25 | }
26 |
27 | a {
28 | color: inherit;
29 | text-decoration: none;
30 | border-bottom: 1px solid var(--foreground);
31 | }
32 |
33 | a:hover {
34 | opacity: 0.8;
35 | }
36 |
37 | h1,
38 | h2,
39 | h3 {
40 | font-weight: 700;
41 | line-height: 1.2;
42 | margin-top: 1rem;
43 | margin-bottom: 0.75rem;
44 | }
45 |
46 | h1 {
47 | font-size: 2rem;
48 | font-weight: 600;
49 | }
50 | h2 {
51 | font-size: 1.5rem;
52 | font-weight: 600;
53 | }
54 |
55 | h3 {
56 | font-size: 1.25rem;
57 | font-weight: 600;
58 | }
59 |
60 | .title {
61 | font-size: 4rem;
62 | font-weight: 700;
63 | margin-bottom: 0.5rem;
64 | }
65 |
66 | .subtitle {
67 | font-size: 2.2rem;
68 | font-weight: 400;
69 | margin-top: 0.1rem;
70 | }
71 |
72 | .header {
73 | margin-bottom: 0.5rem;
74 | }
75 |
76 | .table-of-contents-title {
77 | margin-top: 0.5rem;
78 | margin-bottom: 1.2rem;
79 | }
80 |
81 | code {
82 | font-family: monospace;
83 | background-color: rgba(0, 0, 0, 0.05);
84 | padding: 0.2em 0.4em;
85 | border-radius: 3px;
86 | font-size: 0.9em;
87 | }
88 |
89 | /* Container */
90 | .container {
91 | max-width: 1300px;
92 | margin: 0 auto;
93 | padding: 2rem;
94 | }
95 |
96 | /* Header */
97 | header {
98 | margin-bottom: 2rem;
99 | padding-bottom: 1rem;
100 | /* border-bottom: 1px solid var(--border); */
101 | }
102 |
103 | /* Table of Contents */
104 | #table-of-contents {
105 | margin-bottom: 3rem;
106 | padding: 1.5rem;
107 | border: 1px solid var(--border);
108 | background-color: rgba(0, 0, 0, 0.02);
109 | }
110 |
111 | #table-of-contents ol {
112 | margin-left: 1.5rem;
113 | }
114 |
115 | #table-of-contents li {
116 | margin-bottom: 0.5rem;
117 | }
118 |
119 | /* New table of contents styling for better hierarchy */
120 | #table-of-contents > ol > li > a {
121 | font-size: 1.1rem;
122 | font-weight: 500;
123 | }
124 |
125 | #table-of-contents > ol > li > ul > li > a {
126 | font-size: 0.95rem;
127 | font-weight: 400;
128 | }
129 |
130 | #table-of-contents > ol > li > ul > li > ul > li > a {
131 | font-size: 0.9rem;
132 | font-weight: 400;
133 | color: var(--muted);
134 | }
135 |
136 | #table-of-contents ul {
137 | margin-top: 0.3rem;
138 | margin-bottom: 0.5rem;
139 | }
140 |
141 | /* Content */
142 | section {
143 | margin-bottom: 0;
144 | }
145 |
146 | p {
147 | margin-bottom: 1rem;
148 | }
149 |
150 | ul,
151 | ol {
152 | margin-left: 1.5rem;
153 | margin-bottom: 1.5rem;
154 | }
155 |
156 | li {
157 | margin-bottom: 0.5rem;
158 | }
159 |
160 | sup {
161 | font-size: 0.75em;
162 | vertical-align: super;
163 | line-height: 0;
164 | cursor: pointer;
165 | }
166 |
167 | sup:hover {
168 | text-decoration: underline;
169 | }
170 |
171 | .arrow-list {
172 | list-style: none;
173 | margin-left: 0;
174 | }
175 |
176 | .arrow-list li {
177 | position: relative;
178 | padding-left: 1.5rem;
179 | }
180 |
181 | .arrow-list li::before {
182 | content: "→";
183 | position: absolute;
184 | left: 0;
185 | color: var(--foreground);
186 | }
187 |
188 | .footnote .arrow-list li {
189 | margin: 0;
190 | }
191 |
192 | ol.list-decimal {
193 | list-style-type: decimal;
194 | }
195 |
196 | ol.list-decimal li {
197 | margin-bottom: 0.75rem;
198 | }
199 |
200 | /* Grid */
201 | .grid {
202 | display: grid;
203 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
204 | gap: 2rem;
205 | margin-bottom: 2rem;
206 | }
207 |
208 | /* Footer */
209 | footer {
210 | margin-top: 4rem;
211 | padding-top: 1.5rem;
212 | border-top: 1px solid var(--border);
213 | font-size: 0.9rem;
214 | color: var(--muted);
215 | }
216 |
217 | footer p {
218 | margin-bottom: 0.5rem;
219 | }
220 |
221 | .github-link {
222 | margin-top: 1rem;
223 | }
224 |
225 | .github-link a {
226 | color: var(--foreground);
227 | font-weight: 500;
228 | border-bottom: 1px solid var(--foreground);
229 | transition: opacity 0.2s ease;
230 | }
231 |
232 | .github-link a:hover {
233 | opacity: 0.7;
234 | }
235 |
236 | /* Chapter Row Layout */
237 | .chunk-row {
238 | display: flex;
239 | position: relative;
240 | gap: 3rem;
241 | justify-content: space-between;
242 | }
243 |
244 | .chunk-content {
245 | width: calc(75% - 1.5rem);
246 | flex: 0 0 auto;
247 | }
248 |
249 | .chunk-notes {
250 | width: calc(25% - 1.5rem);
251 | flex: 0 0 auto;
252 | font-size: 0.9rem;
253 | color: var(--muted);
254 | display: flex;
255 | flex-direction: column;
256 | position: relative;
257 | }
258 |
259 | .chapter-image {
260 | margin-top: 3rem;
261 | }
262 |
263 | /* Footnotes aligned at the bottom of chapter content */
264 | .chunk-footnotes {
265 | position: static;
266 | padding-left: 1rem;
267 | /* border-left: 1px solid var(--border); */
268 | margin-top: auto;
269 | }
270 |
271 | .chunk-footnotes h3 {
272 | font-size: 1rem;
273 | margin-top: 0;
274 | margin-bottom: 1rem;
275 | color: var(--foreground);
276 | opacity: 0.7;
277 | }
278 |
279 | .footnote {
280 | margin-bottom: 1.5rem;
281 | }
282 |
283 | /* Responsive adjustments */
284 | @media (max-width: 768px) {
285 | .chunk-row {
286 | flex-direction: column;
287 | gap: 1rem;
288 | }
289 |
290 | .chunk-content {
291 | width: 100%;
292 | flex: 1 1 auto;
293 | }
294 |
295 | .chunk-notes {
296 | width: 100%;
297 | flex: 1 1 auto;
298 | justify-content: flex-start;
299 | }
300 |
301 | .chunk-footnotes {
302 | border-left: none;
303 | border-top: 1px solid var(--border);
304 | padding-left: 0;
305 | padding-top: 1rem;
306 | margin-top: 1rem;
307 | }
308 |
309 | .image-hide-narrow {
310 | display: none;
311 | }
312 |
313 | }
314 |
315 | .chunk-image {
316 | margin-top: 40px;
317 | width: 100%;
318 | display: flex;
319 | flex-direction: column;
320 | align-items: center;
321 | }
322 |
323 | /* Class for images that should appear within the content flow */
324 | .chunk-image-inline {
325 | display: flex;
326 | margin: 2rem auto;
327 | }
328 |
329 |
330 | .image-caption {
331 | margin-top: 0.75rem;
332 | margin-bottom: 1.5rem;
333 | font-size: 0.9rem;
334 | color: var(--muted);
335 | text-align: center;
336 | font-style: italic;
337 | font-weight: 400;
338 | max-width: 90%;
339 | margin-left: auto;
340 | margin-right: auto;
341 | }
342 |
343 | /* Binary paragraph numbers */
344 | .binary-paragraph-number {
345 | font-family: monospace;
346 | font-size: 0.9rem;
347 | color: var(--muted);
348 | opacity: 0.6;
349 | text-align: left;
350 | letter-spacing: 1px;
351 | }
352 |
353 | .binary-container {
354 | position: absolute;
355 | right: 0;
356 | width: 100%;
357 | text-align: left;
358 | pointer-events: none;
359 | }
360 |
361 | /* Remove page indicator styles since we're not using it anymore */
362 | #page-indicator {
363 | display: none;
364 | }
365 |
366 | pre {
367 | background-color: #bdbdbd;
368 | border: 1px solid #ddd;
369 | border-radius: 4px;
370 | padding: 16px;
371 | overflow: auto;
372 | margin: 16px 0;
373 | max-width: 100%;
374 | box-sizing: border-box;
375 | }
376 |
377 | pre code {
378 | background-color: transparent;
379 | padding: 0;
380 | border: none;
381 | font-family: "DM Mono", monospace, Consolas, Monaco, "Andale Mono",
382 | "Ubuntu Mono";
383 | font-size: 0.7em;
384 | line-height: 1.5;
385 | white-space: pre-wrap;
386 | word-break: break-word;
387 | display: block;
388 | width: 100%;
389 | }
390 |
391 | pre code .pre-highlight {
392 | background-color: #f2f2f2;
393 | }
394 |
395 | .nobreak {
396 | white-space: nowrap;
397 | word-break: keep-all;
398 | }
399 |
400 |
401 | .data-table {
402 | width: 100%;
403 | border-collapse: collapse;
404 | margin-top: 1em;
405 | margin-bottom: 1em;
406 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
407 | font-size: 75%;
408 | font-family: "DM Mono", monospace;
409 | }
410 |
411 | .data-table th {
412 | background-color: #f2f2f2;
413 | color: #333;
414 | font-weight: 600;
415 | text-align: left;
416 | padding: 0.75em 0.5em;
417 | border: 1px solid #ddd;
418 | }
419 |
420 | .data-table td {
421 | padding: 0.25em 0.5em;
422 | border: 1px solid #ddd;
423 | vertical-align: middle;
424 | }
425 |
426 | .data-table tr:nth-child(even) {
427 | background-color: #f8f8f8;
428 | }
429 |
430 | .data-table tr:hover {
431 | background-color: #f0f0f0;
432 | }
433 |
434 | .latency-breakdown {
435 | font-size: 60%;
436 | }
437 |
438 | .latency-breakdown th:last-child {
439 | text-align: right;
440 | }
441 |
442 | .latency-breakdown tr {
443 | background-color: #f8f8f8
444 | }
445 |
446 | .latency-breakdown .network-row {
447 | background-color: #f2f2f2;
448 | }
449 |
450 | .latency-breakdown td:last-child {
451 | text-align: right;
452 | font-family: "DM Mono", monospace;
453 | }
454 |
455 | .table-caption {
456 | font-size: 0.85em;
457 | margin-top: 8px;
458 | margin-bottom: 16px;
459 | font-style: italic;
460 | color: #555;
461 | }
462 |
463 | /* Specific styling for cost and latency tables */
464 |
465 | .model-comparison td:first-child {
466 | font-weight: 500;
467 | }
468 |
469 | .model-comparison td:not(:first-child) {
470 | text-align: center;
471 | }
472 |
473 | .total-row {
474 | border-top: 2px solid #ccc;
475 | background-color: #f2f2f2 !important;
476 | }
477 |
478 | .total-row td {
479 | font-weight: 600;
480 | }
481 |
482 | .footnote-image {
483 | display: block;
484 | max-width: 100%;
485 | height: auto;
486 | margin: 12px 0;
487 | border-radius: 4px;
488 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
489 | }
490 |
491 | .footnote .image-caption {
492 | font-size: 0.85em;
493 | margin-top: 4px;
494 | margin-bottom: 12px;
495 | font-style: italic;
496 | color: #555;
497 | }
498 |
499 | .language-python {
500 | font-family: "DM Mono", monospace;
501 | background-color: #343433;
502 | color: #f8f8f8;
503 | padding: 0;
504 | }
505 |
506 | .subtle-code {
507 | background-color: #f4f5f6;
508 | color: #1e773e;
509 | }
510 |
--------------------------------------------------------------------------------