Build a 3D Cybersecurity Portfolio

Build and publish a responsive 3D cybersecurity portfolio with GitHub Pages.

Introduction

30 Second Summary

Recruiters often meet your work in a browser before they meet you. A flat or crowded page can hide strong projects within seconds.

In this project, you will build a premium portfolio for Tarun CL with Semantic HTML, responsive CSS, JavaScript, and an autonomous Web Component. You will publish it with GitHub Pages so recruiters can open the finished site from a public URL.

What You'll Build

Open the finished portfolio to see Tarun's profile inside a warm-white editorial layout with pointer-reactive 3D cards, direct project links, and a certificate lightbox.

By the end of this project, you'll have:

  • Present Tarun's background through a responsive portfolio that stays readable on desktop and mobile screens.
  • Showcase AURUM, FinEase, and VITTrace through interactive 3D cards with direct links to their GitHub repositories.
  • Display the seven supplied certification names in a gallery where each placeholder opens inside a keyboard-friendly lightbox.
  • Secret Mission: audit the live site using only the keyboard, Windows reduced-motion settings, and a narrow mobile viewport.

Are there any prerequisites?

You need a Windows computer. You also need an existing GitHub account. The guide covers Visual Studio Code and GitHub Desktop setup, so no previous front-end experience is required.

Before We Start

Before any hands-on work begins, define the portfolio you are committing to build. This keeps every section focused on Tarun CL's supplied information while giving recruiters a clear view of his work.

Set Up the Windows Workspace

A recruiter-facing portfolio needs a dependable workspace before the first page takes shape. A visual Git workflow keeps setup friction away from the design work ahead.

You will prepare Visual Studio Code for editing the site. You will use GitHub Desktop to create the repository. Your first local preview will open in Microsoft Edge.

In this step, get ready to:
  • Install Visual Studio Code and GitHub Desktop.
  • Create the local repository for the portfolio.
  • Build the starter files and preview the heading in Microsoft Edge.
Install the workspace tools

Visual Studio Code gives you one place to manage the portfolio files. GitHub Desktop provides visual controls for the repository workflow.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.

✔️ Visual Studio Code opens

  • Select Visual Studio Code from the search results.

Visual Studio Code is ready for the repository you create next.

ⓧ Visual Studio Code is not installed

The User Setup installs Visual Studio Code without requiring administrator permissions.

  • Open the official Visual Studio Code Windows setup guide.
  • Download the Visual Studio Code User Setup for Windows.
  • Run the downloaded installer.
  • Accept the default installer options.
  • Complete the installation.
  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Select Visual Studio Code from the search results.

You should see the Visual Studio Code editor window.

Visual Studio Code Still Missing?

Close the installer before searching again. Restart Windows if the application still does not appear.

Ask for help with the exact stage that failed: help me troubleshoot my Visual Studio Code User Setup on Windows.

  • Press the Windows key to open Windows search.
  • Type GitHub Desktop into the search field.

✔️ GitHub Desktop opens

  • Select GitHub Desktop from the search results.

GitHub Desktop is ready to connect your existing GitHub account.

ⓧ GitHub Desktop is not installed

GitHub Desktop supports the visual repository workflow used throughout this project.

  • Open the official GitHub Desktop installation guide.
  • Select Download for Windows.
  • Run the downloaded installer.
  • Wait for GitHub Desktop to launch after the installation.

You should see the GitHub Desktop welcome or repository screen.

GitHub Desktop Does Not Open?

Search for GitHub Desktop again after the installer finishes. Restart Windows if the installation completed but the application remains unavailable.

Ask for help with the installer symptoms you can see: help me troubleshoot GitHub Desktop installation on Windows.

Authentication happens through GitHub in your browser. This keeps your account credentials inside the existing GitHub sign-in flow.

  • Click File in the GitHub Desktop menu bar.
  • Click Options.
  • Select the Accounts pane.
  • Click Sign Into for GitHub.
  • Click Continue With Browser.
  • Complete the GitHub sign-in in your browser.
  • Follow the browser prompt that returns you to GitHub Desktop.

That setup hurdle is cleared. GitHub Desktop now shows your account identity and can create repositories for it.

Sign-In Does Not Complete?

Repeat the browser sign-in from the Accounts pane. Complete any account verification requested by GitHub before returning to GitHub Desktop.

Ask for help without sharing your password or verification code: help me complete GitHub Desktop browser authentication.

Create the local repository

A local Git repository records the portfolio files as they change. Its name also matches the repository GitHub Pages will publish later.

  • Return to GitHub Desktop.
  • Click Create a New Repository on your Hard Drive....
  • Enter taruncl-cloud.github.io as the repository name.
  • Keep the proposed local path so GitHub Desktop can manage the folder.
  • Click Create repository.

You should see taruncl-cloud.github.io as the current repository in GitHub Desktop.

  • Click Open in Visual Studio Code in the repository view.
  • Confirm that you trust the folder if Visual Studio Code asks about workspace trust.

You should see the taruncl-cloud.github.io folder in the Visual Studio Code Explorer.

Why Create the Repository First?

GitHub Desktop creates the local Git history before you begin editing. Every portfolio file you add inside this folder can become part of the same tracked project.

Repository Not Open in Visual Studio Code?

Make sure taruncl-cloud.github.io is the current repository in GitHub Desktop. Select Repository from the menu bar if you need to use the editor-opening option from there.

Ask for help connecting the two applications: help me open my GitHub Desktop repository in Visual Studio Code.

Create and preview the starter files

The starter uses three files with separate responsibilities. HTML provides the page structure. The stylesheet and JavaScript files are ready for the visual system and interactions you add later.

  • Click the new-file icon in the Visual Studio Code Explorer.
  • Enter index.html as the file name.
  • Click the new-file icon in the Explorer.
  • Enter styles.css as the file name.
  • Click the new-file icon in the Explorer.
  • Enter app.js as the file name.

You should see all three starter files beneath taruncl-cloud.github.io in the Explorer.

  • Select index.html in the Explorer.
  • Add the starter document by copying the code below into index.html:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Tarun CL cybersecurity and software development portfolio">
  <title>Tarun CL | Cybersecurity Portfolio</title>
  <link rel="stylesheet" href="styles.css">
  <script src="app.js" defer></script>
</head>
<body>
  <h1>Tarun <span>CL</span></h1>
</body>
</html>

What Does This Starter Document Do?

  • The document metadata gives the browser a page title and a mobile-friendly viewport.
  • The stylesheet link prepares the page to load its visual rules from styles.css.
  • The deferred script link prepares the page to load interactions from app.js after the document is parsed.
  • The heading gives you an immediate local result before the complete portfolio content is rendered.
  • Save index.html by pressing Ctrl+S.

Before you open the page, do you expect the browser to show the portfolio heading or an empty document?

  • Right-click the taruncl-cloud.github.io folder in the Visual Studio Code Explorer.
  • Select Reveal in File Explorer.
  • Double-click index.html in File Explorer.
  • Select Microsoft Edge if Windows asks which application should open the file.

You should see a local starter page headed Tarun CL. The Microsoft Edge tab should read Tarun CL | Cybersecurity Portfolio.

Starter Heading Not Visible?

Confirm that you saved index.html inside the taruncl-cloud.github.io folder. Check that Windows did not add a hidden .txt extension to the file.

Ask for help using the files you can see: help me debug why my local index.html page does not show the Tarun CL heading.

✔️ Awesome, I've got everything!

Great. Keep styles.css and app.js empty for now. Make sure index.html is saved.

ⓧ I'd like to double check the full code

Your styles.css and app.js files should be empty starter files. Compare index.html with this complete starter document.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Tarun CL cybersecurity and software development portfolio">
  <title>Tarun CL | Cybersecurity Portfolio</title>
  <link rel="stylesheet" href="styles.css">
  <script src="app.js" defer></script>
</head>
<body>
  <h1>Tarun <span>CL</span></h1>
</body>
</html>

Your Windows workspace now turns a local file into a visible browser page. Next, you will replace the starter heading with the complete portfolio content rendered through a custom element.

Render the Portfolio Content

Your starter page proves that the browser can load the project files. A single heading cannot yet show recruiters Tarun's background, projects, credentials, or professional links.

This step turns that starter page into a complete content-first portfolio using a Web Component. The deliberately flat result makes every supplied detail visible before the next step adds hierarchy, responsive design, and depth.

In this step, get ready to:
  • Connect the page to an autonomous custom element.
  • Render Tarun's profile, education, skills, projects, and professional links.
  • Add seven certification names with matching gallery placeholders.
Connect the portfolio custom element

An autonomous custom element is a reusable HTML element backed by a JavaScript class. The browser connects <portfolio-app> to PortfolioApp after the class is registered.

  • Switch back to the index.html tab in Visual Studio Code.
  • Replace the starter document with this HTML:
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Tarun CL cybersecurity and software development portfolio">
  <title>Tarun CL | Cybersecurity Portfolio</title>
  <link rel="stylesheet" href="styles.css">
  <script src="app.js" defer></script>
</head>
<body>
  <portfolio-app></portfolio-app>
</body>
</html>

What does this HTML do?

  • The metadata gives the page a responsive viewport, a description, and a browser-tab title.
  • The stylesheet link loads the presentation rules from styles.css.
  • The defer attribute waits for the HTML to be parsed before app.js runs.
  • The <portfolio-app> element becomes the host for all rendered portfolio content.
  • Save index.html.
  • Refresh the existing local preview in Microsoft Edge.

You'll see the starter heading disappear. The empty host element is now waiting for its JavaScript class.

Still seeing the starter heading?

Confirm that you replaced the body content with <portfolio-app></portfolio-app>. Save the file before refreshing Edge.

If the old page remains, close that browser tab and double-click the current index.html file in File Explorer.

Ask for help checking why the updated HTML is not loading.

The component needs structured data before it can produce cards and lists. Keeping that data in arrays gives each project and certification one source of truth.

  • Switch back to the existing app.js tab.
  • Replace the starter text with the certification data below:
const certifications = [
  { name: "Agentic AI Foundations Associate \u2014 Oracle", image: "" },
  { name: "Introduction to Cybersecurity", image: "" },
  { name: "Google TechSpirit", image: "" },
  { name: "Cybersecurity Certification", image: "" },
  { name: "CEHv13 ISO", image: "" },
  { name: "Cisco Packet Tracer", image: "" },
  { name: "CompTIA Security+", image: "" }
];

How is the certification data structured?

Each object stores the supplied certification name and an empty image value. The empty value lets the gallery show a replaceable placeholder without inventing certificate files.

The labels stay exactly as supplied. No dates, scores, grades, or certificate IDs are added.

  • Add the project data directly below the certification array:
const projects = [
  {
    number: "01",
    name: "AURUM",
    description: "A single-page AI tools platform prototype with chatbot, document generation, and workflow interfaces.",
    stack: ["HTML", "CSS", "JavaScript"],
    url: "https://github.com/taruncl-cloud/aurum-ai"
  },
  {
    number: "02",
    name: "FinEase",
    description: "A responsive landing-page concept for small-business finance tracking, reporting, and tax workflows.",
    stack: ["HTML", "CSS", "JavaScript"],
    url: "https://github.com/taruncl-cloud/finease-website"
  },
  {
    number: "03",
    name: "VITTrace",
    description: "A VIT-AP lost-and-found Android app with authentication, item reporting, search, image upload, and status tracking.",
    stack: ["Kotlin", "Jetpack Compose", "Firebase"],
    url: "https://github.com/taruncl-cloud/VITTrace"
  }
];

Why use project objects?

Each object keeps a project's name, supported description, technology list, and repository URL together. The component can transform the array into consistent cards without copying the card markup three times.

The first half of the class establishes its state and turns each project object into a card. This code continues in the next block, so keep both blocks together before saving.

  • Add the class declaration directly below the project array:
class PortfolioApp extends HTMLElement {
  constructor() {
    super();
    this.activeCertificateIndex = 0;
    this.lastGalleryTrigger = null;
  }

  connectedCallback() {
    this.render();
  }

  renderProject(project) {
    const stack = project.stack.map((item) => `<span>${item}</span>`).join("");

    return `
      <article class="project-card tilt-card" data-tilt>
        <div class="project-number">${project.number}</div>
        <h3>${project.name}</h3>
        <p>${project.description}</p>
        <div class="stack">${stack}</div>
        <a class="project-link" href="${project.url}" target="_blank" rel="noreferrer">View repository</a>
      </article>
    `;
  }

What does the class establish?

  • The constructor stores the active certificate position and the gallery control that most recently received focus.
  • The browser calls connectedCallback() when <portfolio-app> joins the document.
  • The renderProject() method converts one project object into semantic article markup.

The second half adds the gallery-card template and the initial rendering method. The minimal <main> element keeps the class valid while you add page sections in small, visible slices.

  • Continue the class directly below renderProject() with this code:
  renderGalleryCard(certificate, index) {
    const shortMark = String(index + 1).padStart(2, "0");

    return `
      <button class="gallery-card tilt-card" type="button" data-certificate-index="${index}" data-tilt aria-label="Open ${certificate.name} certificate placeholder">
        <span class="certificate-art" aria-hidden="true">
          <span class="placeholder-mark">${shortMark}</span>
        </span>
        <span class="gallery-caption">
          <strong>${certificate.name}</strong>
          <span>Image placeholder</span>
        </span>
      </button>
    `;
  }

  render() {
    const projectCards = projects.map((project) => this.renderProject(project)).join("");
    const certificationNames = certifications.map((certificate) => `<li>${certificate.name}</li>`).join("");
    const galleryCards = certifications.map((certificate, index) => this.renderGalleryCard(certificate, index)).join("");

    this.innerHTML = `
      <main>
      </main>
    `;
  }
}

customElements.define("portfolio-app", PortfolioApp);

How does rendering work?

The three mapped values turn the data arrays into project cards, certification list items, and gallery buttons. Joining each result produces one HTML string without unwanted commas.

The final line registers portfolio-app with the browser. The custom element can now render whenever it appears in the page.

  • Save app.js.
  • Refresh the local page in Edge.

You'll see a blank page without the old heading. This confirms the registered component has replaced the host's contents with its empty main area.

Seeing an empty page with a console error?

Check that both arrays end with ];. Confirm that the class ends before customElements.define("portfolio-app", PortfolioApp);.

Check every template string for matching backticks. A missing backtick prevents the component from registering.

Ask for help finding the JavaScript syntax error.

Render Tarun's profile and project content

The render template now has a stable JavaScript foundation. You can add each section inside its open template literal and refresh after every content slice.

  • In app.js, find the opening <main> line inside render().
  • Insert the header directly above that <main> line:
      <header class="site-header">
        <a class="brand" href="#home" aria-label="Tarun CL home">
          <span class="brand-mark">TC</span>
          <span>Tarun CL</span>
        </a>
        <nav class="nav-links" aria-label="Primary navigation">
          <a href="#about">About</a>
          <a href="#experience">Experience</a>
          <a href="#projects">Projects</a>
          <a href="#certifications">Certifications</a>
          <a href="#gallery">Gallery</a>
          <a href="#contact">Contact</a>
        </nav>
      </header>

Why start with semantic navigation?

The header and nav elements give the page a clear structure. Each navigation link targets a section ID that the remaining fragments add.

  • Save app.js.
  • Refresh Edge to reveal the Tarun CL brand and six navigation links.

The component is rendering visible content now. Your JavaScript class is successfully controlling the page.

  • Find the empty space between <main> and </main>.
  • Insert the hero section into that space:
        <section class="page-section hero" id="home">
          <div>
            <p class="eyebrow">Cybersecurity, web, and Android development</p>
            <h1>Tarun <span>CL</span></h1>
            <p class="hero-copy">Final-year CSE (Cyber Security) student at VIT-AP, building practical web and mobile projects with a security-minded approach.</p>
            <div class="action-row">
              <a class="button primary" href="#projects">Explore projects</a>
              <a class="button" href="https://drive.google.com/file/d/1bT0JmgT3nRx-suxcMZvDJJkpOaui-ahx/view?usp=drivesdk" target="_blank" rel="noreferrer">Open resume</a>
            </div>
          </div>

          <div class="hero-visual" aria-label="Animated Tarun CL monogram">
            <div class="identity-card">
              <div class="identity-grid"></div>
              <div class="orbit"></div>
              <div class="orbit secondary"></div>
              <div class="monogram">TC</div>
              <div class="identity-caption">
                <span>Bengaluru, India</span>
                <span>Security and software</span>
              </div>
            </div>
          </div>
        </section>

What does the hero establish?

The hero immediately identifies Tarun as a final-year CSE student specializing in Cyber Security at VIT-AP. It also gives visitors direct routes to the featured projects and supplied resume.

The nested identity elements create layers for the visual treatment in the next step. Their content remains readable before any motion is added.

  • Save app.js.
  • Refresh Edge to display Tarun's name, profile summary, project link, resume link, and monogram content.
  • Insert the portfolio metrics directly below the closing hero </section> tag:
        <div class="section-shell metric-rail" aria-label="Portfolio metrics">
          <div class="metric"><strong>10</strong><span>Public repositories</span></div>
          <div class="metric"><strong>7</strong><span>Certifications</span></div>
          <div class="metric"><strong>3</strong><span>Featured projects</span></div>
        </div>

What do the metrics communicate?

The rail gives visitors three quick portfolio signals. Each number is tied to supplied or verified project information.

  • Save app.js.
  • Refresh Edge to reveal the three metrics below the hero.
  • Insert the profile section directly below the metrics </div> tag:
        <section class="page-section" id="about">
          <div class="section-shell">
            <p class="eyebrow">Profile</p>
            <h2 class="section-title">A security-focused developer who learns by building.</h2>
            <div class="about-grid">
              <article class="panel">
                <h3>Education</h3>
                <p>Final-year Computer Science and Engineering student specializing in Cyber Security at VIT-AP.</p>
              </article>
              <article class="panel">
                <h3>Skills</h3>
                <ul class="skill-list">
                  <li>Cybersecurity</li>
                  <li>HTML</li>
                  <li>CSS</li>
                  <li>JavaScript</li>
                  <li>Kotlin</li>
                  <li>Jetpack Compose</li>
                  <li>Firebase</li>
                  <li>GitHub</li>
                </ul>
              </article>
            </div>
          </div>
        </section>

How is the profile kept accurate?

The education card uses only the supplied VIT-AP course information. The skills list stays tied to the portfolio work and the demonstrated project technologies.

No employer, job title, employment date, email address, or portrait is introduced.

  • Save app.js.
  • Refresh Edge to reveal the education and skills content below the metrics.
  • Insert the experience section directly below the profile section:
        <section class="page-section dark-band" id="experience">
          <div class="section-shell">
            <p class="eyebrow">Experience and education</p>
            <h2 class="section-title">Learning in public through focused technical projects.</h2>
            <p class="section-lead">This section avoids unsupported employers, dates, and job titles. It presents only the academic path and repository-backed project experience supplied for the portfolio.</p>
            <div class="timeline">
              <article class="timeline-row">
                <strong>Education</strong>
                <span>VIT-AP</span>
                <p>Final-year CSE (Cyber Security) student developing foundations across security, networking, and software development.</p>
              </article>
              <article class="timeline-row">
                <strong>Project experience</strong>
                <span>Independent development</span>
                <p>Built and published web prototypes and an Android application through the AURUM, FinEase, and VITTrace repositories.</p>
              </article>
            </div>
          </div>
        </section>

Why use an experience boundary?

The timeline presents academic and repository-backed experience without implying formal employment. That boundary keeps the portfolio credible when a recruiter checks its claims.

  • Save app.js.
  • Refresh Edge to reveal the education and independent project experience rows.
  • Insert the projects section directly below the experience section:
        <section class="page-section" id="projects">
          <div class="section-shell">
            <p class="eyebrow">Selected work</p>
            <h2 class="section-title">Projects that turn ideas into visible products.</h2>
            <p class="section-lead">Each card links directly to Tarun's public GitHub repository.</p>
            <div class="projects-grid">${projectCards}</div>
          </div>
        </section>

How do the project cards appear?

The ${projectCards} expression inserts the three article strings created by renderProject(). Each card receives its own supported description, stack, and repository destination.

  • Save app.js.
  • Refresh Edge to reveal separate cards for AURUM, FinEase, and VITTrace.
Add credentials and gallery placeholders

The same data-driven approach can power the visible certification list and its gallery. One certification array now keeps both areas synchronized.

  • Insert the certifications section directly below the projects section:
        <section class="page-section dark-band" id="certifications">
          <div class="section-shell">
            <p class="eyebrow">Credentials</p>
            <h2 class="section-title">Certifications</h2>
            <ul class="certification-list">${certificationNames}</ul>
          </div>
        </section>

Why reuse the certification array?

The ${certificationNames} expression renders every supplied label as one list item. Updating the source array later updates this list without duplicating the labels.

  • Save app.js.
  • Refresh Edge to count seven certification names in the new section.
  • Insert the gallery section directly below the certifications section:
        <section class="page-section" id="gallery">
          <div class="section-shell">
            <p class="eyebrow">Certification Gallery</p>
            <h2 class="section-title">A gallery ready for the original certificate images.</h2>
            <div class="gallery-grid">${galleryCards}</div>
          </div>
        </section>

How do the placeholders stay matched?

The ${galleryCards} expression creates one numbered button for every certification object. Empty image values keep the interface honest while the original files are unavailable.

  • Save app.js.
  • Refresh Edge to count seven numbered gallery placeholders.
  • Insert the contact section directly below the gallery section:
        <section class="page-section" id="contact">
          <div class="section-shell contact-panel">
            <div>
              <p class="eyebrow">Connect</p>
              <h2 class="section-title">Explore the work and professional profile.</h2>
            </div>
            <div class="contact-links">
              <a class="button primary" href="https://github.com/taruncl-cloud" target="_blank" rel="noreferrer">GitHub</a>
              <a class="button" href="https://www.linkedin.com/in/tarun-cl-96076031b/" target="_blank" rel="noreferrer">LinkedIn</a>
              <a class="button" href="https://drive.google.com/file/d/1bT0JmgT3nRx-suxcMZvDJJkpOaui-ahx/view?usp=drivesdk" target="_blank" rel="noreferrer">Resume</a>
            </div>
          </div>
        </section>

What can visitors open?

The contact area exposes only the supplied GitHub, LinkedIn, and resume destinations. Each external destination opens in a separate browser tab.

  • Save app.js.
  • Refresh Edge to reveal the three professional links near the bottom of the page.

The footer closes the visible page. The native dialog markup prepares the gallery interface for the controls added in a later step.

  • Find the closing </main> tag inside the template.
  • Insert the footer and dialog directly below that tag:
      <footer class="site-footer">
        <span>Tarun CL</span>
        <span>Cybersecurity portfolio built with HTML, CSS, and JavaScript.</span>
      </footer>

      <dialog class="certificate-dialog" id="certificate-lightbox" aria-labelledby="lightbox-title">
        <div class="lightbox-toolbar">
          <button class="lightbox-close" id="lightbox-close" type="button" autofocus>Close</button>
        </div>
        <div class="lightbox-body">
          <div class="lightbox-stage">
            <img id="lightbox-image" alt="" hidden>
            <div class="lightbox-placeholder" id="lightbox-placeholder">
              <strong id="lightbox-title"></strong>
              <span>Certificate image placeholder</span>
            </div>
          </div>
          <div class="lightbox-nav">
            <button id="lightbox-previous" type="button">Previous</button>
            <button id="lightbox-next" type="button">Next</button>
          </div>
        </div>
      </dialog>

Why include a native dialog now?

The dialog contains a close control, an image region, a placeholder, and navigation buttons. Its IDs give the later gallery logic stable elements to update.

The dialog remains closed during this step. The visible page still shows all seven gallery buttons and their labels.

  • Save app.js.
  • Refresh Edge to reveal the footer below the contact section.

