Add consistent header/footer to standalone /tools page
The /tools page now has the same chrome as the main page: - Skip-to-content link - Full navigation bar with all dropdown menus - Accessibility notice in footer - Footer CTA with shimmer border and contact modal - Matomo + PostHog analytics scripts - Same max-width (800px) and font/color variables Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b39bf68f06
commit
cde28f7069
412
src/tools.html
412
src/tools.html
|
|
@ -9,202 +9,231 @@
|
|||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #ffffff; --text-color: #333333; --accent-color: #0056b3;
|
||||
--border-color: #e0e0e0; --hover-color: #003d82; --theme-bg: #f5f5f5;
|
||||
--theme-hover: #e0e0e0; --date-color: #555555;
|
||||
--bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #eaeaea;
|
||||
--text-primary: #333333; --focus-outline-color: #0056b3;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: #1a1a1a; --text-color: #e0e0e0; --accent-color: #5fa9ff;
|
||||
--border-color: #404040; --hover-color: #8ac2ff; --theme-bg: #2d2d2d;
|
||||
--theme-hover: #3d3d3d; --date-color: #a0a0a0;
|
||||
--bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #3d3d3d;
|
||||
--text-primary: #e0e0e0; --focus-outline-color: #5fa9ff;
|
||||
}
|
||||
}
|
||||
html[data-theme='light'] {
|
||||
--bg-color: #ffffff; --text-color: #333333; --accent-color: #0056b3;
|
||||
--border-color: #e0e0e0; --hover-color: #003d82; --theme-bg: #f5f5f5;
|
||||
--theme-hover: #e0e0e0; --date-color: #555555;
|
||||
--bg-primary: #ffffff; --bg-secondary: #f5f5f5; --bg-tertiary: #eaeaea;
|
||||
--text-primary: #333333; --focus-outline-color: #0056b3;
|
||||
}
|
||||
html[data-theme='dark'] {
|
||||
--bg-color: #1a1a1a; --text-color: #e0e0e0; --accent-color: #5fa9ff;
|
||||
--border-color: #404040; --hover-color: #8ac2ff; --theme-bg: #2d2d2d;
|
||||
--theme-hover: #3d3d3d; --date-color: #a0a0a0;
|
||||
--bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --bg-tertiary: #3d3d3d;
|
||||
--text-primary: #e0e0e0; --focus-outline-color: #5fa9ff;
|
||||
}
|
||||
:root { --bg-color:#ffffff;--text-color:#333;--accent-color:#0056b3;--border-color:#e0e0e0;--hover-color:#003d82;--theme-bg:#f5f5f5;--theme-border:#ddd;--theme-hover:#e0e0e0;--date-color:#555;--bg-primary:#ffffff;--bg-secondary:#f5f5f5;--bg-tertiary:#eaeaea;--bg-hover:#f0f0f0;--text-primary:#333;--focus-outline-color:#0056b3; }
|
||||
@media (prefers-color-scheme: dark) { :root { --bg-color:#1a1a1a;--text-color:#e0e0e0;--accent-color:#5fa9ff;--border-color:#404040;--hover-color:#8ac2ff;--theme-bg:#2d2d2d;--theme-border:#404040;--theme-hover:#3d3d3d;--date-color:#a0a0a0;--bg-primary:#1a1a1a;--bg-secondary:#2d2d2d;--bg-tertiary:#3d3d3d;--bg-hover:#333;--text-primary:#e0e0e0;--focus-outline-color:#5fa9ff; } }
|
||||
html[data-theme='light'] { --bg-color:#ffffff;--text-color:#333;--accent-color:#0056b3;--border-color:#e0e0e0;--hover-color:#003d82;--theme-bg:#f5f5f5;--theme-border:#ddd;--theme-hover:#e0e0e0;--date-color:#555;--bg-primary:#ffffff;--bg-secondary:#f5f5f5;--bg-tertiary:#eaeaea;--bg-hover:#f0f0f0;--text-primary:#333;--focus-outline-color:#0056b3; }
|
||||
html[data-theme='dark'] { --bg-color:#1a1a1a;--text-color:#e0e0e0;--accent-color:#5fa9ff;--border-color:#404040;--hover-color:#8ac2ff;--theme-bg:#2d2d2d;--theme-border:#404040;--theme-hover:#3d3d3d;--date-color:#a0a0a0;--bg-primary:#1a1a1a;--bg-secondary:#2d2d2d;--bg-tertiary:#3d3d3d;--bg-hover:#333;--text-primary:#e0e0e0;--focus-outline-color:#5fa9ff; }
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
line-height: 1.6; color: var(--text-color); background-color: var(--bg-color);
|
||||
margin: 0 auto; padding: 20px; max-width: 700px;
|
||||
}
|
||||
.skip-to-content { position: absolute; top: -40px; left: 0; background: var(--accent-color); color: white; padding: 8px 16px; text-decoration: none; z-index: 100; border-radius: 0 0 4px 0; font-weight: bold; }
|
||||
.skip-to-content:focus { top: 0; outline: 3px solid var(--focus-outline-color); outline-offset: 2px; }
|
||||
body { font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; color: var(--text-color); background-color: var(--bg-color); margin: 0 auto; padding: 20px; max-width: 800px; }
|
||||
a { color: var(--accent-color); text-decoration: underline; }
|
||||
a:hover { color: var(--hover-color); }
|
||||
.main-nav { display: flex; justify-content: center; margin: 1rem 0; }
|
||||
.main-nav ul { display: flex; list-style: none; margin: 0; padding: 0.5rem 1rem; gap: 1rem; border-radius: 4px; background-color: var(--theme-bg); box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
.main-nav li { margin: 0; padding: 0; position: relative; }
|
||||
.main-nav a { display: block; padding: 0.5rem 1rem; text-decoration: none; color: var(--text-color); font-weight: 500; border-radius: 4px; transition: background-color 0.3s, color 0.3s; }
|
||||
.main-nav a:hover { background-color: var(--theme-hover); color: var(--accent-color); }
|
||||
.main-nav .dropdown > a::after { content: "\25BC"; font-size: 0.7em; margin-left: 0.5em; }
|
||||
.main-nav .dropdown-content { display: none; position: absolute; top: 100%; left: 0; background-color: var(--theme-bg); min-width: 200px; box-shadow: 0 8px 16px rgba(0,0,0,0.15); z-index: 1000; border-radius: 4px; padding: 0.5rem 0; margin-top: 0.25rem; }
|
||||
.main-nav .dropdown:hover .dropdown-content { display: block; }
|
||||
.main-nav .dropdown-content a { padding: 0.5rem 1rem; display: block; white-space: nowrap; border-radius: 0; }
|
||||
.theme-switch { position: fixed; top: 20px; right: 20px; z-index: 1000; }
|
||||
.theme-switch button {
|
||||
background: var(--theme-bg); border: 1px solid var(--border-color); font-size: 1.5em;
|
||||
cursor: pointer; padding: 8px 12px; border-radius: 8px; transition: background-color 0.3s;
|
||||
}
|
||||
.theme-switch button { background: var(--theme-bg); border: 1px solid var(--border-color); font-size: 1.5em; cursor: pointer; padding: 8px 12px; border-radius: 8px; transition: background-color 0.3s; }
|
||||
.theme-switch button:hover { background-color: var(--theme-hover); }
|
||||
h1 {
|
||||
font-size: 1.75em; color: var(--accent-color); border-bottom: 2px solid var(--accent-color);
|
||||
padding-bottom: 0.3em; margin-bottom: 0.25em;
|
||||
}
|
||||
h1 { font-size: 2em; color: var(--accent-color); border-bottom: 2px solid var(--accent-color); padding-bottom: 0.3em; margin-top: 1.5em; margin-bottom: 0.5em; }
|
||||
h3 { font-size: 1.1em; color: var(--text-color); margin-top: 0; margin-bottom: 0.75rem; }
|
||||
.subtitle { color: var(--date-color); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
||||
.back-link { font-size: 0.85rem; margin-bottom: 1rem; display: block; }
|
||||
.card {
|
||||
background: var(--bg-secondary); border: 1px solid var(--border-color);
|
||||
border-radius: 8px; padding: 1.5rem; margin-bottom: 1.25rem;
|
||||
}
|
||||
.subtitle { color: var(--date-color); font-style: italic; margin-bottom: 1.5rem; }
|
||||
.card { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; padding: 1.5rem; margin-bottom: 1.25rem; }
|
||||
.controls { display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: flex-end; }
|
||||
.field { flex: 1; min-width: 120px; }
|
||||
label {
|
||||
display: block; font-size: 0.85rem; color: var(--date-color);
|
||||
margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
}
|
||||
input, select {
|
||||
width: 100%; padding: 0.75rem 1rem; background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color); border-radius: 4px;
|
||||
color: var(--text-color); font-family: monospace; font-size: 0.9rem;
|
||||
}
|
||||
input:focus, select:focus {
|
||||
outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(0,86,179,0.1);
|
||||
}
|
||||
.btn-primary {
|
||||
padding: 0.75rem 2rem; background: var(--accent-color); border: none; border-radius: 4px;
|
||||
color: white; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s;
|
||||
}
|
||||
label { display: block; font-size: 0.85rem; color: var(--date-color); margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
input, select { width: 100%; padding: 0.75rem 1rem; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-color); font-family: monospace; font-size: 0.9rem; }
|
||||
input:focus, select:focus { outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 3px rgba(0,86,179,0.1); }
|
||||
.btn-primary { padding: 0.75rem 2rem; background: var(--accent-color); border: none; border-radius: 4px; color: white; font-weight: 600; font-size: 1rem; cursor: pointer; transition: background 0.2s; }
|
||||
.btn-primary:hover { background: var(--hover-color); }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.output {
|
||||
background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px;
|
||||
padding: 1rem; font-family: monospace; font-size: 0.85rem; word-break: break-all;
|
||||
line-height: 1.6; min-height: 60px; max-height: 120px; overflow-y: auto; color: var(--accent-color);
|
||||
}
|
||||
.output { background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 1rem; font-family: monospace; font-size: 0.85rem; word-break: break-all; line-height: 1.6; min-height: 60px; max-height: 120px; overflow-y: auto; color: var(--accent-color); }
|
||||
.output.error { color: #dc3545; }
|
||||
.output.copyable { cursor: pointer; position: relative; }
|
||||
.output.copyable:hover { border-color: var(--accent-color); }
|
||||
.copy-toast {
|
||||
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%);
|
||||
background: var(--accent-color); color: #fff; padding: 0.5rem 1.25rem;
|
||||
border-radius: 6px; font-size: 0.85rem; font-weight: 500;
|
||||
opacity: 0; transition: opacity 0.25s; pointer-events: none; z-index: 9999;
|
||||
}
|
||||
.copy-toast { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); background: var(--accent-color); color: #fff; padding: 0.5rem 1.25rem; border-radius: 6px; font-size: 0.85rem; font-weight: 500; opacity: 0; transition: opacity 0.25s; pointer-events: none; z-index: 9999; }
|
||||
.copy-toast.show { opacity: 1; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||
.loading { animation: pulse 1s infinite; }
|
||||
.eightball-answer {
|
||||
font-size: 1.3rem; font-weight: 600; text-align: center; padding: 1.5rem;
|
||||
border-radius: 50%; width: 180px; height: 180px; margin: 1rem auto;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: radial-gradient(circle at 40% 40%, #333, #111);
|
||||
color: #fff; font-family: serif; line-height: 1.3;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3); transition: transform 0.3s;
|
||||
}
|
||||
.eightball-answer { font-size: 1.3rem; font-weight: 600; text-align: center; padding: 1.5rem; border-radius: 50%; width: 180px; height: 180px; margin: 1rem auto; display: flex; align-items: center; justify-content: center; background: radial-gradient(circle at 40% 40%, #333, #111); color: #fff; font-family: serif; line-height: 1.3; box-shadow: 0 4px 20px rgba(0,0,0,0.3); transition: transform 0.3s; }
|
||||
.eightball-answer:hover { transform: scale(1.05); }
|
||||
.eightball-answer.positive { color: #4cff72; }
|
||||
.eightball-answer.neutral { color: #ffd644; }
|
||||
.eightball-answer.negative { color: #ff6b6b; }
|
||||
@keyframes shake8ball {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
10% { transform: rotate(-8deg); } 20% { transform: rotate(8deg); }
|
||||
30% { transform: rotate(-6deg); } 40% { transform: rotate(6deg); }
|
||||
50% { transform: rotate(-3deg); } 60% { transform: rotate(3deg); }
|
||||
70% { transform: rotate(-1deg); } 80% { transform: rotate(1deg); }
|
||||
}
|
||||
@keyframes shake8ball { 0%, 100% { transform: rotate(0deg); } 10% { transform: rotate(-8deg); } 20% { transform: rotate(8deg); } 30% { transform: rotate(-6deg); } 40% { transform: rotate(6deg); } 50% { transform: rotate(-3deg); } 60% { transform: rotate(3deg); } 70% { transform: rotate(-1deg); } 80% { transform: rotate(1deg); } }
|
||||
.eightball-shaking { animation: shake8ball 0.6s ease-in-out; }
|
||||
@media (max-width: 600px) {
|
||||
body { padding: 10px; } h1 { font-size: 1.5em; }
|
||||
.controls { flex-direction: column; }
|
||||
}
|
||||
.footer-cta { text-align: center; padding: 1.5rem; margin: 1rem 0; background: var(--bg-secondary); border-radius: 8px; position: relative; overflow: hidden; }
|
||||
.footer-cta::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 8px; padding: 2px; background: linear-gradient(90deg, rgba(0,123,255,0.4), rgba(138,43,226,0.5), rgba(255,0,255,0.5), rgba(255,20,147,0.5), rgba(0,255,255,0.5), rgba(0,200,255,0.5), rgba(0,123,255,0.4)); background-size: 300% 100%; -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude; animation: shimmer 4.5s linear infinite; pointer-events: none; }
|
||||
@keyframes shimmer { 0% { background-position: 0% 0%; } 100% { background-position: 300% 0%; } }
|
||||
.footer-cta > * { position: relative; z-index: 1; }
|
||||
.footer-cta p { margin: 0; font-size: 1.1rem; }
|
||||
.cta-link-btn { background: none; border: none; color: var(--accent-color); font-size: inherit; font-weight: 600; cursor: pointer; text-decoration: underline; padding: 0; font-family: 'Montserrat', sans-serif; }
|
||||
.cta-link-btn:hover { color: var(--hover-color); }
|
||||
.accessibility-notice { font-size: 0.8rem; color: var(--date-color); line-height: 1.5; margin: 1rem 0; padding: 0.75rem 0; }
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 9999; padding: 1rem; }
|
||||
.modal-content { background: var(--bg-primary); border-radius: 12px; padding: 2rem; max-width: 500px; width: 100%; max-height: 90vh; overflow-y: auto; position: relative; box-shadow: 0 4px 20px rgba(0,0,0,0.15); }
|
||||
.modal-close { position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: 1.5rem; cursor: pointer; color: var(--text-color); line-height: 1; padding: 0.25rem 0.5rem; }
|
||||
.modal-close:hover { color: var(--accent-color); }
|
||||
#modal-title { margin-top: 0; margin-bottom: 0.5rem; color: var(--accent-color); }
|
||||
.modal-subtitle { color: var(--date-color); margin-bottom: 1.5rem; }
|
||||
.modal-content .form-group { margin-bottom: 1rem; }
|
||||
.modal-content .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
|
||||
.modal-content .form-group input[type="text"], .modal-content .form-group input[type="email"], .modal-content .form-group input[type="tel"], .modal-content .form-group textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-color); font-size: 1rem; font-family: 'Montserrat', sans-serif; box-sizing: border-box; }
|
||||
.modal-content .form-group input:focus, .modal-content .form-group textarea:focus { outline: none; border-color: var(--accent-color); box-shadow: 0 0 0 2px rgba(0,123,255,0.25); }
|
||||
.modal-content .form-group textarea { resize: vertical; min-height: 80px; }
|
||||
.modal-content .form-hint { font-size: 0.85rem; color: var(--date-color); margin-top: 0.25rem; }
|
||||
.modal-content .checkbox-group label { display: flex; align-items: flex-start; gap: 0.5rem; font-weight: normal; cursor: pointer; }
|
||||
.modal-content .checkbox-group input[type="checkbox"] { margin-top: 0.25rem; width: auto; }
|
||||
.modal-content .form-message { padding: 0.75rem 1rem; border-radius: 4px; margin-bottom: 1rem; font-size: 0.9rem; }
|
||||
.modal-content .form-message.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
|
||||
.modal-content .form-message.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; }
|
||||
.modal-content .form-message.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
|
||||
.modal-content .cta-button { display: block; width: 100%; padding: 0.75rem 1.5rem; background: var(--accent-color); color: white; border: none; border-radius: 4px; font-size: 1rem; font-weight: 500; cursor: pointer; transition: background 0.2s; font-family: 'Montserrat', sans-serif; }
|
||||
.modal-content .cta-button:hover { background: var(--hover-color); }
|
||||
.modal-content .cta-button:disabled { opacity: 0.6; cursor: not-allowed; }
|
||||
@media (max-width: 600px) { body { padding: 10px; } h1 { font-size: 1.75em; } .main-nav ul { flex-direction: column; gap: 0.5rem; } .main-nav a { text-align: center; } .main-nav .dropdown-content { position: static; box-shadow: none; } .controls { flex-direction: column; } }
|
||||
@media (max-width: 480px) { .modal-content { padding: 1.5rem; } }
|
||||
</style>
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="//metrics.nixc.us/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '3']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
!function(t,e){var o,n,p,r;e.__SV||(window.posthog&&window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture identify setPersonProperties group reset get_distinct_id getGroups get_session_id alias set_config opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||
posthog.init('phc_3WDvcJlYYXlBVYL8vC1raT0gMfjkMuCyOpXdmgjK0CK',{api_host:'https://eu.i.posthog.com',person_profiles:'identified_only'})
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-to-content">Skip to content</a>
|
||||
<div class="theme-switch">
|
||||
<button id="themeToggle" type="button" role="switch" aria-label="Theme mode: Auto">🌓</button>
|
||||
</div>
|
||||
|
||||
<a href="/" class="back-link">← Back to Camera TRNG</a>
|
||||
<h1>Quantum Random Tools</h1>
|
||||
<p class="subtitle">Dice, passwords, coin flips, and a Magic 8 Ball — all from quantum camera noise.</p>
|
||||
<nav class="main-nav">
|
||||
<ul>
|
||||
<li><a href="https://colinknapp.com/" id="nav-portfolio">Portfolio</a></li>
|
||||
<li class="dropdown">
|
||||
<a href="https://colinknapp.com/resumes/" id="nav-resumes">Resumes</a>
|
||||
<div class="dropdown-content">
|
||||
<a href="https://colinknapp.com/resumes/business-development.html">Business Development</a>
|
||||
<a href="https://colinknapp.com/resumes/devsecops.html">DevSecOps</a>
|
||||
<a href="https://colinknapp.com/resumes/team-leadership.html">Team Leadership</a>
|
||||
<a href="https://colinknapp.com/resumes/tool-building.html">Tool Building</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="https://colinknapp.com/stories/" id="nav-stories">Stories</a>
|
||||
<div class="dropdown-content">
|
||||
<a href="https://colinknapp.com/stories/app-development.html">App Development</a>
|
||||
<a href="https://colinknapp.com/stories/athion-turnaround.html">Athion Turnaround</a>
|
||||
<a href="https://colinknapp.com/stories/home-infrastructure.html">Home Infrastructure</a>
|
||||
<a href="https://colinknapp.com/stories/motherboard-repair.html">Motherboard Repair</a>
|
||||
<a href="https://colinknapp.com/stories/nuclear-dns.html">Nuclear Dns</a>
|
||||
<a href="https://colinknapp.com/stories/open-source-success.html">Open Source Success</a>
|
||||
<a href="https://colinknapp.com/stories/scansnap-webdav.html">Scansnap Webdav</a>
|
||||
<a href="https://colinknapp.com/stories/showerloop.html">Showerloop</a>
|
||||
<a href="https://colinknapp.com/stories/well-known-mcp.html">.well-known MCP</a>
|
||||
<a href="https://colinknapp.com/stories/web-design-java.html">Web Design Java</a>
|
||||
<a href="https://colinknapp.com/stories/youtube-game-dev.html">Youtube Game Dev</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="https://colinknapp.com/one-pager-tools/csv-tool.html" id="nav-tools">Tools</a>
|
||||
<div class="dropdown-content">
|
||||
<a href="https://colinknapp.com/website-questionnaire/">Website Questionnaire</a>
|
||||
<a href="https://colinknapp.com/rich-snippets/">Rich Snippets Generator</a>
|
||||
<a href="https://colinknapp.com/one-pager-tools/csv-tool.html">CSV Tool</a>
|
||||
<a href="https://colinknapp.com/one-pager-tools/utm-tool.html">UTM Builder</a>
|
||||
<a href="https://md.colinknapp.com" target="_blank" rel="noopener">Markdown Tool</a>
|
||||
<a href="https://nix.colinknapp.com" target="_blank" rel="noopener">NixOS Validator</a>
|
||||
<a href="https://qr.colinknapp.com" target="_blank" rel="noopener">QR Code Tool</a>
|
||||
<a href="https://ai-live.colinknapp.com" target="_blank" rel="noopener">AI Live ISO</a>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="https://meet.colinknapp.com" target="_blank" rel="noopener" title="No-account web meetings">Meet</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Dice -->
|
||||
<div class="card">
|
||||
<h3>Quantum Dice</h3>
|
||||
<div class="controls">
|
||||
<div class="field">
|
||||
<label for="dice-sides">Sides (d)</label>
|
||||
<input type="number" id="dice-sides" value="6" min="2" max="256">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dice-count">Count</label>
|
||||
<input type="number" id="dice-count" value="1" min="1" max="100">
|
||||
</div>
|
||||
<button id="btn-dice" class="btn-primary">Roll</button>
|
||||
</div>
|
||||
<div class="output copyable" id="out-dice" title="Click to copy">Roll some dice...</div>
|
||||
</div>
|
||||
<main id="main-content">
|
||||
<h1>Quantum Random Tools</h1>
|
||||
<p class="subtitle">Dice, passwords, coin flips, and a Magic 8 Ball — all from quantum camera noise. <a href="/" style="font-style:normal;">Back to Camera TRNG →</a></p>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="card">
|
||||
<h3>Quantum Password</h3>
|
||||
<div class="controls">
|
||||
<div class="field">
|
||||
<label for="pw-length">Length</label>
|
||||
<input type="number" id="pw-length" value="16" min="1" max="128">
|
||||
<div class="card">
|
||||
<h3>Quantum Dice</h3>
|
||||
<div class="controls">
|
||||
<div class="field"><label for="dice-sides">Sides (d)</label><input type="number" id="dice-sides" value="6" min="2" max="256"></div>
|
||||
<div class="field"><label for="dice-count">Count</label><input type="number" id="dice-count" value="1" min="1" max="100"></div>
|
||||
<button id="btn-dice" class="btn-primary">Roll</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="pw-charset">Charset</label>
|
||||
<select id="pw-charset">
|
||||
<option value="alphanumeric">Alphanumeric</option>
|
||||
<option value="full">Full (+ symbols)</option>
|
||||
<option value="hex">Hex</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="btn-password" class="btn-primary">Generate</button>
|
||||
<div class="output copyable" id="out-dice" title="Click to copy">Roll some dice...</div>
|
||||
</div>
|
||||
<label for="pw-no-ambiguous" style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;color:var(--date-color);cursor:pointer;margin:-0.5rem 0 0.75rem;">
|
||||
<input type="checkbox" id="pw-no-ambiguous" checked style="width:auto;">
|
||||
Exclude ambiguous <code style="font-size:0.8em;opacity:0.7;">0 O o I l 1 |</code>
|
||||
</label>
|
||||
<div class="output copyable" id="out-password" title="Click to copy">Generate a password...</div>
|
||||
</div>
|
||||
|
||||
<!-- Coin -->
|
||||
<div class="card">
|
||||
<h3>Quantum Coin Flip</h3>
|
||||
<div class="controls">
|
||||
<button id="btn-coin" class="btn-primary">Flip</button>
|
||||
</div>
|
||||
<div class="output copyable" id="out-coin" title="Click to copy">Flip a coin...</div>
|
||||
</div>
|
||||
|
||||
<!-- 8 Ball -->
|
||||
<div class="card">
|
||||
<h3>Quantum 8 Ball</h3>
|
||||
<div class="controls">
|
||||
<div class="field" style="flex:3;">
|
||||
<label for="eightball-question">Ask the Quantum Void</label>
|
||||
<input type="text" id="eightball-question" placeholder="Will I ever understand quantum mechanics?" maxlength="200">
|
||||
<div class="card">
|
||||
<h3>Quantum Password</h3>
|
||||
<div class="controls">
|
||||
<div class="field"><label for="pw-length">Length</label><input type="number" id="pw-length" value="16" min="1" max="128"></div>
|
||||
<div class="field"><label for="pw-charset">Charset</label>
|
||||
<select id="pw-charset"><option value="alphanumeric">Alphanumeric</option><option value="full">Full (+ symbols)</option><option value="hex">Hex</option></select>
|
||||
</div>
|
||||
<button id="btn-password" class="btn-primary">Generate</button>
|
||||
</div>
|
||||
<button id="btn-eightball" class="btn-primary">Shake</button>
|
||||
<label for="pw-no-ambiguous" style="display:flex;align-items:center;gap:0.5rem;font-size:0.85rem;color:var(--date-color);cursor:pointer;margin:-0.5rem 0 0.75rem;">
|
||||
<input type="checkbox" id="pw-no-ambiguous" checked style="width:auto;">
|
||||
Exclude ambiguous <code style="font-size:0.8em;opacity:0.7;">0 O o I l 1 |</code>
|
||||
</label>
|
||||
<div class="output copyable" id="out-password" title="Click to copy">Generate a password...</div>
|
||||
</div>
|
||||
<div class="eightball-answer" id="eightball-orb" style="display:none;"></div>
|
||||
<div class="output copyable" id="out-eightball" title="Click to copy">Shake the 8 ball...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Quantum Coin Flip</h3>
|
||||
<div class="controls"><button id="btn-coin" class="btn-primary">Flip</button></div>
|
||||
<div class="output copyable" id="out-coin" title="Click to copy">Flip a coin...</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Quantum 8 Ball</h3>
|
||||
<div class="controls">
|
||||
<div class="field" style="flex:3;"><label for="eightball-question">Ask the Quantum Void</label><input type="text" id="eightball-question" placeholder="Will I ever understand quantum mechanics?" maxlength="200"></div>
|
||||
<button id="btn-eightball" class="btn-primary">Shake</button>
|
||||
</div>
|
||||
<div class="eightball-answer" id="eightball-orb" style="display:none;"></div>
|
||||
<div class="output copyable" id="out-eightball" title="Click to copy">Shake the 8 ball...</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<hr>
|
||||
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
|
||||
<div id="footer-cta" class="footer-cta">
|
||||
<p>Interested in working together? <button type="button" id="open-contact-modal" class="cta-link-btn">Get in touch</button></p>
|
||||
</div>
|
||||
<div id="contact-modal" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<button type="button" class="modal-close" id="close-contact-modal" aria-label="Close contact form">×</button>
|
||||
<h2 id="modal-title">Let's Work Together</h2>
|
||||
<p class="modal-subtitle">Have a project in mind? Send me a message.</p>
|
||||
<form id="contactForm">
|
||||
<input type="hidden" name="form_id" value="colinknapp_contact">
|
||||
<div class="form-group"><label for="contact-name">Name</label><input type="text" id="contact-name" name="name" required maxlength="100" placeholder="Your name"></div>
|
||||
<div class="form-group"><label for="contact-email">Email</label><input type="email" id="contact-email" name="email" maxlength="254" placeholder="your@email.com"></div>
|
||||
<div class="form-group"><label for="contact-phone">Phone</label><input type="tel" id="contact-phone" name="phone" required inputmode="tel" placeholder="(555) 555-5555" maxlength="20"><p class="form-hint">Required so I can reach you</p></div>
|
||||
<div class="form-group"><label for="contact-message">Message</label><textarea id="contact-message" name="message" rows="4" required maxlength="2000" placeholder="Tell me about your project..."></textarea></div>
|
||||
<div class="form-group" style="display:none;"><label for="website">Website</label><input type="text" id="website" name="website" autocomplete="off"></div>
|
||||
<div class="form-group checkbox-group"><label><input type="checkbox" id="contact-accept" name="accept_terms" required> I agree to the <a href="https://motherboardrepair.ca/privacy.html" target="_blank" rel="noopener noreferrer">Privacy Policy</a></label></div>
|
||||
<div id="contact-form-message" class="form-message" style="display:none;"></div>
|
||||
<button type="submit" class="cta-button" id="contact-submit-btn">Send Message</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="copy-toast" id="copy-toast">Copied!</div>
|
||||
|
||||
<script>
|
||||
// Theme toggle (shared with main page via localStorage)
|
||||
// Theme toggle
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const html = document.documentElement;
|
||||
const saved = localStorage.getItem('theme') || 'auto';
|
||||
|
|
@ -241,14 +270,10 @@
|
|||
const outCoin = document.getElementById('out-coin');
|
||||
const outBall = document.getElementById('out-eightball');
|
||||
const orb = document.getElementById('eightball-orb');
|
||||
|
||||
// Wire click-to-copy on every output box
|
||||
[outDice, outPw, outCoin, outBall].forEach(el => el.addEventListener('click', () => copyText(el)));
|
||||
|
||||
const allBtns = ['btn-dice','btn-password','btn-coin','btn-eightball'].map(id => document.getElementById(id));
|
||||
function disableAll(v) { allBtns.forEach(b => b.disabled = v); }
|
||||
|
||||
// Dice
|
||||
document.getElementById('btn-dice').addEventListener('click', async () => {
|
||||
disableAll(true); outDice.className = 'output copyable loading'; outDice.textContent = 'Rolling...';
|
||||
try {
|
||||
|
|
@ -263,7 +288,6 @@
|
|||
disableAll(false);
|
||||
});
|
||||
|
||||
// Password
|
||||
document.getElementById('btn-password').addEventListener('click', async () => {
|
||||
disableAll(true); outPw.className = 'output copyable loading'; outPw.textContent = 'Generating...';
|
||||
try {
|
||||
|
|
@ -277,14 +301,12 @@
|
|||
const j = await r.json();
|
||||
outPw.className = 'output copyable';
|
||||
outPw.style.fontFamily = "'SF Mono', Monaco, 'Cascadia Code', 'Fira Code', monospace";
|
||||
outPw.style.fontSize = '1.1rem';
|
||||
outPw.style.letterSpacing = '0.08em';
|
||||
outPw.style.fontSize = '1.1rem'; outPw.style.letterSpacing = '0.08em';
|
||||
outPw.textContent = j.password;
|
||||
} catch (e) { outPw.className = 'output copyable error'; outPw.textContent = 'Error: ' + e.message; }
|
||||
disableAll(false);
|
||||
});
|
||||
|
||||
// Coin
|
||||
document.getElementById('btn-coin').addEventListener('click', async () => {
|
||||
disableAll(true); outCoin.className = 'output copyable loading'; outCoin.textContent = 'Flipping...';
|
||||
try {
|
||||
|
|
@ -297,11 +319,9 @@
|
|||
disableAll(false);
|
||||
});
|
||||
|
||||
// 8 Ball
|
||||
document.getElementById('btn-eightball').addEventListener('click', async () => {
|
||||
disableAll(true); outBall.className = 'output copyable loading'; outBall.textContent = '';
|
||||
orb.style.display = 'flex';
|
||||
orb.className = 'eightball-answer eightball-shaking'; orb.textContent = '...';
|
||||
orb.style.display = 'flex'; orb.className = 'eightball-answer eightball-shaking'; orb.textContent = '...';
|
||||
try {
|
||||
const q = document.getElementById('eightball-question').value;
|
||||
let url = origin + '/8ball';
|
||||
|
|
@ -309,16 +329,58 @@
|
|||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
const j = await r.json();
|
||||
orb.className = 'eightball-answer ' + j.sentiment;
|
||||
orb.textContent = j.answer;
|
||||
orb.className = 'eightball-answer ' + j.sentiment; orb.textContent = j.answer;
|
||||
outBall.className = 'output copyable';
|
||||
outBall.textContent = (q ? 'Q: ' + q + ' \u2192 ' : '') + j.answer;
|
||||
} catch (e) {
|
||||
orb.style.display = 'none';
|
||||
outBall.className = 'output copyable error'; outBall.textContent = 'Error: ' + e.message;
|
||||
}
|
||||
} catch (e) { orb.style.display = 'none'; outBall.className = 'output copyable error'; outBall.textContent = 'Error: ' + e.message; }
|
||||
disableAll(false);
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
var modal = document.getElementById('contact-modal');
|
||||
var openBtn = document.getElementById('open-contact-modal');
|
||||
var closeBtn = document.getElementById('close-contact-modal');
|
||||
var form = document.getElementById('contactForm');
|
||||
var submitBtn = document.getElementById('contact-submit-btn');
|
||||
var formMessage = document.getElementById('contact-form-message');
|
||||
var phoneInput = document.getElementById('contact-phone');
|
||||
if (!modal || !form) return;
|
||||
function openModal() { modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; var fi = modal.querySelector('input, button'); if (fi) fi.focus(); }
|
||||
function closeModal() { modal.style.display = 'none'; document.body.style.overflow = ''; }
|
||||
if (openBtn) openBtn.addEventListener('click', openModal);
|
||||
if (closeBtn) closeBtn.addEventListener('click', closeModal);
|
||||
modal.addEventListener('click', function(e) { if (e.target === modal) closeModal(); });
|
||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && modal.style.display === 'flex') closeModal(); });
|
||||
if (phoneInput) {
|
||||
phoneInput.addEventListener('input', function(e) {
|
||||
var v = e.target.value.replace(/\D/g, '');
|
||||
if (v.length > 10) v = v.slice(0, 10);
|
||||
if (v.length >= 6) v = '(' + v.slice(0,3) + ') ' + v.slice(3,6) + '-' + v.slice(6);
|
||||
else if (v.length >= 3) v = '(' + v.slice(0,3) + ') ' + v.slice(3);
|
||||
else if (v.length > 0) v = '(' + v;
|
||||
e.target.value = v;
|
||||
});
|
||||
}
|
||||
function showMessage(type, msg) { formMessage.textContent = msg; formMessage.className = 'form-message ' + type; formMessage.style.display = 'block'; if (type === 'success') setTimeout(function() { formMessage.style.display = 'none'; closeModal(); }, 3000); }
|
||||
function sanitizeText(s) { return typeof s !== 'string' ? '' : s.replace(/[\u0000-\u001F\u007F]/g, '').trim().replace(/\s+/g, ' '); }
|
||||
function sanitizeMultiline(s) { return typeof s !== 'string' ? '' : s.replace(/[\u0000-\u0009\u000B-\u001F\u007F]/g, '').replace(/[<>]/g, ''); }
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault(); submitBtn.disabled = true; submitBtn.textContent = 'Sending...';
|
||||
var fd = new FormData(form);
|
||||
if (fd.get('website')) { submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; showMessage('success', 'Thank you! Your message has been received.'); return; }
|
||||
var data = { form_id: 'colinknapp_contact', name: sanitizeText(fd.get('name')).slice(0,100), email: (fd.get('email')||'').trim().slice(0,254), phone: (fd.get('phone')||'').replace(/\D/g,'').slice(0,10), message: sanitizeMultiline(fd.get('message')).slice(0,2000) };
|
||||
if (!data.name) { showMessage('error', 'Please enter your name.'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; return; }
|
||||
if (!data.phone || data.phone.length < 10) { showMessage('error', 'Please enter a valid phone number.'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; return; }
|
||||
if (!data.message) { showMessage('error', 'Please enter a message.'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; return; }
|
||||
showMessage('info', 'Sending your message...');
|
||||
fetch('https://forms.motherboardrepair.ca/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
|
||||
.then(function(r) { if (!r.ok) throw new Error('Server error: ' + r.status); return r.json(); })
|
||||
.then(function() { showMessage('success', "Thank you! I'll get back to you soon."); form.reset(); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; })
|
||||
.catch(function(err) { console.error('Form error:', err); showMessage('error', 'Sorry, there was an issue. Please try again.'); submitBtn.disabled = false; submitBtn.textContent = 'Send Message'; });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue