diff --git a/script.js b/script.js
index 912aae9..9d61442 100644
--- a/script.js
+++ b/script.js
@@ -323,11 +323,11 @@ function initTerminalTyping() {
setInterval(typeCommand, 8000);
}
-// Initialize terminal typing when contact section is visible
+// Initialize terminal typing when contact section is visible - limited to 3 cycles
const terminalObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
- initTerminalTyping();
+ initLimitedTerminalTyping();
terminalObserver.unobserve(entry.target);
}
});
@@ -338,6 +338,97 @@ if (contactSection) {
terminalObserver.observe(contactSection);
}
+/**
+ * Limited Terminal Typing Effect (3 cycles max)
+ */
+function initLimitedTerminalTyping() {
+ const terminal = document.querySelector('.terminal-body');
+ if (!terminal) return;
+
+ const commands = [
+ { text: 'docker ps', output: 'CONTAINER ID IMAGE STATUS\nabc123 nginx Up 3 days\ndef456 nextcloud Up 5 days' },
+ { text: 'uptime', output: 'Load: 1.06, 1.06, 1.14 | RAM: 18% of 96GB' },
+ { text: 'tailscale status', output: '100.x.x.x compute-01 linux -' }
+ ];
+
+ let currentCommand = 0;
+ let cycleCount = 0;
+ const maxCycles = 3;
+ let isTyping = false;
+
+ const typeCommand = () => {
+ if (isTyping || cycleCount >= maxCycles) return;
+ isTyping = true;
+
+ const cmd = commands[currentCommand];
+ const lines = terminal.querySelectorAll('.terminal-line');
+
+ // Remove cursor from previous line
+ const cursor = terminal.querySelector('.cursor');
+ if (cursor) cursor.remove();
+
+ // Create new command line
+ const newLine = document.createElement('div');
+ newLine.className = 'terminal-line';
+ newLine.innerHTML = `
+ ➜
+ ~
+
+ |
+ `;
+ terminal.appendChild(newLine);
+
+ const commandSpan = newLine.querySelector('.command');
+ let charIndex = 0;
+
+ const typeChar = () => {
+ if (charIndex < cmd.text.length) {
+ commandSpan.textContent += cmd.text[charIndex];
+ charIndex++;
+ setTimeout(typeChar, 50 + Math.random() * 50);
+ } else {
+ setTimeout(() => {
+ const outputDiv = document.createElement('div');
+ outputDiv.className = 'terminal-output';
+ outputDiv.innerHTML = cmd.output.replace(/\n/g, '
');
+ terminal.appendChild(outputDiv);
+
+ const nextLine = document.createElement('div');
+ nextLine.className = 'terminal-line';
+ nextLine.innerHTML = `
+ ➜
+ ~
+ |
+ `;
+ terminal.appendChild(nextLine);
+
+ terminal.scrollTop = terminal.scrollHeight;
+
+ isTyping = false;
+ currentCommand = (currentCommand + 1) % commands.length;
+
+ // Count completed cycles
+ if (currentCommand === 0) {
+ cycleCount++;
+ }
+ }, 500);
+ }
+ };
+
+ typeChar();
+ };
+
+ // Start typing effect every 6 seconds, but only for 3 cycles
+ const intervalId = setInterval(() => {
+ if (cycleCount >= maxCycles) {
+ clearInterval(intervalId);
+ // Keep cursor blinking at the end
+ return;
+ }
+ typeCommand();
+ }, 6000);
+}
+
/**
* Network connection lines animation
*/
diff --git a/styles.css b/styles.css
index 0cc2370..a89daa1 100644
--- a/styles.css
+++ b/styles.css
@@ -1518,6 +1518,17 @@ body {
color: var(--color-text);
}
+/* ========================================
+ Service Modals - Hide by default
+ ======================================== */
+.service-modal {
+ display: none !important;
+}
+
+.service-modal.active {
+ display: flex !important;
+}
+
/* ========================================
Service Status Checking
======================================== */
@@ -1698,3 +1709,73 @@ body {
padding: 1.5rem;
}
}
+
+
+/* ========================================
+ Services Categories (Public/Private)
+ ======================================== */
+.services-category {
+ margin-bottom: 3rem;
+}
+
+.category-title {
+ font-family: 'Space Grotesk', sans-serif;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: var(--color-text);
+ margin-bottom: 1.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.category-icon {
+ font-size: 1.5rem;
+}
+
+.category-badge {
+ margin-left: auto;
+ padding: 0.35rem 0.75rem;
+ background: rgba(34, 197, 94, 0.1);
+ border: 1px solid rgba(34, 197, 94, 0.3);
+ border-radius: 100px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #22c55e;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.services-category:last-child .category-badge {
+ background: rgba(239, 68, 68, 0.1);
+ border-color: rgba(239, 68, 68, 0.3);
+ color: #ef4444;
+}
+
+/* Public Service Styling */
+.service-card.public {
+ border-color: rgba(34, 197, 94, 0.2);
+}
+
+.service-card.public:hover {
+ border-color: rgba(34, 197, 94, 0.5);
+ box-shadow: 0 8px 32px rgba(34, 197, 94, 0.1);
+}
+
+/* Private Service Styling */
+.service-card.private-service {
+ border-color: rgba(239, 68, 68, 0.2);
+ cursor: pointer;
+}
+
+.service-card.private-service:hover {
+ border-color: rgba(239, 68, 68, 0.5);
+ box-shadow: 0 8px 32px rgba(239, 68, 68, 0.1);
+}
+
+.service-card.private-service .service-arrow svg {
+ width: 20px;
+ height: 20px;
+}