The content is complete, but the browser defaults make sections blend together. A compact baseline stylesheet separates the page areas while preserving the intentionally flat presentation.

  • Switch back to the existing styles.css tab.
  • Replace the starter text with the color and document foundations below:
:root {
  --paper: #f7f1e7;
  --paper-strong: #fffaf2;
  --ink: #171717;
  --muted: #6f6a63;
  --line: #d9d0c4;
  --coral: #ef6a4b;
  --orange: #f39a35;
  --blue: #2457d6;
  --blue-dark: #163783;
  --white: #ffffff;
  --shadow: 0 24px 70px rgba(31, 27, 23, 0.16);
  --radius: 26px;
  color-scheme: light;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  color: var(--ink);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

What do the foundations control?

The custom properties give later rules consistent colors, spacing accents, and reusable surface values. Border-box sizing keeps padding inside each element's declared width.

  • Save styles.css.
  • Refresh Edge to see the page switch to the system font and dark text.
  • Add the interactive-element and page-shell rules below the body rule:
button,
a {
  font: inherit;
}

a {
  color: inherit;
}

button {
  cursor: pointer;
}

:focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 4px;
}

portfolio-app {
  display: block;
  min-height: 100vh;
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.8rem clamp(1rem, 4vw, 4rem);
  border-bottom: 1px solid rgba(23, 23, 23, 0.1);
}

What changes in the page shell?

Links and buttons inherit the page typeface. Keyboard focus receives a visible outline, while the header separates the brand from the navigation.

  • Save styles.css.
  • Refresh Edge to see the brand and navigation share one header row.
  • Add the navigation and section typography rules below the header rule:
.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  text-decoration: none;
  font-weight: 900;
}

.nav-links {
  display: flex;
  gap: 1.15rem;
  overflow-x: auto;
}

.nav-links a {
  color: #4c4944;
  font-size: 0.76rem;
  font-weight: 800;
  text-decoration: none;
}

.page-section {
  padding: clamp(4.5rem, 9vw, 8rem) clamp(1rem, 6vw, 6rem);
}

.section-shell {
  width: min(1180px, 100%);
  margin: 0 auto;
}

How does this improve scanning?

The header links stay in one horizontal group. Each page section gains enough space to read as a separate part of the portfolio.

  • Save styles.css.
  • Refresh Edge to see wider gaps between the major sections.
  • Add the headings, action links, and content-surface rules below the section shell:
.eyebrow {
  margin: 0 0 0.9rem;
  color: var(--coral);
  font-size: 0.75rem;
  font-weight: 900;
}

.section-title {
  max-width: 780px;
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
}

.section-lead {
  max-width: 700px;
  margin: 1.3rem 0 0;
  color: var(--muted);
  line-height: 1.75;
}

.action-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  margin-top: 2rem;
}

.button {
  display: inline-flex;
  padding: 0.8rem 1.1rem;
  border: 1px solid var(--ink);
  text-decoration: none;
}

What becomes easier to spot?

Eyebrows identify each section's role, while serif headings distinguish major statements. The action links now read as controls instead of ordinary inline links.

  • Save styles.css.
  • Refresh Edge to see clearer section headings and outlined action links.
  • Add the card, project, certification, and gallery rules below the button rule:
.panel,
.project-card,
.certification-card,
.gallery-card {
  border: 1px solid var(--line);
  background: rgba(255, 250, 242, 0.78);
}

.panel {
  padding: clamp(1.4rem, 4vw, 2.5rem);
}

.projects-grid,
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
  margin-top: 3rem;
}

.project-card {
  position: relative;
  min-height: 330px;
  padding: 1.5rem;
}

.certification-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin: 3rem 0 0;
  padding: 0;
  list-style: none;
}

Why does the page still feel flat?

Borders and simple grids make every card readable, but the page still lacks responsive breakpoints, shadows, layered transforms, and motion. That visible gap is the design problem addressed next.

  • Save styles.css.
  • Refresh Edge to see three project cards, two certification columns, and three gallery columns.
  • Add the gallery, contact, footer, and dialog rules below the certification list:
.gallery-card {
  width: 100%;
  overflow: hidden;
  padding: 0;
  color: var(--ink);
  text-align: left;
}

.certificate-art {
  min-height: 220px;
  display: grid;
  place-items: center;
  border-bottom: 1px solid var(--line);
}

.gallery-caption {
  min-height: 92px;
  padding: 1rem 1.2rem;
}

.contact-panel {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2rem;
  padding: clamp(2rem, 6vw, 4rem);
  border: 1px solid var(--line);
}

.site-footer {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.4rem clamp(1rem, 6vw, 6rem);
  border-top: 1px solid var(--line);
}

.certificate-dialog {
  width: min(880px, calc(100% - 2rem));
  padding: 0;
  border: 0;
  background: var(--paper-strong);
  color: var(--ink);
}

What does the baseline finish?

Gallery buttons gain a visible image area and caption. The contact area, footer, and dialog receive enough structure for the complete content to remain readable.

  • Save styles.css.
  • Refresh Edge to see bordered gallery placeholders, a separated contact area, and a distinct footer.

Do the styles look unchanged?

Confirm that index.html still links to styles.css. Check that both files are in the same taruncl-cloud.github.io folder.

Check the CSS block above the first missing style for an unclosed brace. One missing brace can stop the following rules from applying.

Ask for help finding the stylesheet problem.

Before the final refresh, which content group do you expect to be easiest to count on the page?

  • Save index.html.
  • Save app.js.
  • Save styles.css.
  • Refresh the local index.html page in Edge.

You'll see the hero, education, skills, experience, three project cards, seven certification names, seven gallery placeholders, three professional links, and the footer on one page.

  • Select the GitHub link to confirm the supplied GitHub profile opens in a separate tab.
  • Select the LinkedIn link to confirm the supplied LinkedIn profile opens in a separate tab.
  • Select the Resume link to confirm the supplied resume opens in a separate tab.

That completes the content layer. The portfolio now presents the supplied information without unsupported employment details or credential metadata.

✔️ Awesome, I've got everything!

Great work. Save all three files once more before moving on.

ⓧ I'd like to double check the full code

Compare your files with these cumulative versions. They represent the complete project state at the end of this step.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="Tarun CL cybersecurity and software development portfolio">
  <title>Tarun CL | Cybersecurity Portfolio</title>
  <link rel="stylesheet" href="styles.css">
  <script src="app.js" defer></script>
</head>
<body>
  <portfolio-app></portfolio-app>
</body>
</html>

What should match in index.html?

The document loads styles.css and deferred app.js. Its body contains only the <portfolio-app> host.

const certifications = [
  { name: "Agentic AI Foundations Associate \u2014 Oracle", image: "" },
  { name: "Introduction to Cybersecurity", image: "" },
  { name: "Google TechSpirit", image: "" },
  { name: "Cybersecurity Certification", image: "" },
  { name: "CEHv13 ISO", image: "" },
  { name: "Cisco Packet Tracer", image: "" },
  { name: "CompTIA Security+", image: "" }
];

const projects = [
  {
    number: "01",
    name: "AURUM",
    description: "A single-page AI tools platform prototype with chatbot, document generation, and workflow interfaces.",
    stack: ["HTML", "CSS", "JavaScript"],
    url: "https://github.com/taruncl-cloud/aurum-ai"
  },
  {
    number: "02",
    name: "FinEase",
    description: "A responsive landing-page concept for small-business finance tracking, reporting, and tax workflows.",
    stack: ["HTML", "CSS", "JavaScript"],
    url: "https://github.com/taruncl-cloud/finease-website"
  },
  {
    number: "03",
    name: "VITTrace",
    description: "A VIT-AP lost-and-found Android app with authentication, item reporting, search, image upload, and status tracking.",
    stack: ["Kotlin", "Jetpack Compose", "Firebase"],
    url: "https://github.com/taruncl-cloud/VITTrace"
  }
];

class PortfolioApp extends HTMLElement {
  constructor() {
    super();
    this.activeCertificateIndex = 0;
    this.lastGalleryTrigger = null;
  }

  connectedCallback() {
    this.render();
  }

  renderProject(project) {
    const stack = project.stack.map((item) => `<span>${item}</span>`).join("");

    return `
      <article class="project-card tilt-card" data-tilt>
        <div class="project-number">${project.number}</div>
        <h3>${project.name}</h3>
        <p>${project.description}</p>
        <div class="stack">${stack}</div>
        <a class="project-link" href="${project.url}" target="_blank" rel="noreferrer">View repository</a>
      </article>
    `;
  }

  renderGalleryCard(certificate, index) {
    const shortMark = String(index + 1).padStart(2, "0");

    return `
      <button class="gallery-card tilt-card" type="button" data-certificate-index="${index}" data-tilt aria-label="Open ${certificate.name} certificate placeholder">
        <span class="certificate-art" aria-hidden="true">
          <span class="placeholder-mark">${shortMark}</span>
        </span>
        <span class="gallery-caption">
          <strong>${certificate.name}</strong>
          <span>Image placeholder</span>
        </span>
      </button>
    `;
  }

  render() {
    const projectCards = projects.map((project) => this.renderProject(project)).join("");
    const certificationNames = certifications.map((certificate) => `<li>${certificate.name}</li>`).join("");
    const galleryCards = certifications.map((certificate, index) => this.renderGalleryCard(certificate, index)).join("");

    this.innerHTML = `
      <header class="site-header">
        <a class="brand" href="#home" aria-label="Tarun CL home">
          <span class="brand-mark">TC</span>
          <span>Tarun CL</span>
        </a>
        <nav class="nav-links" aria-label="Primary navigation">
          <a href="#about">About</a>
          <a href="#experience">Experience</a>
          <a href="#projects">Projects</a>
          <a href="#certifications">Certifications</a>
          <a href="#gallery">Gallery</a>
          <a href="#contact">Contact</a>
        </nav>
      </header>

      <main>
        <section class="page-section hero" id="home">
          <div>
            <p class="eyebrow">Cybersecurity, web, and Android development</p>
            <h1>Tarun <span>CL</span></h1>
            <p class="hero-copy">Final-year CSE (Cyber Security) student at VIT-AP, building practical web and mobile projects with a security-minded approach.</p>
            <div class="action-row">
              <a class="button primary" href="#projects">Explore projects</a>
              <a class="button" href="https://drive.google.com/file/d/1bT0JmgT3nRx-suxcMZvDJJkpOaui-ahx/view?usp=drivesdk" target="_blank" rel="noreferrer">Open resume</a>
            </div>
          </div>

          <div class="hero-visual" aria-label="Animated Tarun CL monogram">
            <div class="identity-card">
              <div class="identity-grid"></div>
              <div class="orbit"></div>
              <div class="orbit secondary"></div>
              <div class="monogram">TC</div>
              <div class="identity-caption">
                <span>Bengaluru, India</span>
                <span>Security and software</span>
              </div>
            </div>
          </div>
        </section>

        <div class="section-shell metric-rail" aria-label="Portfolio metrics">
          <div class="metric"><strong>10</strong><span>Public repositories</span></div>
          <div class="metric"><strong>7</strong><span>Certifications</span></div>
          <div class="metric"><strong>3</strong><span>Featured projects</span></div>
        </div>

        <section class="page-section" id="about">
          <div class="section-shell">
            <p class="eyebrow">Profile</p>
            <h2 class="section-title">A security-focused developer who learns by building.</h2>
            <div class="about-grid">
              <article class="panel">
                <h3>Education</h3>
                <p>Final-year Computer Science and Engineering student specializing in Cyber Security at VIT-AP.</p>
              </article>
              <article class="panel">
                <h3>Skills</h3>
                <ul class="skill-list">
                  <li>Cybersecurity</li>
                  <li>HTML</li>
                  <li>CSS</li>
                  <li>JavaScript</li>
                  <li>Kotlin</li>
                  <li>Jetpack Compose</li>
                  <li>Firebase</li>
                  <li>GitHub</li>
                </ul>
              </article>
            </div>
          </div>
        </section>

        <section class="page-section dark-band" id="experience">
          <div class="section-shell">
            <p class="eyebrow">Experience and education</p>
            <h2 class="section-title">Learning in public through focused technical projects.</h2>
            <p class="section-lead">This section avoids unsupported employers, dates, and job titles. It presents only the academic path and repository-backed project experience supplied for the portfolio.</p>
            <div class="timeline">
              <article class="timeline-row">
                <strong>Education</strong>
                <span>VIT-AP</span>
                <p>Final-year CSE (Cyber Security) student developing foundations across security, networking, and software development.</p>
              </article>
              <article class="timeline-row">
                <strong>Project experience</strong>
                <span>Independent development</span>
                <p>Built and published web prototypes and an Android application through the AURUM, FinEase, and VITTrace repositories.</p>
              </article>
            </div>
          </div>
        </section>

        <section class="page-section" id="projects">
          <div class="section-shell">
            <p class="eyebrow">Selected work</p>
            <h2 class="section-title">Projects that turn ideas into visible products.</h2>
            <p class="section-lead">Each card links directly to Tarun's public GitHub repository.</p>
            <div class="projects-grid">${projectCards}</div>
          </div>
        </section>

        <section class="page-section dark-band" id="certifications">
          <div class="section-shell">
            <p class="eyebrow">Credentials</p>
            <h2 class="section-title">Certifications</h2>
            <ul class="certification-list">${certificationNames}</ul>
          </div>
        </section>

        <section class="page-section" id="gallery">
          <div class="section-shell">
            <p class="eyebrow">Certification Gallery</p>
            <h2 class="section-title">A gallery ready for the original certificate images.</h2>
            <div class="gallery-grid">${galleryCards}</div>
          </div>
        </section>

        <section class="page-section" id="contact">
          <div class="section-shell contact-panel">
            <div>
              <p class="eyebrow">Connect</p>
              <h2 class="section-title">Explore the work and professional profile.</h2>
            </div>
            <div class="contact-links">
              <a class="button primary" href="https://github.com/taruncl-cloud" target="_blank" rel="noreferrer">GitHub</a>
              <a class="button" href="https://www.linkedin.com/in/tarun-cl-96076031b/" target="_blank" rel="noreferrer">LinkedIn</a>
              <a class="button" href="https://drive.google.com/file/d/1bT0JmgT3nRx-suxcMZvDJJkpOaui-ahx/view?usp=drivesdk" target="_blank" rel="noreferrer">Resume</a>
            </div>
          </div>
        </section>
      </main>

      <footer class="site-footer">
        <span>Tarun CL</span>
        <span>Cybersecurity portfolio built with HTML, CSS, and JavaScript.</span>
      </footer>

      <dialog class="certificate-dialog" id="certificate-lightbox" aria-labelledby="lightbox-title">
        <div class="lightbox-toolbar">
          <button class="lightbox-close" id="lightbox-close" type="button" autofocus>Close</button>
        </div>
        <div class="lightbox-body">
          <div class="lightbox-stage">
            <img id="lightbox-image" alt="" hidden>
            <div class="lightbox-placeholder" id="lightbox-placeholder">
              <strong id="lightbox-title"></strong>
              <span>Certificate image placeholder</span>
            </div>
          </div>
          <div class="lightbox-nav">
            <button id="lightbox-previous" type="button">Previous</button>
            <button id="lightbox-next" type="button">Next</button>
          </div>
        </div>
      </dialog>
    `;
  }
}

customElements.define("portfolio-app", PortfolioApp);

What should match in app.js?

The file contains both data arrays, the complete PortfolioApp class, all rendered content sections, and the custom-element registration. The component renders immediately through connectedCallback().

:root {
  --paper: #f7f1e7;
  --paper-strong: #fffaf2;
  --ink: #171717;
  --muted: #6f6a63;
  --line: #d9d0c4;
  --coral: #ef6a4b;
  --orange: #f39a35;
  --blue: #2457d6;
  --blue-dark: #163783;
  --white: #ffffff;
  --shadow: 0 24px 70px rgba(31, 27, 23, 0.16);
  --radius: 26px;
  color-scheme: light;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  color: var(--ink);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

button,
a {
  font: inherit;
}

a {
  color: inherit;
}

button {
  cursor: pointer;
}

:focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 4px;
}

portfolio-app {
  display: block;
  min-height: 100vh;
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.8rem clamp(1rem, 4vw, 4rem);
  border-bottom: 1px solid rgba(23, 23, 23, 0.1);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  text-decoration: none;
  font-weight: 900;
}

.nav-links {
  display: flex;
  gap: 1.15rem;
  overflow-x: auto;
}

.nav-links a {
  color: #4c4944;
  font-size: 0.76rem;
  font-weight: 800;
  text-decoration: none;
}

.page-section {
  padding: clamp(4.5rem, 9vw, 8rem) clamp(1rem, 6vw, 6rem);
}

.section-shell {
  width: min(1180px, 100%);
  margin: 0 auto;
}

.eyebrow {
  margin: 0 0 0.9rem;
  color: var(--coral);
  font-size: 0.75rem;
  font-weight: 900;
}

.section-title {
  max-width: 780px;
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
}

.section-lead {
  max-width: 700px;
  margin: 1.3rem 0 0;
  color: var(--muted);
  line-height: 1.75;
}

.action-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  margin-top: 2rem;
}

.button {
  display: inline-flex;
  padding: 0.8rem 1.1rem;
  border: 1px solid var(--ink);
  text-decoration: none;
}

.panel,
.project-card,
.certification-card,
.gallery-card {
  border: 1px solid var(--line);
  background: rgba(255, 250, 242, 0.78);
}

.panel {
  padding: clamp(1.4rem, 4vw, 2.5rem);
}

.projects-grid,
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
  margin-top: 3rem;
}

.project-card {
  position: relative;
  min-height: 330px;
  padding: 1.5rem;
}

.certification-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  margin: 3rem 0 0;
  padding: 0;
  list-style: none;
}

.gallery-card {
  width: 100%;
  overflow: hidden;
  padding: 0;
  color: var(--ink);
  text-align: left;
}

.certificate-art {
  min-height: 220px;
  display: grid;
  place-items: center;
  border-bottom: 1px solid var(--line);
}

.gallery-caption {
  min-height: 92px;
  padding: 1rem 1.2rem;
}

.contact-panel {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2rem;
  padding: clamp(2rem, 6vw, 4rem);
  border: 1px solid var(--line);
}

.site-footer {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.4rem clamp(1rem, 6vw, 6rem);
  border-top: 1px solid var(--line);
}

.certificate-dialog {
  width: min(880px, calc(100% - 2rem));
  padding: 0;
  border: 0;
  background: var(--paper-strong);
  color: var(--ink);
}

What should match in styles.css?

The stylesheet supplies the flat baseline for typography, navigation, sections, cards, certifications, gallery placeholders, contact links, the footer, and the dialog. Responsive rules and 3D effects remain outside this step.

You now have a complete content-first portfolio backed by one custom element. Next, you'll turn its flat sections into a responsive visual system with controlled 3D depth.

Build the Premium 3D Visual System

Your portfolio now renders Tarun's profile, projects, certifications, gallery placeholders, and professional links. That content foundation gives every visual choice a real purpose.

The current CSS presentation feels flat on a desktop screen. Its layout also becomes cramped when the browser narrows, which weakens the premium experience.

In this step, get ready to:
  • Build the editorial colour system and strengthen the page hierarchy.
  • Add layered depth and pointer-controlled card movement.
  • Create responsive layouts and reduced-motion fallbacks.
Shape the editorial design system

A design system stores recurring colours, spacing values, shadows, and corner shapes as reusable properties. This keeps the portfolio consistent when the same visual language appears across the hero, cards, gallery, and dialog.

Before you resize the current page, do you expect every section to remain readable in a narrow window?

  • Switch back to Microsoft Edge from earlier.
  • Narrow the current browser window to a phone-sized width.

You'll see the flat layout become cramped as the available width shrinks. That visible shortfall is the reason this step needs both visual hierarchy and responsive rules.

  • Return the browser window to a desktop width.
  • Switch back to Visual Studio Code from earlier.
  • Select styles.css in the Explorer sidebar.
  • Replace the existing colour properties at the top of styles.css with this design system:
:root {
  --paper: #f7f1e7;
  --paper-strong: #fffaf2;
  --ink: #171717;
  --muted: #6f6a63;
  --line: #d9d0c4;
  --coral: #ef6a4b;
  --orange: #f39a35;
  --blue: #2457d6;
  --blue-dark: #163783;
  --white: #ffffff;
  --shadow: 0 24px 70px rgba(31, 27, 23, 0.16);
  --radius: 26px;
  color-scheme: light;
}

How does the palette work?

  • The paper colours create the warm-white editorial surface used across the page.
  • The ink colour creates the charcoal showcase bands and primary text.
  • The coral and orange properties provide warm accents for buttons, labels, and animated rings.
  • The blue properties provide contrast for links and visible keyboard focus.
  • Replace the global page foundation below the colour properties with this code:
* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background:
    radial-gradient(circle at 8% 8%, rgba(239, 106, 75, 0.14), transparent 24rem),
    radial-gradient(circle at 92% 28%, rgba(36, 87, 214, 0.1), transparent 28rem),
    var(--paper);
  color: var(--ink);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

What does this foundation change?

The universal sizing rule keeps padding inside each element's declared width. The body layers two soft colour glows over the warm paper background.

The system font stack keeps body text readable across Windows devices. Later heading rules use Georgia to create the editorial contrast.

  • Save styles.css.
  • Refresh the local page in Microsoft Edge.

You'll see a warm-white canvas with coral and blue background glows. The text now uses the charcoal ink colour from the shared palette.

Still seeing the plain background?

Confirm that styles.css is saved in the taruncl-cloud.github.io repository. Check that index.html still links to the same filename.

Refresh the page after saving the file. A stale browser view can continue showing the previous styles.

help me find why my CSS colour properties and body background are not appearing

Add depth and pointer tilt

CSS 3D transforms place visual layers at different depths from the screen. The hero establishes that depth with perspective before its identity card moves in three-dimensional space.

  • Find the hero artwork selectors in styles.css.
  • Replace the hero scene selectors with this code:
.hero-visual {
  perspective: 1100px;
}

.identity-card {
  position: relative;
  min-height: 500px;
  overflow: hidden;
  border: 1px solid rgba(23, 23, 23, 0.22);
  border-radius: var(--radius);
  background: linear-gradient(145deg, #1b1b1b, #252525 65%, #111111);
  box-shadow: var(--shadow);
  transform: rotateY(-8deg) rotateX(3deg);
  transform-style: preserve-3d;
  animation: float-card 6s ease-in-out infinite;
}

.identity-grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.055) 1px, transparent 1px);
  background-size: 42px 42px;
}

How does the hero gain depth?

The hero perspective controls how strongly its depth appears to the viewer. A smaller distance would create a more dramatic effect.

The identity card preserves the depth of its child layers. Its initial rotations make the card feel angled before pointer interaction begins.

  • Save styles.css.
  • Refresh the local page in Microsoft Edge.

You'll see a dark identity card with a fine grid inside the hero. The card floats at a slight angle above the warm page.

Is the identity card still flat?

Check that the stylesheet uses .hero-visual and .identity-card exactly. These class names already exist in the rendered hero markup.

Confirm that the closing brace for each selector remains in place. A missing brace can prevent every rule below it from loading.

help me debug why the hero perspective and identity card styles are not visible

  • Replace the orbit selectors in styles.css with this code:
.orbit {
  position: absolute;
  inset: 16% 13%;
  border: 1px solid rgba(243, 154, 53, 0.7);
  border-radius: 50%;
  transform: translateZ(45px) rotateX(68deg);
  animation: orbit 9s linear infinite;
}

.orbit.secondary {
  inset: 26% 8%;
  border-color: rgba(36, 87, 214, 0.72);
  transform: translateZ(24px) rotateY(68deg);
  animation-direction: reverse;
  animation-duration: 12s;
}

What do the orbit layers do?

The first ring sits closer to the viewer through its positive Z translation. The secondary ring uses a different orientation to create crossing depth.

Their different durations stop the movement from feeling mechanically synchronized.

  • Save styles.css.
  • Refresh the local page in Microsoft Edge.

You'll see orange and blue rings rotate inside the identity card. Their crossing angles make the artwork feel layered.

  • Replace the monogram and caption selectors in styles.css with this code:
.monogram {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--paper-strong);
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(6rem, 16vw, 12rem);
  letter-spacing: -0.11em;
  text-shadow: 18px 18px 0 rgba(239, 106, 75, 0.26);
  transform: translateZ(85px);
}

.identity-caption {
  position: absolute;
  right: 1.5rem;
  bottom: 1.5rem;
  left: 1.5rem;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  color: #d8d2c9;
  font-size: 0.76rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transform: translateZ(70px);
}

Why separate the foreground layers?

The monogram sits furthest forward at translateZ(85px). The caption sits slightly behind it at translateZ(70px).

Those different depths become visible when the parent card rotates.

  • Save styles.css.
  • Refresh the local page in Microsoft Edge.

You'll see the large TC monogram float above the rings. The location caption appears as another foreground layer near the bottom.

  • Find the shared card movement selectors in styles.css.
  • Replace those selectors with this code:
.tilt-card {
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  transform: perspective(900px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transform-style: preserve-3d;
  transition: transform 160ms ease, box-shadow 160ms ease;
}

.tilt-card:hover {
  box-shadow: 0 28px 65px rgba(23, 23, 23, 0.2);
}

How do the tilt properties work?

Each card starts with zero rotation through --tilt-x and --tilt-y. The JavaScript interaction updates those properties without replacing the rest of the card's styling.

The short transition smooths the movement. The hover shadow reinforces the card's distance from the page.

  • Save styles.css.
  • Refresh the local page in Microsoft Edge.
  • Move the pointer over a project card.

You'll see the card shadow deepen when the pointer enters it. The next edit connects the card's rotation to the pointer position.

  • Select app.js in the Explorer sidebar.
  • Locate the connectedCallback() method.
  • Add this method call directly after this.render():
this.bindTilt();

Why call bindTilt here?

The render method creates the project and gallery cards first. The new call attaches pointer listeners after those elements exist.

  • Locate the closing brace of the PortfolioApp class.
  • Add this method immediately before that closing brace:
bindTilt() {
  this.querySelectorAll("[data-tilt]").forEach((card) => {
    card.addEventListener("pointermove", (event) => {
      const bounds = card.getBoundingClientRect();
      const horizontal = (event.clientX - bounds.left) / bounds.width - 0.5;
      const vertical = (event.clientY - bounds.top) / bounds.height - 0.5;
      card.style.setProperty("--tilt-x", `${vertical * -8}deg`);
      card.style.setProperty("--tilt-y", `${horizontal * 10}deg`);
    });

    card.addEventListener("pointerleave", () => {
      card.style.setProperty("--tilt-x", "0deg");
      card.style.setProperty("--tilt-y", "0deg");
    });
  });
}

What does the pointer code do?

  • The selector finds every project or gallery card carrying the data-tilt attribute.
  • The pointer position becomes a value around the centre of the card.
  • The calculated values update the two CSS rotation properties.
  • The pointer-leave listener returns each card to its resting position.
  • Save app.js.

Before you refresh, do you expect the project cards to stay fixed or follow the pointer?

  • Refresh the local page in Microsoft Edge.
  • Move the pointer across each project card.
  • Move the pointer away from the cards.

You'll see each project card tilt toward the pointer. It returns to a level position when the pointer leaves.

Cards not following the pointer?

Confirm that this.bindTilt() appears after this.render() inside connectedCallback().

Check that the card markup still uses tilt-card and data-tilt. The method only attaches listeners to elements with that attribute.

help me debug why bindTilt does not rotate my data-tilt cards on pointer movement

Make the layout responsive and motion-aware

Responsive CSS changes the layout when the viewport crosses defined widths. The prefers-reduced-motion query respects a user's request to minimize non-essential movement without removing any portfolio content.

The complete stylesheet below includes breakpoints at 920px and 620px. It also contains every animation, focus, card, gallery, dialog, and reduced-motion rule needed for the final visual system.

  • Compare styles.css with the full reference in the second tab below.
  • Replace any differing selector so the file matches the reference.
  • Save styles.css.

✔️ My stylesheet matches

Your stylesheet now contains the complete editorial layout, 3D scene, responsive breakpoints, and reduced-motion fallback.

ⓧ I'd like to double check the full code

:root {
  --paper: #f7f1e7;
  --paper-strong: #fffaf2;
  --ink: #171717;
  --muted: #6f6a63;
  --line: #d9d0c4;
  --coral: #ef6a4b;
  --orange: #f39a35;
  --blue: #2457d6;
  --blue-dark: #163783;
  --white: #ffffff;
  --shadow: 0 24px 70px rgba(31, 27, 23, 0.16);
  --radius: 26px;
  color-scheme: light;
}

* {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background:
    radial-gradient(circle at 8% 8%, rgba(239, 106, 75, 0.14), transparent 24rem),
    radial-gradient(circle at 92% 28%, rgba(36, 87, 214, 0.1), transparent 28rem),
    var(--paper);
  color: var(--ink);
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

button,
a {
  font: inherit;
}

a {
  color: inherit;
}

button {
  cursor: pointer;
}

:focus-visible {
  outline: 3px solid var(--blue);
  outline-offset: 4px;
}

portfolio-app {
  display: block;
  min-height: 100vh;
}

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 72px;
  padding: 0.8rem clamp(1rem, 4vw, 4rem);
  border-bottom: 1px solid rgba(23, 23, 23, 0.1);
  background: rgba(247, 241, 231, 0.9);
  backdrop-filter: blur(18px);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.65rem;
  text-decoration: none;
  font-weight: 900;
  letter-spacing: -0.04em;
}

.brand-mark {
  display: grid;
  width: 2.1rem;
  aspect-ratio: 1;
  place-items: center;
  background: var(--coral);
  color: var(--white);
  transform: rotate(-8deg);
}

.nav-links {
  display: flex;
  gap: 1.15rem;
  overflow-x: auto;
  scrollbar-width: none;
}

.nav-links::-webkit-scrollbar {
  display: none;
}

.nav-links a {
  color: #4c4944;
  font-size: 0.76rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-decoration: none;
  text-transform: uppercase;
  white-space: nowrap;
}

.nav-links a:hover {
  color: var(--coral);
}

.page-section {
  padding: clamp(4.5rem, 9vw, 8rem) clamp(1rem, 6vw, 6rem);
}

.section-shell {
  width: min(1180px, 100%);
  margin: 0 auto;
}

.eyebrow {
  margin: 0 0 0.9rem;
  color: var(--coral);
  font-size: 0.75rem;
  font-weight: 900;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.section-title {
  max-width: 780px;
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(2.3rem, 6vw, 5.6rem);
  font-weight: 500;
  letter-spacing: -0.055em;
  line-height: 0.98;
}

.section-lead {
  max-width: 700px;
  margin: 1.3rem 0 0;
  color: var(--muted);
  font-size: clamp(1rem, 1.6vw, 1.2rem);
  line-height: 1.75;
}

.hero {
  min-height: calc(100vh - 72px);
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr);
  align-items: center;
  gap: clamp(2rem, 7vw, 7rem);
}

.hero h1 {
  max-width: 760px;
  margin: 0;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(3.6rem, 9vw, 8.6rem);
  font-weight: 500;
  letter-spacing: -0.075em;
  line-height: 0.84;
}

.hero h1 span {
  display: block;
  color: var(--coral);
  font-style: italic;
}

.hero-copy {
  max-width: 650px;
  margin: 1.6rem 0 0;
  color: #514c46;
  font-size: clamp(1rem, 1.8vw, 1.24rem);
  line-height: 1.75;
}

.action-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  margin-top: 2rem;
}

.button {
  display: inline-flex;
  min-height: 48px;
  align-items: center;
  justify-content: center;
  padding: 0.8rem 1.1rem;
  border: 1px solid var(--ink);
  background: transparent;
  color: var(--ink);
  font-size: 0.78rem;
  font-weight: 900;
  letter-spacing: 0.08em;
  text-decoration: none;
  text-transform: uppercase;
  transition: transform 180ms ease, box-shadow 180ms ease, background 180ms ease;
}

.button.primary {
  border-color: var(--coral);
  background: var(--coral);
  color: var(--white);
}

.button:hover {
  transform: translateY(-3px);
  box-shadow: 7px 7px 0 var(--ink);
}

.hero-visual {
  perspective: 1100px;
}

.identity-card {
  position: relative;
  min-height: 500px;
  overflow: hidden;
  border: 1px solid rgba(23, 23, 23, 0.22);
  border-radius: var(--radius);
  background: linear-gradient(145deg, #1b1b1b, #252525 65%, #111111);
  box-shadow: var(--shadow);
  transform: rotateY(-8deg) rotateX(3deg);
  transform-style: preserve-3d;
  animation: float-card 6s ease-in-out infinite;
}

.identity-grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.055) 1px, transparent 1px);
  background-size: 42px 42px;
}

.orbit {
  position: absolute;
  inset: 16% 13%;
  border: 1px solid rgba(243, 154, 53, 0.7);
  border-radius: 50%;
  transform: translateZ(45px) rotateX(68deg);
  animation: orbit 9s linear infinite;
}

.orbit.secondary {
  inset: 26% 8%;
  border-color: rgba(36, 87, 214, 0.72);
  transform: translateZ(24px) rotateY(68deg);
  animation-direction: reverse;
  animation-duration: 12s;
}

.monogram {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--paper-strong);
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(6rem, 16vw, 12rem);
  letter-spacing: -0.11em;
  text-shadow: 18px 18px 0 rgba(239, 106, 75, 0.26);
  transform: translateZ(85px);
}

.identity-caption {
  position: absolute;
  right: 1.5rem;
  bottom: 1.5rem;
  left: 1.5rem;
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  color: #d8d2c9;
  font-size: 0.76rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transform: translateZ(70px);
}

.metric-rail {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.metric {
  padding: 1.4rem;
  border-right: 1px solid var(--line);
}

.metric:last-child {
  border-right: 0;
}

.metric strong {
  display: block;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(2rem, 4vw, 3.5rem);
  font-weight: 500;
}

.metric span {
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 0.8fr);
  gap: 2rem;
  margin-top: 3rem;
}

.panel,
.project-card,
.certification-card,
.gallery-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: rgba(255, 250, 242, 0.78);
  box-shadow: 0 14px 45px rgba(49, 42, 34, 0.08);
}

.panel {
  padding: clamp(1.4rem, 4vw, 2.5rem);
}

.panel h3 {
  margin: 0 0 1rem;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 1.8rem;
}

.panel p {
  color: var(--muted);
  line-height: 1.75;
}

.skill-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.55rem;
  padding: 0;
  list-style: none;
}

.skill-list li {
  padding: 0.58rem 0.78rem;
  border: 1px solid var(--line);
  background: var(--white);
  font-size: 0.78rem;
  font-weight: 800;
}

.dark-band {
  background: var(--ink);
  color: var(--paper-strong);
}

.dark-band .eyebrow {
  color: var(--orange);
}

.dark-band .section-lead {
  color: #b9b1a7;
}

.timeline {
  margin-top: 3rem;
  border-top: 1px solid #3b3b3b;
}

.timeline-row {
  display: grid;
  grid-template-columns: 0.6fr 1fr 1.5fr;
  gap: 1rem;
  padding: 1.6rem 0;
  border-bottom: 1px solid #3b3b3b;
}

.timeline-row p {
  margin: 0;
  color: #aaa39a;
  line-height: 1.65;
}

.projects-grid,
.gallery-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
  margin-top: 3rem;
}

.tilt-card {
  --tilt-x: 0deg;
  --tilt-y: 0deg;
  transform: perspective(900px) rotateX(var(--tilt-x)) rotateY(var(--tilt-y));
  transform-style: preserve-3d;
  transition: transform 160ms ease, box-shadow 160ms ease;
}

.tilt-card:hover {
  box-shadow: 0 28px 65px rgba(23, 23, 23, 0.2);
}

.project-card {
  position: relative;
  min-height: 330px;
  padding: 1.5rem;
}

.project-number {
  color: var(--coral);
  font-family: Georgia, "Times New Roman", serif;
  font-size: 3.7rem;
  line-height: 1;
  transform: translateZ(38px);
}

.project-card h3 {
  margin: 2.5rem 0 0.5rem;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 2rem;
  transform: translateZ(48px);
}

.project-card p {
  color: var(--muted);
  line-height: 1.65;
  transform: translateZ(30px);
}

.project-link {
  display: inline-block;
  margin-top: 1rem;
  color: var(--blue);
  font-size: 0.76rem;
  font-weight: 900;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transform: translateZ(45px);
}

.stack {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 1rem;
  transform: translateZ(34px);
}

.stack span {
  padding: 0.35rem 0.55rem;
  background: #eee7dd;
  font-size: 0.7rem;
  font-weight: 800;
}

.certification-list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0;
  margin: 3rem 0 0;
  padding: 0;
  border-top: 1px solid #3b3b3b;
  list-style: none;
}

.certification-list li {
  min-height: 92px;
  display: flex;
  align-items: center;
  padding: 1rem 1.2rem;
  border-right: 1px solid #3b3b3b;
  border-bottom: 1px solid #3b3b3b;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(1.05rem, 2.2vw, 1.45rem);
}

.gallery-card {
  width: 100%;
  overflow: hidden;
  padding: 0;
  color: var(--ink);
  text-align: left;
}

.certificate-art {
  position: relative;
  min-height: 220px;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-bottom: 1px solid var(--line);
  background:
    linear-gradient(135deg, rgba(36, 87, 214, 0.12), transparent 46%),
    linear-gradient(315deg, rgba(239, 106, 75, 0.17), transparent 48%),
    var(--paper-strong);
}

.certificate-art::before,
.certificate-art::after {
  content: "";
  position: absolute;
  border: 1px solid rgba(23, 23, 23, 0.18);
}

.certificate-art::before {
  inset: 18px;
}

.certificate-art::after {
  inset: 34px;
}

.placeholder-mark {
  position: relative;
  z-index: 1;
  display: grid;
  width: 74px;
  aspect-ratio: 1;
  place-items: center;
  border-radius: 50%;
  background: var(--ink);
  color: var(--white);
  font-family: Georgia, "Times New Roman", serif;
  font-size: 1.8rem;
}

.gallery-caption {
  min-height: 92px;
  padding: 1rem 1.2rem;
}

.gallery-caption strong {
  display: block;
  line-height: 1.35;
}

.gallery-caption span {
  display: block;
  margin-top: 0.4rem;
  color: var(--muted);
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.contact-panel {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: end;
  gap: 2rem;
  padding: clamp(2rem, 6vw, 4rem);
  border: 1px solid var(--line);
  background: var(--paper-strong);
  box-shadow: var(--shadow);
}

.contact-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
}

.site-footer {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.4rem clamp(1rem, 6vw, 6rem);
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.82rem;
}

.certificate-dialog {
  width: min(880px, calc(100% - 2rem));
  padding: 0;
  border: 0;
  border-radius: var(--radius);
  background: var(--paper-strong);
  color: var(--ink);
  box-shadow: 0 30px 100px rgba(0, 0, 0, 0.48);
}

.certificate-dialog::backdrop {
  background: rgba(10, 10, 10, 0.78);
  backdrop-filter: blur(8px);
}

.lightbox-toolbar {
  display: flex;
  justify-content: flex-end;
  padding: 0.8rem;
  border-bottom: 1px solid var(--line);
}

.lightbox-close,
.lightbox-nav button {
  min-height: 42px;
  padding: 0.65rem 0.9rem;
  border: 1px solid var(--ink);
  background: transparent;
  color: var(--ink);
  font-weight: 800;
}

.lightbox-body {
  padding: clamp(1rem, 4vw, 2.5rem);
}

.lightbox-stage {
  min-height: 420px;
  display: grid;
  place-items: center;
  overflow: hidden;
  border: 1px solid var(--line);
  background: #e9e1d6;
}

.lightbox-stage img {
  display: block;
  width: 100%;
  max-height: 70vh;
  object-fit: contain;
}

.lightbox-placeholder {
  max-width: 520px;
  padding: 2rem;
  text-align: center;
}

.lightbox-placeholder strong {
  display: block;
  font-family: Georgia, "Times New Roman", serif;
  font-size: clamp(1.6rem, 4vw, 3rem);
}

.lightbox-placeholder span {
  display: block;
  margin-top: 1rem;
  color: var(--muted);
}

.lightbox-nav {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1rem;
}

@keyframes float-card {
  0%, 100% { transform: rotateY(-8deg) rotateX(3deg) translateY(0); }
  50% { transform: rotateY(-4deg) rotateX(1deg) translateY(-12px); }
}

@keyframes orbit {
  to { transform: translateZ(45px) rotateX(68deg) rotateZ(360deg); }
}

@media (max-width: 920px) {
  .site-header {
    align-items: flex-start;
    flex-direction: column;
  }

  .nav-links {
    width: 100%;
    padding-bottom: 0.25rem;
  }

  .hero,
  .about-grid,
  .contact-panel {
    grid-template-columns: 1fr;
  }

  .hero {
    min-height: auto;
  }

  .identity-card {
    min-height: 420px;
  }

  .timeline-row {
    grid-template-columns: 1fr;
  }

  .projects-grid,
  .gallery-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 620px) {
  .page-section {
    padding-top: 4rem;
    padding-bottom: 4rem;
  }

  .hero h1 {
    font-size: clamp(3.1rem, 20vw, 5.5rem);
  }

  .metric-rail,
  .certification-list,
  .projects-grid,
  .gallery-grid {
    grid-template-columns: 1fr;
  }

  .metric {
    border-right: 0;
    border-bottom: 1px solid var(--line);
  }

  .metric:last-child {
    border-bottom: 0;
  }

  .certification-list li {
    border-right: 0;
  }

  .identity-card {
    min-height: 340px;
  }

  .lightbox-stage {
    min-height: 300px;
  }

  .site-footer {
    flex-direction: column;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }

  .identity-card,
  .tilt-card,
  .orbit,
  .monogram,
  .identity-caption {
    transform: none !important;
  }
}

What should the full stylesheet contain?

This reference contains the complete warm-white and charcoal visual system from the project code. It includes the exact responsive breakpoints and motion overrides used by the finished portfolio.

The reduced-motion rules keep every section visible. They suppress transforms, animation, smooth scrolling, and long transitions when the operating system requests less motion.

Before you test the finished visual system, which parts do you expect to move at desktop width?

  • Refresh the local page in Microsoft Edge.
  • Move the pointer across the hero identity card.
  • Move the pointer across each project card.

You'll see the hero artwork float with layered depth. Each project card tilts according to the pointer position.

  • Narrow the Microsoft Edge window to a phone-sized width.
  • Scroll from the navigation to the footer.

You'll see the hero, education panels, timeline, projects, certifications, gallery, contact panel, and footer stack into single-column layouts. The page remains readable without horizontal overflow.

Still seeing horizontal overflow?

Confirm that both responsive queries are present at the bottom of styles.css. Check that the projects and gallery use the responsive grid rules from the full reference.

Inspect the hero width if the page still scrolls sideways. The complete stylesheet removes the two-column hero layout below the first breakpoint.

help me find which portfolio element causes horizontal overflow at a narrow viewport

Your portfolio now has the depth, hierarchy, and responsive behaviour expected from a premium presentation. Next, you'll turn the certification placeholders into an accessible lightbox experience.

Add the Certification Gallery and Lightbox

Your portfolio now has depth, responsive layouts, and pointer-driven motion. However, visitors still need a focused way to inspect each certification placeholder in Microsoft Edge.

The certificate images are not available yet. In this step, you will connect the existing gallery to a native modal that works with placeholders now and real images later.

In this step, get ready to:
  • Connect all seven gallery buttons to the matching certification data.
  • Add Previous and Next controls for moving through the certificates.
  • Support keyboard navigation, modal dismissal, and focus restoration.
Connect the gallery to the dialog

Each object in certifications already contains a supplied name and an empty image value. The same data powers each gallery button, which keeps its caption linked to the correct placeholder.

Why use a native dialog?

A native dialog opened with showModal() places the lightbox above the page. The browser also makes the rest of the document inactive while the modal remains open.

The browser supports Escape dismissal by default. Your code adds visible controls and restores focus to the gallery button that opened the modal.

  • Switch back to app.js in the open Visual Studio Code window.
  • Locate the closing brace of bindTilt() near the bottom of the PortfolioApp class.
  • Add this method immediately below bindTilt():
  bindGallery() {
    this.lightbox = this.querySelector("#certificate-lightbox");
    this.lightboxImage = this.querySelector("#lightbox-image");
    this.lightboxPlaceholder = this.querySelector("#lightbox-placeholder");
    this.lightboxTitle = this.querySelector("#lightbox-title");

    this.querySelectorAll("[data-certificate-index]").forEach((button) => {
      button.addEventListener("click", () => {
        this.lastGalleryTrigger = button;
        this.activeCertificateIndex = Number(button.dataset.certificateIndex);
        this.updateLightbox();
        this.lightbox.showModal();
      });
    });

    this.querySelector("#lightbox-close").addEventListener("click", () => this.lightbox.close());
  }

What does this code do?

  • The four stored element references give every lightbox action one consistent set of controls to update.
  • Each data-certificate-index value identifies the matching object in certifications.
  • The click handler records the selected button before showModal() opens the lightbox.
  • The Close button calls close() on the open dialog.

The method now exists, but the Web Component still needs to activate it after rendering the gallery.

  • Find this existing lifecycle method near the top of the PortfolioApp class:
  connectedCallback() {
    this.render();
    this.bindTilt();
  }

What happens in the current lifecycle?

The current method renders the portfolio before attaching pointer tilt. It does not activate the gallery controls.

  • Replace the existing connectedCallback() method with this updated version:
  connectedCallback() {
    this.render();
    this.bindTilt();
    this.bindGallery();
  }

Why call bindGallery here?

The call runs after render() creates the gallery buttons and dialog. This timing gives bindGallery() access to every element it needs.

Before you test the first connection, which certification name do you expect the modal to show?

  • Save app.js.
  • Refresh the local portfolio in Microsoft Edge.
  • Select any card in the Certification Gallery.

You should see the native lightbox open with the selected certification name and its styled image placeholder.

  • Select Close in the lightbox toolbar.

The modal closes and reveals the portfolio again.

Does the gallery card stay unresponsive?

Check that this.bindGallery(); sits inside connectedCallback() after this.render();.

Check that the new method remains inside the PortfolioApp class. A misplaced class brace prevents the browser from loading the script.

Help me debug a gallery button that does not open my native dialog.

Add certificate navigation and image switching

The selected array index acts as the lightbox cursor. Moving that index backward or forward changes the displayed certification without rebuilding the dialog.

The display logic also checks the empty image value. A placeholder appears today, while a future file path automatically switches the same stage to an image.

  • Inside bindGallery(), locate the listener for #lightbox-close.
  • Add these navigation and backdrop listeners immediately below that listener:
    this.querySelector("#lightbox-previous").addEventListener("click", () => this.moveLightbox(-1));
    this.querySelector("#lightbox-next").addEventListener("click", () => this.moveLightbox(1));

    this.lightbox.addEventListener("click", (event) => {
      if (event.target === this.lightbox) {
        this.lightbox.close();
      }
    });

How do these controls work?

  • The Previous button sends -1 to the shared movement method.
  • The Next button sends 1 to the same method.
  • The dialog closes when the click lands directly on its backdrop instead of its inner content.
  • Add these two methods immediately below bindGallery() inside the PortfolioApp class:
  moveLightbox(direction) {
    this.activeCertificateIndex = (this.activeCertificateIndex + direction + certifications.length) % certifications.length;
    this.updateLightbox();
  }

  updateLightbox() {
    const certificate = certifications[this.activeCertificateIndex];
    this.lightboxTitle.textContent = certificate.name;

    if (certificate.image) {
      this.lightboxImage.src = certificate.image;
      this.lightboxImage.alt = certificate.name;
      this.lightboxImage.hidden = false;
      this.lightboxPlaceholder.hidden = true;
    } else {
      this.lightboxImage.removeAttribute("src");
      this.lightboxImage.alt = "";
      this.lightboxImage.hidden = true;
      this.lightboxPlaceholder.hidden = false;
    }
  }

What does the display logic do?

  • The wrapped index keeps navigation inside the seven-item array when a visitor passes either end.
  • The selected certification name becomes the lightbox title.
  • A non-empty image path reveals the image with matching alternative text.
  • An empty image path removes any old source before revealing the placeholder.
  • Save app.js.
  • Refresh the local portfolio in Microsoft Edge.
  • Select the first gallery placeholder.
  • Select Next.

You should see the lightbox title change to the next certification while the placeholder remains visible.

  • Select Previous.

You should see the original certification name return.

  • Select the dark backdrop outside the lightbox panel.

You should see the dialog close without changing the selected certification data.

Add Images Without Redesigning

Later, place a local certificate file in the repository and add its path to the matching image value in certifications. The existing updateLightbox() method handles the switch from placeholder to image.

Does the title stay unchanged?

Check that both button listeners call moveLightbox() with different direction values.

Check that updateLightbox() reads from certifications[this.activeCertificateIndex] before updating the title.

Help me fix lightbox navigation that does not change the certification title.

Restore keyboard focus after closing

Keyboard navigation needs the same access as pointer navigation. The dialog listens for arrow keys while open, then returns focus to the exact gallery card that launched it.

  • Inside bindGallery(), locate the backdrop click listener.
  • Add these keyboard and close listeners immediately below the backdrop listener:
    this.lightbox.addEventListener("keydown", (event) => {
      if (event.key === "ArrowLeft") this.moveLightbox(-1);
      if (event.key === "ArrowRight") this.moveLightbox(1);
    });

    this.lightbox.addEventListener("close", () => {
      this.lastGalleryTrigger?.focus();
    });

How is keyboard context preserved?

  • The left and right arrow keys reuse the same movement method as the visible buttons.
  • The dialog handles Escape dismissal through its native modal behavior.
  • The close event focuses lastGalleryTrigger when that saved button exists.
  • Save app.js.

✔️ Awesome, I've got everything!

Your app.js now connects the seven gallery cards to a keyboard-accessible lightbox. Make sure the file is saved before the final test.

ⓧ I'd like to double check the full code

const certifications = [
  { name: "Agentic AI Foundations Associate \u2014 Oracle", image: "" },
  { name: "Introduction to Cybersecurity", image: "" },
  { name: "Google TechSpirit", image: "" },
  { name: "Cybersecurity Certification", image: "" },
  { name: "CEHv13 ISO", image: "" },
  { name: "Cisco Packet Tracer", image: "" },
  { name: "CompTIA Security+", image: "" }
];

const projects = [
  {
    number: "01",
    name: "AURUM",
    description: "A single-page AI tools platform prototype with chatbot, document generation, and workflow interfaces.",
    stack: ["HTML", "CSS", "JavaScript"],
    url: "https://github.com/taruncl-cloud/aurum-ai"
  },
  {
    number: "02",
    name: "FinEase",
    description: "A responsive landing-page concept for small-business finance tracking, reporting, and tax workflows.",
    stack: ["HTML", "CSS", "JavaScript"],
    url: "https://github.com/taruncl-cloud/finease-website"
  },
  {
    number: "03",
    name: "VITTrace",
    description: "A VIT-AP lost-and-found Android app with authentication, item reporting, search, image upload, and status tracking.",
    stack: ["Kotlin", "Jetpack Compose", "Firebase"],
    url: "https://github.com/taruncl-cloud/VITTrace"
  }
];

class PortfolioApp extends HTMLElement {
  constructor() {
    super();
    this.activeCertificateIndex = 0;
    this.lastGalleryTrigger = null;
  }

  connectedCallback() {
    this.render();
    this.bindTilt();
    this.bindGallery();
  }

  renderProject(project) {
    const stack = project.stack.map((item) => `<span>${item}</span>`).join("");

    return `
      <article class="project-card tilt-card" data-tilt>
        <div class="project-number">${project.number}</div>
        <h3>${project.name}</h3>
        <p>${project.description}</p>
        <div class="stack">${stack}</div>
        <a class="project-link" href="${project.url}" target="_blank" rel="noreferrer">View repository</a>
      </article>
    `;
  }

  renderGalleryCard(certificate, index) {
    const shortMark = String(index + 1).padStart(2, "0");

    return `
      <button class="gallery-card tilt-card" type="button" data-certificate-index="${index}" data-tilt aria-label="Open ${certificate.name} certificate placeholder">
        <span class="certificate-art" aria-hidden="true">
          <span class="placeholder-mark">${shortMark}</span>
        </span>
        <span class="gallery-caption">
          <strong>${certificate.name}</strong>
          <span>Image placeholder</span>
        </span>
      </button>
    `;
  }

  render() {
    const projectCards = projects.map((project) => this.renderProject(project)).join("");
    const certificationNames = certifications.map((certificate) => `<li>${certificate.name}</li>`).join("");
    const galleryCards = certifications.map((certificate, index) => this.renderGalleryCard(certificate, index)).join("");

    this.innerHTML = `
      <header class="site-header">
        <a class="brand" href="#home" aria-label="Tarun CL home">
          <span class="brand-mark">TC</span>
          <span>Tarun CL</span>
        </a>
        <nav class="nav-links" aria-label="Primary navigation">
          <a href="#about">About</a>
          <a href="#experience">Experience</a>
          <a href="#projects">Projects</a>
          <a href="#certifications">Certifications</a>
          <a href="#gallery">Gallery</a>
          <a href="#contact">Contact</a>
        </nav>
      </header>

      <main>
        <section class="page-section hero" id="home">
          <div>
            <p class="eyebrow">Cybersecurity, web, and Android development</p>
            <h1>Tarun <span>CL</span></h1>
            <p class="hero-copy">Final-year CSE (Cyber Security) student at VIT-AP, building practical web and mobile projects with a security-minded approach.</p>
            <div class="action-row">
              <a class="button primary" href="#projects">Explore projects</a>
              <a class="button" href="https://drive.google.com/file/d/1bT0JmgT3nRx-suxcMZvDJJkpOaui-ahx/view?usp=drivesdk" target="_blank" rel="noreferrer">Open resume</a>
            </div>
          </div>

          <div class="hero-visual" aria-label="Animated Tarun CL monogram">
            <div class="identity-card">
              <div class="identity-grid"></div>
              <div class="orbit"></div>
              <div class="orbit secondary"></div>
              <div class="monogram">TC</div>
              <div class="identity-caption">
                <span>Bengaluru, India</span>
                <span>Security and software</span>
              </div>
            </div>
          </div>
        </section>

        <div class="section-shell metric-rail" aria-label="Portfolio metrics">
          <div class="metric"><strong>10</strong><span>Public repositories</span></div>
          <div class="metric"><strong>7</strong><span>Certifications</span></div>
          <div class="metric"><strong>3</strong><span>Featured projects</span></div>
        </div>

        <section class="page-section" id="about">
          <div class="section-shell">
            <p class="eyebrow">Profile</p>
            <h2 class="section-title">A security-focused developer who learns by building.</h2>
            <div class="about-grid">
              <article class="panel">
                <h3>Education</h3>
                <p>Final-year Computer Science and Engineering student specializing in Cyber Security at VIT-AP.</p>
              </article>
              <article class="panel">
                <h3>Skills</h3>
                <ul class="skill-list">
                  <li>Cybersecurity</li>
                  <li>HTML</li>
                  <li>CSS</li>
                  <li>JavaScript</li>
                  <li>Kotlin</li>
                  <li>Jetpack Compose</li>
                  <li>Firebase</li>
                  <li>GitHub</li>
                </ul>
              </article>
            </div>
          </div>
        </section>

        <section class="page-section dark-band" id="experience">
          <div class="section-shell">
            <p class="eyebrow">Experience and education</p>
            <h2 class="section-title">Learning in public through focused technical projects.</h2>
            <p class="section-lead">This section avoids unsupported employers, dates, and job titles. It presents only the academic path and repository-backed project experience supplied for the portfolio.</p>
            <div class="timeline">
              <article class="timeline-row">
                <strong>Education</strong>
                <span>VIT-AP</span>
                <p>Final-year CSE (Cyber Security) student developing foundations across security, networking, and software development.</p>
              </article>
              <article class="timeline-row">
                <strong>Project experience</strong>
                <span>Independent development</span>
                <p>Built and published web prototypes and an Android application through the AURUM, FinEase, and VITTrace repositories.</p>
              </article>
            </div>
          </div>
        </section>

        <section class="page-section" id="projects">
          <div class="section-shell">
            <p class="eyebrow">Selected work</p>
            <h2 class="section-title">Projects that turn ideas into visible products.</h2>
            <p class="section-lead">Each card links directly to Tarun's public GitHub repository.</p>
            <div class="projects-grid">${projectCards}</div>
          </div>
        </section>

        <section class="page-section dark-band" id="certifications">
          <div class="section-shell">
            <p class="eyebrow">Credentials</p>
            <h2 class="section-title">Certifications</h2>
            <ul class="certification-list">${certificationNames}</ul>
          </div>
        </section>

        <section class="page-section" id="gallery">
          <div class="section-shell">
            <p class="eyebrow">Certification Gallery</p>
            <h2 class="section-title">A gallery ready for the original certificate images.</h2>
            <p class="section-lead">Select a placeholder to open the lightbox. Later, add an image file path to the matching certification object in `app.js`.</p>
            <div class="gallery-grid">${galleryCards}</div>
          </div>
        </section>

        <section class="page-section" id="contact">
          <div class="section-shell contact-panel">
            <div>
              <p class="eyebrow">Connect</p>
              <h2 class="section-title">Explore the work and professional profile.</h2>
            </div>
            <div class="contact-links">
              <a class="button primary" href="https://github.com/taruncl-cloud" target="_blank" rel="noreferrer">GitHub</a>
              <a class="button" href="https://www.linkedin.com/in/tarun-cl-96076031b/" target="_blank" rel="noreferrer">LinkedIn</a>
              <a class="button" href="https://drive.google.com/file/d/1bT0JmgT3nRx-suxcMZvDJJkpOaui-ahx/view?usp=drivesdk" target="_blank" rel="noreferrer">Resume</a>
            </div>
          </div>
        </section>
      </main>

      <footer class="site-footer">
        <span>Tarun CL</span>
        <span>Cybersecurity portfolio built with HTML, CSS, and JavaScript.</span>
      </footer>

      <dialog class="certificate-dialog" id="certificate-lightbox" aria-labelledby="lightbox-title">
        <div class="lightbox-toolbar">
          <button class="lightbox-close" id="lightbox-close" type="button" autofocus>Close</button>
        </div>
        <div class="lightbox-body">
          <div class="lightbox-stage">
            <img id="lightbox-image" alt="" hidden>
            <div class="lightbox-placeholder" id="lightbox-placeholder">
              <strong id="lightbox-title"></strong>
              <span>Certificate image placeholder</span>
            </div>
          </div>
          <div class="lightbox-nav">
            <button id="lightbox-previous" type="button">Previous</button>
            <button id="lightbox-next" type="button">Next</button>
          </div>
        </div>
      </dialog>
    `;
  }

  bindTilt() {
    this.querySelectorAll("[data-tilt]").forEach((card) => {
      card.addEventListener("pointermove", (event) => {
        const bounds = card.getBoundingClientRect();
        const horizontal = (event.clientX - bounds.left) / bounds.width - 0.5;
        const vertical = (event.clientY - bounds.top) / bounds.height - 0.5;
        card.style.setProperty("--tilt-x", `${vertical * -8}deg`);
        card.style.setProperty("--tilt-y", `${horizontal * 10}deg`);
      });

      card.addEventListener("pointerleave", () => {
        card.style.setProperty("--tilt-x", "0deg");
        card.style.setProperty("--tilt-y", "0deg");
      });
    });
  }

  bindGallery() {
    this.lightbox = this.querySelector("#certificate-lightbox");
    this.lightboxImage = this.querySelector("#lightbox-image");
    this.lightboxPlaceholder = this.querySelector("#lightbox-placeholder");
    this.lightboxTitle = this.querySelector("#lightbox-title");

    this.querySelectorAll("[data-certificate-index]").forEach((button) => {
      button.addEventListener("click", () => {
        this.lastGalleryTrigger = button;
        this.activeCertificateIndex = Number(button.dataset.certificateIndex);
        this.updateLightbox();
        this.lightbox.showModal();
      });
    });

    this.querySelector("#lightbox-close").addEventListener("click", () => this.lightbox.close());
    this.querySelector("#lightbox-previous").addEventListener("click", () => this.moveLightbox(-1));
    this.querySelector("#lightbox-next").addEventListener("click", () => this.moveLightbox(1));

    this.lightbox.addEventListener("click", (event) => {
      if (event.target === this.lightbox) {
        this.lightbox.close();
      }
    });

    this.lightbox.addEventListener("keydown", (event) => {
      if (event.key === "ArrowLeft") this.moveLightbox(-1);
      if (event.key === "ArrowRight") this.moveLightbox(1);
    });

    this.lightbox.addEventListener("close", () => {
      this.lastGalleryTrigger?.focus();
    });
  }

  moveLightbox(direction) {
    this.activeCertificateIndex = (this.activeCertificateIndex + direction + certifications.length) % certifications.length;
    this.updateLightbox();
  }

  updateLightbox() {
    const certificate = certifications[this.activeCertificateIndex];
    this.lightboxTitle.textContent = certificate.name;

    if (certificate.image) {
      this.lightboxImage.src = certificate.image;
      this.lightboxImage.alt = certificate.name;
      this.lightboxImage.hidden = false;
      this.lightboxPlaceholder.hidden = true;
    } else {
      this.lightboxImage.removeAttribute("src");
      this.lightboxImage.alt = "";
      this.lightboxImage.hidden = true;
      this.lightboxPlaceholder.hidden = false;
    }
  }
}

customElements.define("portfolio-app", PortfolioApp);

How to use this reference

Compare the methods near the bottom of this reference with your saved app.js. The order of the handlers matters because every selector must run after render() creates the dialog.

Before the final check, where do you expect focus to return after you dismiss the lightbox?

  • Refresh the local portfolio in Microsoft Edge.
  • Press Tab until a gallery card receives the visible focus outline.
  • Press Enter to open the focused gallery card.

You should see the lightbox open with the matching certification name and placeholder.

  • Press the Left Arrow key.
  • Press the Right Arrow key.

You should see the title move to the previous certification before returning to the original certification.

  • Press Escape.

You should see the modal close. The visible focus outline returns to the gallery card that opened it.

  • Press Enter to reopen the focused gallery card.
  • Select Close.

You should see the same focus restoration after using the visible Close button. Your seven-item gallery now supports pointer controls, keyboard controls, native Escape dismissal, backdrop dismissal, and future certificate images.

Does focus disappear after closing?

Check that the gallery click handler assigns the selected button to this.lastGalleryTrigger before opening the modal.

Check that the close listener calls this.lastGalleryTrigger?.focus().

Help me restore focus to the gallery button after my dialog closes.

You did it. The local portfolio now gives every certification a focused viewing experience without requiring the final image files.

Your complete portfolio now works locally from hero to lightbox. Next, you will publish it through GitHub Pages so recruiters can open it from a public URL.

Publish the Portfolio with GitHub Pages

Your complete portfolio already works locally in Microsoft Edge. Recruiters still need a public address they can open without your computer.

GitHub Desktop gives you a visual Git workflow for recording the finished source in a public repository. GitHub Pages serves that repository as the live portfolio.

In this step, get ready to:
  • Save the complete portfolio as a commit on the main branch.
  • Publish the main branch through GitHub Pages.
  • Prove the live deployment matches the local portfolio.
Commit and publish the repository

A commit is a named snapshot of your complete source. Publishing connects that snapshot to a remote repository that GitHub Pages can reach.

  • Switch back to GitHub Desktop from earlier.
  • Review the file list in GitHub Desktop.
  • Confirm the list contains index.html, styles.css, and app.js.
  • Confirm the list contains no unrelated changes.
  • Enter Publish complete portfolio as the concise commit summary.
  • Click Commit to main.

The file list clears after the commit. Your local main branch now contains the finished portfolio snapshot.

Publishing makes the repository files visible to anyone. Your current certificate entries use empty image paths, so no private certificate IDs are included.

✔️ I see Publish repository

  • Click Publish repository in GitHub Desktop.
  • Confirm the repository name is taruncl-cloud.github.io.
  • Set the repository visibility to public.
  • Click Publish repository in the publish window.

The public repository appears in your GitHub account. GitHub Desktop remains connected to it for future updates.

ⓧ I see Push origin

The remote repository is already connected. Your new commit only needs to be uploaded.

  • Click Push origin in GitHub Desktop.

GitHub Desktop finishes the push and shows no pending commits. The public repository now contains the complete source.

Repository not publishing?

  • Confirm GitHub Desktop still shows main as the current branch.
  • Confirm your signed-in GitHub account owns the repository.
  • Retry publishing after your internet connection is stable.

Help me fix a GitHub Desktop repository that will not publish or push.

Connect the main branch to GitHub Pages

GitHub Pages can serve this site's HTML, CSS, and JavaScript directly from a repository branch. The source settings point Pages at main.

The / (root) folder contains all three source files. This keeps the deployment aligned with the local repository structure.

  • In Microsoft Edge, visit https://github.com/taruncl-cloud.
  • Select the taruncl-cloud.github.io repository from the profile.
  • Click Settings on the repository page.
  • Select Pages in the repository settings.
  • Choose Deploy from a branch as the publishing source.
  • Select main as the deployment branch.
  • Select / (root) as the deployment folder.
  • Click Save.

The Pages settings now show main with / (root) as the deployment source. This configuration publishes the three source files from the repository root.

The first publication can take up to 10 minutes. A waiting status during that window is normal.

Pages source not saving?

  • Confirm the repository visibility is public.
  • Confirm the Pages source shows main.
  • Confirm the deployment folder shows / (root).

Help me troubleshoot my GitHub Pages branch publishing settings.

Verify the live portfolio

Before you open the live site, do you expect the deployed page to match the local preview?

  • In Microsoft Edge, open https://taruncl-cloud.github.io.

You'll see the Tarun CL hero with the warm-white visual system. The page also shows three project cards and seven certification placeholders.

Live site not ready?

  • Wait up to 10 minutes for the first deployment.
  • Refresh the live URL in Microsoft Edge.
  • Return to the Pages settings from earlier.
  • Confirm the source remains main.
  • Confirm the folder remains / (root).

Help me find why my GitHub Pages portfolio is not available at its public URL.

The public page is now your recruiter-facing version. Check each destination before sharing it.

  • Select GitHub in the contact section.
  • Return to the live portfolio tab.
  • Select LinkedIn in the contact section.
  • Return to the live portfolio tab.
  • Select Resume in the contact section.
  • Return to the live portfolio tab.

The GitHub destination opens Tarun's public profile. The LinkedIn destination opens Tarun's supplied professional profile.

The resume destination opens the supplied Google Drive document.

  • Select View repository on the AURUM card.
  • Return to the live portfolio tab.
  • Select View repository on the FinEase card.
  • Return to the live portfolio tab.
  • Select View repository on the VITTrace card.
  • Return to the live portfolio tab.

Each project link opens its matching public repository.

  • Select the first certification placeholder.

The certification lightbox opens with the matching name. The placeholder remains visible because its image path is empty.

  • Click Previous.
  • Click Next.
  • Click Close.
  • Select another certification placeholder.
  • Press Escape.

Previous and Next change the selected certification name. Close and Escape dismiss the lightbox.

Use the same update loop

Every future portfolio update follows the same workflow. GitHub Pages deploys each new commit from the saved source settings.

  • Switch back to Visual Studio Code from earlier.
  • Correct the affected source file.
  • Save the file you changed.
  • Switch back to GitHub Desktop.
  • Enter a concise commit summary.
  • Click Commit to main.
  • Click Push origin.
  • Refresh the live portfolio after the deployment completes.

That completes the public handoff. Your live portfolio now matches the finished local version.

Secret mission

Prove the Portfolio Is Accessible

The portfolio is live. Now audit it like a quality engineer using keyboard-only navigation, reduced motion, and a narrow mobile viewport.

Clean Up Your Resources

Clean Up Your Resources

All resources in this project cost $0 on the public hosting path you used. Any certificate ID added later becomes visible to visitors.

Resources you used:

  • Local Git repository folder taruncl-cloud.github.io with your source files and commit history.
  • Public GitHub repository taruncl-cloud.github.io containing index.html, styles.css, and app.js on branch main.
  • GitHub Pages deployment published from main and / (root) at https://taruncl-cloud.github.io.

Keep everything running

No action is needed while you are still sharing the portfolio. This choice keeps every copy available.

  • Continue editing index.html, styles.css, and app.js in Visual Studio Code.
  • Commit future changes through GitHub Desktop.
  • Leave the public GitHub repository available for recruiters.
  • Leave the GitHub Pages deployment active at https://taruncl-cloud.github.io.
  • Review each future certificate image for private identifiers before adding it.

Pause - I'll come back to this later

Pausing takes the portfolio offline while preserving the local repository. The public GitHub repository also remains intact.

  • Return to the taruncl-cloud.github.io repository page in Microsoft Edge.
  • Select Settings.
  • Select Pages.
  • Open the deployment menu for the active site.
  • Select Unpublish site.

Your portfolio is safely offline now. Both repository copies still hold the source.

  • Close Visual Studio Code to free its memory.
  • Close GitHub Desktop to free its memory.
  • Close Microsoft Edge when you finish checking the unpublished site.

Your files remain ready for a future GitHub Pages publication.

Delete - I don't want to use this again

Deletion removes the live site plus both repository copies. The local copy stays available until the final folder removal, giving you a last chance to save it.

  • Return to the taruncl-cloud.github.io repository page in Microsoft Edge.
  • Select Settings.
  • Select Pages.
  • Open the deployment menu for the active site.
  • Select Unpublish site.

The public deployment is now removed. Your files remain available during the next cleanup decision.

  • Select General in the repository settings.
  • Scroll to Danger Zone.
  • Select Delete this repository.
  • Complete GitHub's confirmation flow.

The irreversible online cleanup is complete. The local source still remains on your computer.

  • Close Visual Studio Code.
  • Close GitHub Desktop.
  • Press the Windows key to open search.
  • Type File Explorer in the search field.
  • Press Enter to open File Explorer.
  • Search for the taruncl-cloud.github.io folder.
  • Open the matching folder.

You should see index.html, styles.css, and app.js inside. These files confirm that you found the correct local repository.

  • Go back to the folder that contains taruncl-cloud.github.io.
  • Select the taruncl-cloud.github.io folder.
  • Permanently delete the selected folder by pressing Shift+Delete.
  • Confirm the Windows deletion prompt.

Your local repository is now removed. All project resources have been cleaned up.

Nice Work!

Nice Work!

You made it! Tarun CL's recruiter-facing 3D portfolio is live through GitHub Pages.

What you learned:

  • Build a responsive 3D portfolio with semantic HTML. Render its content through an autonomous Web Component.
  • Create a seven-item certificate gallery with replaceable placeholders. Present each certificate through a keyboard-accessible native dialog.
  • Add pointer interactions through CSS 3D transforms. Preserve the complete experience with responsive layouts plus reduced-motion fallbacks.
  • Publish a public portfolio through GitHub Pages using a visual Git workflow.
  • Secret Mission: Complete an accessibility audit using keyboard-only navigation. Verify reduced motion plus narrow-screen usability on the live site.

Ready to quiz yourself?