Sanskrit Learning Web App with PHP

Build a password-protected app to teach Bhagavad Gita Chapter 16 slokas.

Introduction

30 Second Summary

Teaching Sanskrit grammar requires sharing structured notes with students. Whether it's word-by-word meanings, sandhi analysis, or prose word order, students need easy access to organized content they can revisit anytime.

In this project, you will build a password-protected web app that serves Bhagavad Gita Chapter 16 slokas to your students in a clean, navigable format with full grammatical breakdowns.

What You'll Build

A PHP-powered web app where students log in and navigate through all 24 slokas of Chapter 16 (Daivasura Sampad Vibhaga Yoga) with tabbed grammatical analysis.

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

  • A PHP session-based login system protecting your content with a shared student password.
  • A JSON-driven content architecture storing sloka data (original text, sandhi vigraha, anvaya, word meanings, and more) in one editable file.
  • A tabbed, responsive interface displaying each sloka's grammatical breakdown with proper Devanagari web fonts.
  • Secret Mission: Add a localStorage-based progress tracker so students can mark slokas as studied and see their completion status.

Are there any prerequisites?

You should have basic HTML familiarity and access to a PHP-capable web server (local or remote). No database is needed for this project.

Before We Start

To build your Sanskrit learning web app, you need a code editor that understands PHP and a well-organized project folder.

In this step, you will install VS Code with PHP support, create all the files your project needs, and generate a password hash that will protect your app from unauthorized access.

In this step, get ready to:
  • Install VS Code and add the PHP Intelephense extension.
  • Create the project folder and all required files.
  • Generate a bcrypt password hash for student authentication.
Install VS Code and add PHP support

VS Code is a free code editor that gives you syntax highlighting, error detection, and extensions for any language. The PHP Intelephense extension adds smart autocomplete and error checking for PHP files.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to check if it opens.

✔️ VS Code opened

You already have VS Code installed. Skip ahead to the extension step below.

ⓧ I don't have VS Code

  • Open your browser and go to https://code.visualstudio.com/.
  • Click the download button for your operating system.
  • On macOS, open the downloaded .dmg file and drag VS Code to your Applications folder.
  • On Windows, run the installer and follow the prompts.
  • Once installed, open VS Code using the search method described above.

Download not working?

Make sure you are downloading from the official site at code.visualstudio.com. If your browser blocks the download, check your Downloads folder or try a different browser.

Help me install VS Code on my computer

Now install the PHP Intelephense extension to get PHP code intelligence.

  • In VS Code, open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type PHP Intelephense in the search bar.
  • Click Install on the extension by Ben Mewburn.

What does PHP Intelephense do?

This extension gives VS Code the ability to autocomplete PHP functions, detect syntax errors before you run your code, and navigate between files. It makes writing PHP much faster and less error-prone.

Create the project folder structure

Your app needs a specific set of files organized in one folder. You will create this folder on your Desktop so you can find it easily later.

  • Open your terminal by pressing Cmd+Space (macOS) and typing Terminal, or pressing the Windows key (Windows) and typing cmd.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create the project folder and move into it by running these commands:
mkdir gita-app
cd gita-app
  • Create the data subfolder and all the project files by running these commands:
mkdir data
touch config.php login.php logout.php index.php style.css script.js data/chapter16.json
  • Confirm the files exist by running:
ls

You should see config.php, data, index.php, login.php, logout.php, script.js, and style.css in the output.

Don't see all the files?

Make sure you ran the touch command from inside the gita-app folder. If you see "No such file or directory" for the data/chapter16.json file, confirm the data folder was created first with mkdir data.

On Windows, use type nul > instead of touch if the command is not recognized.

Help me create my project files

Now open this folder in VS Code so you can start editing.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the gita-app folder, and click Open.

You should see all your files listed in the Explorer panel on the left side of VS Code.

Generate your password hash

Your app will use a shared student password for login. Instead of storing the password as plain text (which is a security risk), you will store a one-way hash. PHP can then verify login attempts against this hash without ever exposing the real password.

  • In VS Code, click File then New File.
  • Save this new file as generate_hash.php inside your gita-app folder.
  • Paste the following code into generate_hash.php:
<?php
echo password_hash('your-chosen-password', PASSWORD_DEFAULT);
?>

What does this code do?

The password_hash() function takes a plain-text password and produces a bcrypt hash. This hash is a long string of random-looking characters that cannot be reversed back into the original password.

Replace your-chosen-password with the actual password you want your students to use for logging in.

  • Replace your-chosen-password with the password you want students to use.
  • Save the file.
  • In your terminal, make sure you are inside the gita-app folder, then run this command:
php generate_hash.php

You should see a long string starting with $2y$10$ printed in your terminal. This is your bcrypt hash.

  • Copy this entire hash string and save it somewhere safe (a text file or a note). You will use it in the next step.

Seeing 'command not found' for php?

This means PHP is not in your system PATH. Make sure your PHP server installation includes the PHP CLI (command-line interface). On macOS, you may need to install PHP via Homebrew. On Windows, ensure the PHP directory is added to your system PATH.

Help me fix my PHP CLI setup

  • Now delete the temporary file by running this command in your terminal:
rm generate_hash.php
  • Confirm it is gone by running ls again. You should no longer see generate_hash.php in the file list.

Why delete generate_hash.php?

This file contains your password in plain text. Leaving it on your server would let anyone who finds the file see the password. Since you already have the hash, the file has served its purpose and should be removed.

Your environment is ready. Next up, you will build the login system that uses your password hash to protect the app.

Build the Login System

Your project folder is set up with all the files you need. Now it is time to build the authentication system that protects your content from unauthorized access.

In this step, you will create a shared-password login system using PHP sessions. Every student enters the same class password to access the sloka content. No database is needed because the password hash lives directly in a config file.

In this step, get ready to:
  • Create a shared config file that manages authentication state.
  • Build a login page with password verification.
  • Create a logout handler and test the full login flow.
Create the authentication config file

Every PHP page in your app needs to know whether the current visitor is logged in. A single config file handles this by starting a session and providing helper functions that other pages can call.

  • Switch back to VS Code with your gita-app project open.
  • Open config.php and paste the following code:
<?php
session_start();

// Store the bcrypt hash of your student password
define('PASSWORD_HASH', '$2y$10$YOUR_GENERATED_HASH_HERE');

// Check if the current session is authenticated
function isAuthenticated() {
    return isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true;
}

// Redirect unauthenticated visitors to the login page
function requireAuth() {
    if (!isAuthenticated()) {
        header('Location: login.php');
        exit;
    }
}
?>

What does this code do?

This file is the backbone of your authentication system. Every other PHP file will include it at the top.

  • session_start() begins or resumes a PHP session so you can store data (like login status) across page loads.
  • PASSWORD_HASH stores the bcrypt hash you generated in Step 1. PHP's password_verify() will compare form input against this hash.
  • isAuthenticated() returns true only if the session has been marked as logged in.
  • requireAuth() is a guard function. Any page that calls it will immediately redirect unauthenticated visitors to the login page.
  • Replace $2y$10$YOUR_GENERATED_HASH_HERE with the hash string you generated in Step 1. Keep the surrounding single quotes.
  • Save config.php.

Seeing a headers already sent error?

Make sure there are no blank lines or spaces before the opening <?php tag in config.php. PHP sessions require headers to be sent before any output, and even a single space counts as output.

Help me fix my headers already sent error in PHP.

✔️ Awesome, I've got everything!

Great. Make sure you have saved config.php with your actual hash pasted in.

ⓧ I'd like to double check the full code

<?php
session_start();

define('PASSWORD_HASH', '$2y$10$YOUR_GENERATED_HASH_HERE');

function isAuthenticated() {
    return isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true;
}

function requireAuth() {
    if (!isAuthenticated()) {
        header('Location: login.php');
        exit;
    }
}
?>
Build the login page

The login page has two jobs. First, it processes the form submission and checks the password. Second, it displays the login form to the visitor. Both live in one file because PHP can handle logic and HTML together.

  • Open login.php and paste the PHP processing logic at the top of the file:
<?php
require_once 'config.php';

// If already logged in, skip straight to the main page
if (isAuthenticated()) {
    header('Location: index.php');
    exit;
}

$error = '';

// Handle form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $password = $_POST['password'] ?? '';
    if (password_verify($password, PASSWORD_HASH)) {
        // Correct password - mark session as authenticated
        $_SESSION['authenticated'] = true;
        header('Location: index.php');
        exit;
    } else {
        $error = 'Incorrect password. Please try again.';
    }
}
?>

What does this code do?

This block runs on the server before any HTML is sent to the browser.

  • require_once 'config.php' loads your session and helper functions.
  • The first if block redirects already-authenticated visitors straight to index.php so they skip the login form.
  • password_verify() compares the submitted password against the stored bcrypt hash. It is timing-attack safe, so attackers cannot guess the password by measuring response times.
  • On success, $_SESSION['authenticated'] is set to true and the visitor is redirected.
  • Directly below the closing ?> tag, paste the HTML template for the login form:
<!DOCTYPE html>
<html lang="sa">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login - Bhagavad Gita Chapter 16</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;700&family=Tiro+Devanagari+Sanskrit&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>
<body class="login-page">
    <div class="login-container">
        <h1 class="login-title">श्रीमद्भगवद्गीता</h1>
        <p class="login-subtitle">अध्यायः १६ - दैवासुरसम्पद्विभागयोगः</p>
        <?php if ($error): ?>
            <p class="error-message"><?php echo htmlspecialchars($error); ?></p>
        <?php endif; ?>
        <form method="POST" class="login-form">
            <label for="password">कृपया प्रवेशसङ्केतं ददातु</label>
            <input type="password" id="password" name="password" required autofocus placeholder="Password">
            <button type="submit">प्रविशतु</button>
        </form>
    </div>
</body>
</html>

What does the HTML template do?

This template renders the login form your students will see.

  • The <link> tags in the head preconnect to Google Fonts and load Tiro Devanagari Sanskrit and Noto Sans Devanagari for proper Devanagari rendering.
  • The <?php if ($error): ?> block conditionally shows an error message when the password is wrong.
  • htmlspecialchars() sanitizes the error output to prevent cross-site scripting.
  • The form uses method="POST" so the password is not visible in the URL.
  • Save login.php.
  • Visit login.php in your browser. You should see a centered login form with the Sanskrit title and a password field.

Why does the page look unstyled?

The HTML references style.css which is still an empty file. The form will function correctly for authentication, but it will not look polished until you add CSS in Step 5.

Not seeing the login form?

Check that login.php is saved in the gita-app folder on your PHP server. Make sure your browser URL points to the correct server path.

If you see a blank page, check your PHP error log. A typo in the require_once line or a missing config.php file will cause a fatal error.

Help me debug my PHP login page not loading.

✔️ Awesome, I've got everything!

Great. Make sure you have saved login.php and can see the form in your browser.

ⓧ I'd like to double check the full code

<?php
require_once 'config.php';

if (isAuthenticated()) {
    header('Location: index.php');
    exit;
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $password = $_POST['password'] ?? '';
    if (password_verify($password, PASSWORD_HASH)) {
        $_SESSION['authenticated'] = true;
        header('Location: index.php');
        exit;
    } else {
        $error = 'Incorrect password. Please try again.';
    }
}
?>
<!DOCTYPE html>
<html lang="sa">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login - Bhagavad Gita Chapter 16</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;700&family=Tiro+Devanagari+Sanskrit&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>
<body class="login-page">
    <div class="login-container">
        <h1 class="login-title">श्रीमद्भगवद्गीता</h1>
        <p class="login-subtitle">अध्यायः १६ - दैवासुरसम्पद्विभागयोगः</p>
        <?php if ($error): ?>
            <p class="error-message"><?php echo htmlspecialchars($error); ?></p>
        <?php endif; ?>
        <form method="POST" class="login-form">
            <label for="password">कृपया प्रवेशसङ्केतं ददातु</label>
            <input type="password" id="password" name="password" required autofocus placeholder="Password">
            <button type="submit">प्रविशतु</button>
        </form>
    </div>
</body>
</html>
Create the logout handler and test the system

The logout handler destroys the active session and sends the visitor back to the login page. It is the simplest file in the project.

  • Open logout.php and paste the following code:
<?php
require_once 'config.php';
// Destroy the session and send visitor back to login
session_destroy();
header('Location: login.php');
exit;
?>

What does this code do?

  • session_destroy() removes all session data, including the authenticated flag you set during login.
  • The header('Location: login.php') redirect immediately sends the visitor back to the login form.
  • Save logout.php.
  • Test the full login flow in your browser:
  • Visit login.php and enter an incorrect password. You should see the error message "Incorrect password. Please try again." appear on the page.
  • Enter your correct password. You should be redirected to index.php (which will show a blank page for now since you have not built it yet).
  • Visit logout.php directly. You should be redirected back to the login form.
  • Try visiting index.php without logging in. It should show a blank page because there is no requireAuth() call in it yet. That comes in Step 4.

Wrong password is not showing the error?

Double-check that the hash in config.php matches what generate_hash.php produced. Even a single missing character will cause password_verify() to fail silently.

If the page just refreshes with no message, check that your form has method="POST" (not GET). The PHP logic only processes POST requests.

Help me debug my login form error message.

✔️ Awesome, I've got everything!

Great. Make sure logout.php is saved and the full login/logout flow works in your browser.

ⓧ I'd like to double check the full code

<?php
require_once 'config.php';
session_destroy();
header('Location: login.php');
exit;
?>

Your login system is working. Students can authenticate with the shared password and their session stays active as they browse. Next up, you will create the JSON data file that holds all the sloka content for Chapter 16.

Create the Sloka Data

Your login system is working. Students can now authenticate and access the app. The next challenge is giving them something to see once they get in.

In this step, you will build the JSON data file that holds all 24 slokas of Chapter 16. This file is the content backbone of your entire app.

In this step, get ready to:
  • Design a JSON schema with fields for each grammatical breakdown (anvaya, sandhi vigraha, padartha, bhava spashtikaranam, dhaturoopa, samasa).
  • Populate the first slokas with full text data from sanskritdocuments.org.
  • Create placeholder entries for the remaining slokas you will fill in as you prepare class content.
Create the chapter data file

Instead of hardcoding sloka text directly into PHP, you will store all content in a single JSON file. This separation means you can update your teaching material by editing one data file without touching any code.

  • In VS Code, open the file data/chapter16.json inside your gita-app folder.
  • Paste the following JSON content into the file:
{
    "chapter": 16,
    "title": "दैवासुरसम्पद्विभागयोगः",
    "titleSandhi": "दैव-आसुर-सम्पत्-विभाग-योगः",
    "slokas": [
        {
            "number": 1,
            "original": "अभयं सत्त्वसंशुद्धिर्ज्ञानयोगव्यवस्थितिः ।\nदानं दमश्च यज्ञश्च स्वाध्यायस्तप आर्जवम् ॥ १६-१॥",
            "sandhiVigraha": "अभयम् सत्त्व-संशुद्धिः ज्ञान-योग-व्यवस्थितिः ।\nदानम् दमः च यज्ञः च स्वाध्यायः तपः आर्जवम् ॥ १६-१॥",
            "anvaya": "अभयम्, सत्त्व-संशुद्धिः, ज्ञान-योग-व्यवस्थितिः, दानम्, दमः च यज्ञः च, स्वाध्यायः, तपः, आर्जवम्,",
            "padartha": [
                {"word": "अभयम्", "transliteration": "abhayam", "meaning": "fearlessness"},
                {"word": "सत्त्व-संशुद्धिः", "transliteration": "sattva-samshuddhih", "meaning": "purity of mind"},
                {"word": "ज्ञान-योग-व्यवस्थितिः", "transliteration": "jnana-yoga-vyavasthitih", "meaning": "steadfastness in knowledge and yoga"},
                {"word": "दानम्", "transliteration": "danam", "meaning": "charity"},
                {"word": "दमः", "transliteration": "damah", "meaning": "self-control"},
                {"word": "यज्ञः", "transliteration": "yajnah", "meaning": "sacrifice"},
                {"word": "स्वाध्यायः", "transliteration": "svadhyayah", "meaning": "study of scriptures"},
                {"word": "तपः", "transliteration": "tapah", "meaning": "austerity"},
                {"word": "आर्जवम्", "transliteration": "arjavam", "meaning": "straightforwardness"}
            ],
            "bhavaSpashtikaranam": [
                {"prashna": "अत्र भगवान् किं वर्णयति?", "uttaram": "भगवान् दैवीं सम्पदम् अभिजातस्य पुरुषस्य लक्षणानि वर्णयति।"},
                {"prashna": "प्रथमं लक्षणं किम्?", "uttaram": "प्रथमं लक्षणम् अभयम् अस्ति। आत्मज्ञानी पुरुषस्य कुतोऽपि भयं न भवति।"}
            ],
            "dhaturoopa": [],
            "samasa": [
                {"compound": "सत्त्वसंशुद्धिः", "breakdown": "सत्त्वस्य संशुद्धिः", "type": "तत्पुरुषः (षष्ठी)"},
                {"compound": "ज्ञानयोगव्यवस्थितिः", "breakdown": "ज्ञाने योगे च व्यवस्थितिः", "type": "तत्पुरुषः (सप्तमी)"}
            ]
        },
        {"number": 2, "original": "अहिंसा सत्यमक्रोधस्त्यागः शान्तिरपैशुनम् ।\nदया भूतेष्वलोलुप्त्वं मार्दवं ह्रीरचापलम् ॥ १६-२॥", "sandhiVigraha": "अहिंसा सत्यम् अक्रोधः त्यागः शान्तिः अपैशुनम् ।\nदया भूतेषु अलोलुप्त्वम् मार्दवम् ह्रीः अचापलम् ॥ १६-२॥", "anvaya": "अहिंसा, सत्यम्, अक्रोधः, त्यागः, शान्तिः, अपैशुनम्, भूतेषु दया, अलोलुप्त्वम्, मार्दवम्, ह्रीः, अचापलम्,", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 3, "original": "तेजः क्षमा धृतिः शौचमद्रोहो नातिमानिता ।\nभवन्ति सम्पदं दैवीमभिजातस्य भारत ॥ १६-३॥", "sandhiVigraha": "तेजः क्षमा धृतिः शौचम् अद्रोहः न अति-मानिता ।\nभवन्ति सम्पदम् दैवीम् अभिजातस्य भारत ॥ १६-३॥", "anvaya": "हे भारत! तेजः, क्षमा, धृतिः, शौचम्, अद्रोहः, न अति-मानिता (इति एतानि लक्षणानि) दैवीम् सम्पदम् अभिजातस्य (पुरुषस्य) भवन्ति ।", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 4, "original": "दम्भो दर्पोऽभिमानश्च क्रोधः पारुष्यमेव च ।\nअज्ञानं चाभिजातस्य पार्थ सम्पदमासुरीम् ॥ १६-४॥", "sandhiVigraha": "दम्भः दर्पः अभिमानः च क्रोधः पारुष्यम् एव च ।\nअज्ञानम् च अभिजातस्य पार्थ सम्पदम् आसुरीम् ॥ १६-४॥", "anvaya": "हे पार्थ! दम्भः, दर्पः, अभिमानः च, क्रोधः, पारुष्यम्, एव च अज्ञानम् च (एतानि लक्ष्णानि) आसुरीम् सम्पदम् अभिजातस्य (पुरुषस्य भवन्ति) ।", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 5, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 6, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 7, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 8, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 9, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 10, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 11, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 12, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 13, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 14, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 15, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 16, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 17, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 18, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 19, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 20, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 21, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 22, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 23, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 24, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []}
    ]
}

What does this JSON structure contain?

The top-level fields (chapter, title, titleSandhi) identify the chapter. The slokas array holds all 24 entries.

Each sloka object can contain: original (the sloka with sandhi), sandhiVigraha (word separation), anvaya (prose word order), padartha (word-by-word meaning), bhavaSpashtikaranam (Q&A meaning exploration), dhaturoopa (verb analysis), and samasa (compound analysis).

Sloka 1 is your fully populated template with all field types filled in. Slokas 2-4 have the core text (original, sandhi vigraha, anvaya) but empty arrays for the detailed breakdowns. Slokas 5-24 are pure placeholders you will fill in as you prepare each class.

  • Save data/chapter16.json.
  • Check that VS Code shows no red underlines in the file. A green or clean editor gutter confirms the JSON is valid.

Seeing red underlines in the JSON?

  • Check for trailing commas after the last item in an array or object. JSON does not allow trailing commas.
  • Make sure all strings are wrapped in double quotes, not single quotes.
  • Verify that every opening brace or bracket has a matching closing one. A missing } or ] will cause errors across the rest of the file.

Help me fix JSON syntax errors in my chapter16.json file.

Understand the sloka template pattern

Sloka 1 serves as your complete template. It demonstrates every field type your app will render.

How do I add content for more slokas?

The padartha array uses objects with word, transliteration, and meaning keys. Copy the pattern from sloka 1 and fill in each word from the sloka you are preparing.

The bhavaSpashtikaranam array uses objects with prashna and uttaram keys. Add as many Q&A pairs as you need for each sloka's meaning exploration.

The samasa array uses compound, breakdown, and type keys. You only need entries for slokas that contain compound words worth analyzing.

Why JSON instead of a database?

A database would require additional server setup and SQL knowledge. JSON gives you a flat file you can edit in any text editor, share with colleagues, or version control with Git. For a single chapter of 24 slokas, this approach keeps things simple and portable.

✔️ Awesome, I've got everything!

Great. Double check you have saved data/chapter16.json with no red underlines in VS Code.

ⓧ I'd like to double check the full code

Here is the complete data/chapter16.json file. Make sure your file matches this exactly:

{
    "chapter": 16,
    "title": "दैवासुरसम्पद्विभागयोगः",
    "titleSandhi": "दैव-आसुर-सम्पत्-विभाग-योगः",
    "slokas": [
        {
            "number": 1,
            "original": "अभयं सत्त्वसंशुद्धिर्ज्ञानयोगव्यवस्थितिः ।\nदानं दमश्च यज्ञश्च स्वाध्यायस्तप आर्जवम् ॥ १६-१॥",
            "sandhiVigraha": "अभयम् सत्त्व-संशुद्धिः ज्ञान-योग-व्यवस्थितिः ।\nदानम् दमः च यज्ञः च स्वाध्यायः तपः आर्जवम् ॥ १६-१॥",
            "anvaya": "अभयम्, सत्त्व-संशुद्धिः, ज्ञान-योग-व्यवस्थितिः, दानम्, दमः च यज्ञः च, स्वाध्यायः, तपः, आर्जवम्,",
            "padartha": [
                {"word": "अभयम्", "transliteration": "abhayam", "meaning": "fearlessness"},
                {"word": "सत्त्व-संशुद्धिः", "transliteration": "sattva-samshuddhih", "meaning": "purity of mind"},
                {"word": "ज्ञान-योग-व्यवस्थितिः", "transliteration": "jnana-yoga-vyavasthitih", "meaning": "steadfastness in knowledge and yoga"},
                {"word": "दानम्", "transliteration": "danam", "meaning": "charity"},
                {"word": "दमः", "transliteration": "damah", "meaning": "self-control"},
                {"word": "यज्ञः", "transliteration": "yajnah", "meaning": "sacrifice"},
                {"word": "स्वाध्यायः", "transliteration": "svadhyayah", "meaning": "study of scriptures"},
                {"word": "तपः", "transliteration": "tapah", "meaning": "austerity"},
                {"word": "आर्जवम्", "transliteration": "arjavam", "meaning": "straightforwardness"}
            ],
            "bhavaSpashtikaranam": [
                {"prashna": "अत्र भगवान् किं वर्णयति?", "uttaram": "भगवान् दैवीं सम्पदम् अभिजातस्य पुरुषस्य लक्षणानि वर्णयति।"},
                {"prashna": "प्रथमं लक्षणं किम्?", "uttaram": "प्रथमं लक्षणम् अभयम् अस्ति। आत्मज्ञानी पुरुषस्य कुतोऽपि भयं न भवति।"}
            ],
            "dhaturoopa": [],
            "samasa": [
                {"compound": "सत्त्वसंशुद्धिः", "breakdown": "सत्त्वस्य संशुद्धिः", "type": "तत्पुरुषः (षष्ठी)"},
                {"compound": "ज्ञानयोगव्यवस्थितिः", "breakdown": "ज्ञाने योगे च व्यवस्थितिः", "type": "तत्पुरुषः (सप्तमी)"}
            ]
        },
        {"number": 2, "original": "अहिंसा सत्यमक्रोधस्त्यागः शान्तिरपैशुनम् ।\nदया भूतेष्वलोलुप्त्वं मार्दवं ह्रीरचापलम् ॥ १६-२॥", "sandhiVigraha": "अहिंसा सत्यम् अक्रोधः त्यागः शान्तिः अपैशुनम् ।\nदया भूतेषु अलोलुप्त्वम् मार्दवम् ह्रीः अचापलम् ॥ १६-२॥", "anvaya": "अहिंसा, सत्यम्, अक्रोधः, त्यागः, शान्तिः, अपैशुनम्, भूतेषु दया, अलोलुप्त्वम्, मार्दवम्, ह्रीः, अचापलम्,", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 3, "original": "तेजः क्षमा धृतिः शौचमद्रोहो नातिमानिता ।\nभवन्ति सम्पदं दैवीमभिजातस्य भारत ॥ १६-३॥", "sandhiVigraha": "तेजः क्षमा धृतिः शौचम् अद्रोहः न अति-मानिता ।\nभवन्ति सम्पदम् दैवीम् अभिजातस्य भारत ॥ १६-३॥", "anvaya": "हे भारत! तेजः, क्षमा, धृतिः, शौचम्, अद्रोहः, न अति-मानिता (इति एतानि लक्षणानि) दैवीम् सम्पदम् अभिजातस्य (पुरुषस्य) भवन्ति ।", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 4, "original": "दम्भो दर्पोऽभिमानश्च क्रोधः पारुष्यमेव च ।\nअज्ञानं चाभिजातस्य पार्थ सम्पदमासुरीम् ॥ १६-४॥", "sandhiVigraha": "दम्भः दर्पः अभिमानः च क्रोधः पारुष्यम् एव च ।\nअज्ञानम् च अभिजातस्य पार्थ सम्पदम् आसुरीम् ॥ १६-४॥", "anvaya": "हे पार्थ! दम्भः, दर्पः, अभिमानः च, क्रोधः, पारुष्यम्, एव च अज्ञानम् च (एतानि लक्ष्णानि) आसुरीम् सम्पदम् अभिजातस्य (पुरुषस्य भवन्ति) ।", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 5, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 6, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 7, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 8, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 9, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 10, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 11, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 12, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 13, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 14, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 15, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 16, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 17, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 18, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 19, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 20, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 21, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 22, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 23, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []},
        {"number": 24, "original": "", "sandhiVigraha": "", "anvaya": "", "padartha": [], "bhavaSpashtikaranam": [], "dhaturoopa": [], "samasa": []}
    ]
}

Filling in slokas over time

You can add the sloka text for slokas 5-24 from sanskritdocuments.org at any time. Copy the original, sandhiVigraha, and anvaya text from the source and paste it into the corresponding sloka object. The app will display whatever data is available and show a placeholder message for empty fields.

Your data layer is ready. Next up, you will build the PHP page that reads this JSON and displays each sloka in a tabbed interface.

Build the Sloka Display Page

Your JSON data is ready and your login system is protecting the app. Now you need the main page that students actually interact with.

In this step, you will build index.php to load sloka data from your JSON file and display it in a navigable, tabbed interface. You will also write the JavaScript that makes tabs and keyboard navigation work.

In this step, get ready to:
  • Set up the PHP data logic to read JSON and determine which sloka to display.
  • Build the full HTML page with navigation, sloka display, and six tabbed panels.
  • Add JavaScript for tab switching and keyboard navigation between slokas.
Set up the PHP data logic

Before rendering any HTML, the page needs to load the JSON data, figure out which sloka the student is viewing, and calculate the previous/next sloka numbers for navigation.

  • Open index.php in VS Code and add the PHP data logic by pasting this code:
<?php
require_once 'config.php';
requireAuth();

// Load the chapter data from JSON
$jsonData = file_get_contents('data/chapter16.json');
$chapter = json_decode($jsonData, true);

// Determine which sloka to display from the URL
$totalSlokas = count($chapter['slokas']);
$currentSloka = isset($_GET['sloka']) ? (int)$_GET['sloka'] : 1;
$currentSloka = max(1, min($currentSloka, $totalSlokas));

// Get the current sloka data and calculate prev/next
$sloka = $chapter['slokas'][$currentSloka - 1];
$prevSloka = $currentSloka > 1 ? $currentSloka - 1 : null;
$nextSloka = $currentSloka < $totalSlokas ? $currentSloka + 1 : null;
?>

What does this code do?

  • require_once 'config.php' loads the session and authentication helpers. requireAuth() immediately redirects unauthenticated visitors to the login page.
  • file_get_contents() reads your JSON file as a string. json_decode($jsonData, true) converts it into a PHP associative array you can work with.
  • The max(1, min(...)) pattern clamps the sloka number so students cannot request sloka 0 or sloka 99.
  • $prevSloka and $nextSloka are set to null when the student is at the first or last sloka. This lets the HTML disable those navigation buttons.
  • Save index.php.
Build the page HTML structure

The HTML portion of the page renders the header, sloka navigation, the sloka text itself, and a tabbed interface with six grammatical breakdown panels.

  • In index.php, directly below the closing ?> tag, add the HTML head, header, navigation, and sloka display by pasting this code:
<!DOCTYPE html>
<html lang="sa">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>श्लोकः <?php echo $currentSloka; ?> - भगवद्गीता अध्यायः १६</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;700&family=Tiro+Devanagari+Sanskrit&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- Site header with chapter title and logout -->
    <header class="site-header">
        <div class="header-content">
            <h1><?php echo htmlspecialchars($chapter['title']); ?></h1>
            <p class="header-sub"><?php echo htmlspecialchars($chapter['titleSandhi']); ?> | अध्यायः १६</p>
        </div>
        <a href="logout.php" class="logout-btn">बहिर्गच्छतु</a>
    </header>

    <main class="main-content">
        <!-- Top navigation between slokas -->
        <nav class="sloka-nav">
            <?php if ($prevSloka): ?>
                <a href="?sloka=<?php echo $prevSloka; ?>" class="nav-btn nav-prev">← पूर्वश्लोकः</a>
            <?php else: ?>
                <span class="nav-btn nav-prev disabled">← पूर्वश्लोकः</span>
            <?php endif; ?>
            <span class="sloka-indicator">श्लोकः <?php echo $currentSloka; ?> / <?php echo $totalSlokas; ?></span>
            <?php if ($nextSloka): ?>
                <a href="?sloka=<?php echo $nextSloka; ?>" class="nav-btn nav-next">अग्रिमश्लोकः →</a>
            <?php else: ?>
                <span class="nav-btn nav-next disabled">अग्रिमश्लोकः →</span>
            <?php endif; ?>
        </nav>

        <!-- Sloka text display -->
        <section class="sloka-display">
            <div class="sloka-text">
                <?php echo nl2br(htmlspecialchars($sloka['original'])); ?>
            </div>
            <p class="sloka-number">॥ १६-<?php echo $currentSloka; ?> ॥</p>
        </section>

What does this code do?

  • The <head> loads Google Fonts for Devanagari text (Tiro Devanagari Sanskrit for sloka display, Noto Sans Devanagari for UI text) and links to your stylesheet.
  • The header displays the chapter title from your JSON and includes a logout link.
  • The navigation renders prev/next links as clickable <a> tags when available, or disabled <span> elements at the boundaries.
  • nl2br(htmlspecialchars(...)) safely renders the sloka text. htmlspecialchars prevents injection attacks, and nl2br converts the \n characters in your JSON into HTML line breaks.
  • Directly below the sloka-display section, add the tabbed interface with all six grammatical panels by pasting this code:
        <!-- Tabbed interface for grammatical breakdowns -->
        <section class="tabs-container">
            <div class="tab-bar">
                <button class="tab active" data-tab="anvaya">अन्वयः</button>
                <button class="tab" data-tab="sandhi">सन्धिविग्रहः</button>
                <button class="tab" data-tab="padartha">पदार्थः</button>
                <button class="tab" data-tab="bhava">भावस्पष्टीकरणम्</button>
                <button class="tab" data-tab="dhaturoopa">धातुरूपम्</button>
                <button class="tab" data-tab="samasa">समासः</button>
            </div>

            <!-- Anvaya panel -->
            <div class="tab-panel active" id="panel-anvaya">
                <div class="anvaya-text">
                    <?php if (!empty($sloka['anvaya'])): ?>
                        <?php echo nl2br(htmlspecialchars($sloka['anvaya'])); ?>
                    <?php else: ?>
                        <p class="empty-state">अन्वयः शीघ्रमेव आगमिष्यति।</p>
                    <?php endif; ?>
                </div>
            </div>

            <!-- Sandhi Vigraha panel -->
            <div class="tab-panel" id="panel-sandhi">
                <div class="sandhi-text">
                    <?php if (!empty($sloka['sandhiVigraha'])): ?>
                        <?php echo nl2br(htmlspecialchars($sloka['sandhiVigraha'])); ?>
                    <?php else: ?>
                        <p class="empty-state">सन्धिविग्रहः शीघ्रमेव आगमिष्यति।</p>
                    <?php endif; ?>
                </div>
            </div>

            <!-- Padartha panel with word-meaning table -->
            <div class="tab-panel" id="panel-padartha">
                <?php if (!empty($sloka['padartha'])): ?>
                    <div class="table-wrapper">
                        <table class="data-table">
                            <thead><tr><th>पदम्</th><th>Transliteration</th><th>अर्थः</th></tr></thead>
                            <tbody>
                                <?php foreach ($sloka['padartha'] as $entry): ?>
                                    <tr>
                                        <td class="sanskrit-cell"><?php echo htmlspecialchars($entry['word']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['transliteration']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['meaning']); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                <?php else: ?>
                    <p class="empty-state">पदार्थः शीघ्रमेव आगमिष्यति।</p>
                <?php endif; ?>
            </div>

            <!-- Bhava Spashtikaranam panel with Q&A format -->
            <div class="tab-panel" id="panel-bhava">
                <?php if (!empty($sloka['bhavaSpashtikaranam'])): ?>
                    <div class="bhava-list">
                        <?php foreach ($sloka['bhavaSpashtikaranam'] as $qa): ?>
                            <div class="bhava-item">
                                <p class="prashna"><span class="label">प्रश्नः</span> <?php echo htmlspecialchars($qa['prashna']); ?></p>
                                <p class="uttaram"><span class="label">उत्तरम्</span> <?php echo htmlspecialchars($qa['uttaram']); ?></p>
                            </div>
                        <?php endforeach; ?>
                    </div>
                <?php else: ?>
                    <p class="empty-state">भावस्पष्टीकरणम् शीघ्रमेव आगमिष्यति।</p>
                <?php endif; ?>
            </div>

            <!-- Dhaturoopa panel with verb analysis table -->
            <div class="tab-panel" id="panel-dhaturoopa">
                <?php if (!empty($sloka['dhaturoopa'])): ?>
                    <div class="table-wrapper">
                        <table class="data-table">
                            <thead><tr><th>पदम्</th><th>धातुः</th><th>गणः</th><th>लकारः</th><th>पुरुषः</th><th>वचनम्</th></tr></thead>
                            <tbody>
                                <?php foreach ($sloka['dhaturoopa'] as $entry): ?>
                                    <tr>
                                        <td class="sanskrit-cell"><?php echo htmlspecialchars($entry['word']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['dhatu']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['gana']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['lakaara']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['purusha']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['vachana']); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                <?php else: ?>
                    <p class="empty-state">धातुरूपम् शीघ्रमेव आगमिष्यति।</p>
                <?php endif; ?>
            </div>

            <!-- Samasa panel with compound analysis table -->
            <div class="tab-panel" id="panel-samasa">
                <?php if (!empty($sloka['samasa'])): ?>
                    <div class="table-wrapper">
                        <table class="data-table">
                            <thead><tr><th>समस्तपदम्</th><th>समासविग्रहः</th><th>समासप्रकारः</th></tr></thead>
                            <tbody>
                                <?php foreach ($sloka['samasa'] as $entry): ?>
                                    <tr>
                                        <td class="sanskrit-cell"><?php echo htmlspecialchars($entry['compound']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['breakdown']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['type']); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                <?php else: ?>
                    <p class="empty-state">समासः शीघ्रमेव आगमिष्यति।</p>
                <?php endif; ?>
            </div>
        </section>

What does the tabbed interface do?

  • Each <button> in the tab bar has a data-tab attribute that maps to a panel ID (e.g. data-tab="anvaya" maps to panel-anvaya). JavaScript will use this mapping to show/hide panels.
  • The Padartha, Dhaturoopa, and Samasa panels use HTML tables wrapped in table-wrapper divs for horizontal scrolling on mobile.
  • Bhava Spashtikaranam uses a Q&A format, looping through question/answer pairs from the JSON.
  • Every panel has an empty-state message in Sanskrit for slokas you have not yet populated with data.
  • Below the tabs-container section, close the page by adding the bottom navigation, script link, and closing HTML tags:
        <!-- Bottom navigation (repeated for convenience) -->
        <nav class="sloka-nav bottom-nav">
            <?php if ($prevSloka): ?>
                <a href="?sloka=<?php echo $prevSloka; ?>" class="nav-btn nav-prev">← पूर्वश्लोकः</a>
            <?php else: ?>
                <span class="nav-btn nav-prev disabled">← पूर्वश्लोकः</span>
            <?php endif; ?>
            <span class="sloka-indicator">श्लोकः <?php echo $currentSloka; ?> / <?php echo $totalSlokas; ?></span>
            <?php if ($nextSloka): ?>
                <a href="?sloka=<?php echo $nextSloka; ?>" class="nav-btn nav-next">अग्रिमश्लोकः →</a>
            <?php else: ?>
                <span class="nav-btn nav-next disabled">अग्रिमश्लोकः →</span>
            <?php endif; ?>
        </nav>
    </main>

    <script src="script.js"></script>
</body>
</html>

Why repeat the navigation at the bottom?

After reading through six grammatical tabs, students should not have to scroll all the way back up to navigate. The bottom nav duplicates the top nav so the next sloka is always one click away.

  • Save index.php.
  • Visit index.php?sloka=1 in your browser (log in if prompted). You should see the chapter title in the header, the first sloka in Devanagari, and a row of six tab buttons below it.

Seeing a blank page or an error?

  • Check that your data/chapter16.json file is valid JSON with no trailing commas. A single syntax error in the JSON will cause json_decode() to return null.
  • Make sure the file path data/chapter16.json is correct relative to where index.php lives. Both should be in the gita-app folder.
  • If you see a redirect loop, confirm that config.php exists in the same folder and that you are logged in.

Help me debug my index.php page

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the full index.php file for comparison:

<?php
require_once 'config.php';
requireAuth();

$jsonData = file_get_contents('data/chapter16.json');
$chapter = json_decode($jsonData, true);

$totalSlokas = count($chapter['slokas']);
$currentSloka = isset($_GET['sloka']) ? (int)$_GET['sloka'] : 1;
$currentSloka = max(1, min($currentSloka, $totalSlokas));

$sloka = $chapter['slokas'][$currentSloka - 1];
$prevSloka = $currentSloka > 1 ? $currentSloka - 1 : null;
$nextSloka = $currentSloka < $totalSlokas ? $currentSloka + 1 : null;
?>
<!DOCTYPE html>
<html lang="sa">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>श्लोकः <?php echo $currentSloka; ?> - भगवद्गीता अध्यायः १६</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;700&family=Tiro+Devanagari+Sanskrit&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <header class="site-header">
        <div class="header-content">
            <h1><?php echo htmlspecialchars($chapter['title']); ?></h1>
            <p class="header-sub"><?php echo htmlspecialchars($chapter['titleSandhi']); ?> | अध्यायः १६</p>
        </div>
        <a href="logout.php" class="logout-btn">बहिर्गच्छतु</a>
    </header>

    <main class="main-content">
        <nav class="sloka-nav">
            <?php if ($prevSloka): ?>
                <a href="?sloka=<?php echo $prevSloka; ?>" class="nav-btn nav-prev">← पूर्वश्लोकः</a>
            <?php else: ?>
                <span class="nav-btn nav-prev disabled">← पूर्वश्लोकः</span>
            <?php endif; ?>
            <span class="sloka-indicator">श्लोकः <?php echo $currentSloka; ?> / <?php echo $totalSlokas; ?></span>
            <?php if ($nextSloka): ?>
                <a href="?sloka=<?php echo $nextSloka; ?>" class="nav-btn nav-next">अग्रिमश्लोकः →</a>
            <?php else: ?>
                <span class="nav-btn nav-next disabled">अग्रिमश्लोकः →</span>
            <?php endif; ?>
        </nav>

        <section class="sloka-display">
            <div class="sloka-text">
                <?php echo nl2br(htmlspecialchars($sloka['original'])); ?>
            </div>
            <p class="sloka-number">॥ १६-<?php echo $currentSloka; ?> ॥</p>
        </section>

        <section class="tabs-container">
            <div class="tab-bar">
                <button class="tab active" data-tab="anvaya">अन्वयः</button>
                <button class="tab" data-tab="sandhi">सन्धिविग्रहः</button>
                <button class="tab" data-tab="padartha">पदार्थः</button>
                <button class="tab" data-tab="bhava">भावस्पष्टीकरणम्</button>
                <button class="tab" data-tab="dhaturoopa">धातुरूपम्</button>
                <button class="tab" data-tab="samasa">समासः</button>
            </div>
            <div class="tab-panel active" id="panel-anvaya">
                <div class="anvaya-text">
                    <?php if (!empty($sloka['anvaya'])): ?>
                        <?php echo nl2br(htmlspecialchars($sloka['anvaya'])); ?>
                    <?php else: ?>
                        <p class="empty-state">अन्वयः शीघ्रमेव आगमिष्यति।</p>
                    <?php endif; ?>
                </div>
            </div>
            <div class="tab-panel" id="panel-sandhi">
                <div class="sandhi-text">
                    <?php if (!empty($sloka['sandhiVigraha'])): ?>
                        <?php echo nl2br(htmlspecialchars($sloka['sandhiVigraha'])); ?>
                    <?php else: ?>
                        <p class="empty-state">सन्धिविग्रहः शीघ्रमेव आगमिष्यति।</p>
                    <?php endif; ?>
                </div>
            </div>
            <div class="tab-panel" id="panel-padartha">
                <?php if (!empty($sloka['padartha'])): ?>
                    <div class="table-wrapper">
                        <table class="data-table">
                            <thead><tr><th>पदम्</th><th>Transliteration</th><th>अर्थः</th></tr></thead>
                            <tbody>
                                <?php foreach ($sloka['padartha'] as $entry): ?>
                                    <tr>
                                        <td class="sanskrit-cell"><?php echo htmlspecialchars($entry['word']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['transliteration']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['meaning']); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                <?php else: ?>
                    <p class="empty-state">पदार्थः शीघ्रमेव आगमिष्यति।</p>
                <?php endif; ?>
            </div>
            <div class="tab-panel" id="panel-bhava">
                <?php if (!empty($sloka['bhavaSpashtikaranam'])): ?>
                    <div class="bhava-list">
                        <?php foreach ($sloka['bhavaSpashtikaranam'] as $qa): ?>
                            <div class="bhava-item">
                                <p class="prashna"><span class="label">प्रश्नः</span> <?php echo htmlspecialchars($qa['prashna']); ?></p>
                                <p class="uttaram"><span class="label">उत्तरम्</span> <?php echo htmlspecialchars($qa['uttaram']); ?></p>
                            </div>
                        <?php endforeach; ?>
                    </div>
                <?php else: ?>
                    <p class="empty-state">भावस्पष्टीकरणम् शीघ्रमेव आगमिष्यति।</p>
                <?php endif; ?>
            </div>
            <div class="tab-panel" id="panel-dhaturoopa">
                <?php if (!empty($sloka['dhaturoopa'])): ?>
                    <div class="table-wrapper">
                        <table class="data-table">
                            <thead><tr><th>पदम्</th><th>धातुः</th><th>गणः</th><th>लकारः</th><th>पुरुषः</th><th>वचनम्</th></tr></thead>
                            <tbody>
                                <?php foreach ($sloka['dhaturoopa'] as $entry): ?>
                                    <tr>
                                        <td class="sanskrit-cell"><?php echo htmlspecialchars($entry['word']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['dhatu']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['gana']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['lakaara']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['purusha']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['vachana']); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                <?php else: ?>
                    <p class="empty-state">धातुरूपम् शीघ्रमेव आगमिष्यति।</p>
                <?php endif; ?>
            </div>
            <div class="tab-panel" id="panel-samasa">
                <?php if (!empty($sloka['samasa'])): ?>
                    <div class="table-wrapper">
                        <table class="data-table">
                            <thead><tr><th>समस्तपदम्</th><th>समासविग्रहः</th><th>समासप्रकारः</th></tr></thead>
                            <tbody>
                                <?php foreach ($sloka['samasa'] as $entry): ?>
                                    <tr>
                                        <td class="sanskrit-cell"><?php echo htmlspecialchars($entry['compound']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['breakdown']); ?></td>
                                        <td><?php echo htmlspecialchars($entry['type']); ?></td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                <?php else: ?>
                    <p class="empty-state">समासः शीघ्रमेव आगमिष्यति।</p>
                <?php endif; ?>
            </div>
        </section>

        <nav class="sloka-nav bottom-nav">
            <?php if ($prevSloka): ?>
                <a href="?sloka=<?php echo $prevSloka; ?>" class="nav-btn nav-prev">← पूर्वश्लोकः</a>
            <?php else: ?>
                <span class="nav-btn nav-prev disabled">← पूर्वश्लोकः</span>
            <?php endif; ?>
            <span class="sloka-indicator">श्लोकः <?php echo $currentSloka; ?> / <?php echo $totalSlokas; ?></span>
            <?php if ($nextSloka): ?>
                <a href="?sloka=<?php echo $nextSloka; ?>" class="nav-btn nav-next">अग्रिमश्लोकः →</a>
            <?php else: ?>
                <span class="nav-btn nav-next disabled">अग्रिमश्लोकः →</span>
            <?php endif; ?>
        </nav>
    </main>

    <script src="script.js"></script>
</body>
</html>
Create the tab switching JavaScript

The tabs look correct in the HTML, but clicking them does nothing yet. You need JavaScript to toggle the active class between tabs and panels, and to let students navigate between slokas with their keyboard.

  • Open script.js in VS Code and add the tab switching and keyboard navigation logic by pasting this code:
document.addEventListener('DOMContentLoaded', function() {
    // Tab switching logic
    var tabs = document.querySelectorAll('.tab');
    var panels = document.querySelectorAll('.tab-panel');

    tabs.forEach(function(tab) {
        tab.addEventListener('click', function() {
            var targetId = 'panel-' + this.getAttribute('data-tab');

            // Remove active class from all tabs and panels
            tabs.forEach(function(t) {
                t.classList.remove('active');
            });
            panels.forEach(function(p) {
                p.classList.remove('active');
            });

            // Activate the clicked tab and its panel
            this.classList.add('active');
            document.getElementById(targetId).classList.add('active');
        });
    });

    // Keyboard navigation between slokas
    document.addEventListener('keydown', function(e) {
        // Don't hijack typing in input fields
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
            return;
        }

        if (e.key === 'ArrowLeft') {
            var prevBtn = document.querySelector('.nav-prev:not(.disabled)');
            if (prevBtn) {
                window.location.href = prevBtn.href;
            }
        } else if (e.key === 'ArrowRight') {
            var nextBtn = document.querySelector('.nav-next:not(.disabled)');
            if (nextBtn) {
                window.location.href = nextBtn.href;
            }
        }
    });
});

What does this code do?

  • The tab click handler reads the data-tab attribute from the clicked button, builds the panel ID (e.g. panel-anvaya), removes active from everything, then adds it to the clicked tab and the matching panel.
  • The keydown listener checks that the student is not typing in an input field first. Then it looks for a non-disabled prev/next button and navigates to its href on ArrowLeft or ArrowRight.
  • The :not(.disabled) selector ensures arrow keys do nothing when the student is already on the first or last sloka.
  • Save script.js.
  • Refresh index.php?sloka=1 in your browser. Click each tab and confirm the content below changes. The Anvaya and Sandhi Vigraha tabs show Sanskrit text. The Padartha tab shows a table of words and meanings.
  • Click the next sloka button or press the right arrow key on your keyboard. You should navigate to sloka 2.

Tabs not switching?

  • Open your browser's developer console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for JavaScript errors.
  • Make sure the <script src="script.js"></script> tag is at the bottom of your index.php file, just before </body>.
  • Check that the data-tab values on your buttons match the panel IDs exactly (e.g. data-tab="anvaya" matches id="panel-anvaya").

Help me fix my tab switching JavaScript

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the full script.js file:

document.addEventListener('DOMContentLoaded', function() {
    var tabs = document.querySelectorAll('.tab');
    var panels = document.querySelectorAll('.tab-panel');

    tabs.forEach(function(tab) {
        tab.addEventListener('click', function() {
            var targetId = 'panel-' + this.getAttribute('data-tab');

            tabs.forEach(function(t) {
                t.classList.remove('active');
            });
            panels.forEach(function(p) {
                p.classList.remove('active');
            });

            this.classList.add('active');
            document.getElementById(targetId).classList.add('active');
        });
    });

    document.addEventListener('keydown', function(e) {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
            return;
        }

        if (e.key === 'ArrowLeft') {
            var prevBtn = document.querySelector('.nav-prev:not(.disabled)');
            if (prevBtn) {
                window.location.href = prevBtn.href;
            }
        } else if (e.key === 'ArrowRight') {
            var nextBtn = document.querySelector('.nav-next:not(.disabled)');
            if (nextBtn) {
                window.location.href = nextBtn.href;
            }
        }
    });
});

Your sloka display page is functional. Students can log in, navigate between all 24 slokas, and browse grammatical breakdowns through the tabbed interface. Next up, you will style the app with CSS and proper Devanagari fonts to make it polished and readable.

Style with CSS and Fonts

Your app is fully functional. Students can log in, navigate slokas, and switch between grammatical tabs. But right now everything looks like raw unstyled HTML.

In this step, you will write the CSS that transforms the app into a polished, readable interface. You will use Tiro Devanagari Sanskrit for sloka display and Noto Sans Devanagari for body text, giving each a distinct visual role.

In this step, get ready to:
  • Set up base styles with font assignments, page layout, and a warm color scheme.
  • Style the tabbed interface, sloka display, and navigation buttons.
  • Add responsive styles so students can use the app on mobile devices.
Set up base styles, login page, and header

Both login.php and index.php already link the Google Fonts stylesheet in their HTML heads. Now you will write the CSS rules that put those fonts to work.

  • Open style.css in VS Code and paste the following base styles:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Noto Sans Devanagari', sans-serif;
    background-color: #fdf8f0;
    color: #2d1b0e;
    line-height: 1.8;
    min-height: 100vh;
}

/* Login Page */

.login-page {
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #fdf8f0 0%, #f5e6d3 100%);
}

.login-container {
    background: white;
    padding: 3rem 2.5rem;
    border-radius: 12px;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
    text-align: center;
    max-width: 400px;
    width: 90%;
}

.login-title {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 2rem;
    color: #8b4513;
    margin-bottom: 0.5rem;
}

.login-subtitle {
    font-size: 0.95rem;
    color: #6b5744;
    margin-bottom: 2rem;
}

.login-form label {
    display: block;
    text-align: left;
    margin-bottom: 0.5rem;
    font-size: 0.9rem;
    color: #5a4030;
}

.login-form input {
    width: 100%;
    padding: 0.75rem 1rem;
    border: 2px solid #e0d5c8;
    border-radius: 8px;
    font-size: 1rem;
    margin-bottom: 1.5rem;
    transition: border-color 0.2s;
}

.login-form input:focus {
    outline: none;
    border-color: #8b4513;
}

.login-form button {
    width: 100%;
    padding: 0.75rem;
    background-color: #8b4513;
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 1.1rem;
    font-family: 'Noto Sans Devanagari', sans-serif;
    cursor: pointer;
    transition: background-color 0.2s;
}

.login-form button:hover {
    background-color: #6b3410;
}

.error-message {
    background-color: #fee;
    color: #c00;
    padding: 0.75rem;
    border-radius: 6px;
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

/* Header */

.site-header {
    background-color: #8b4513;
    color: white;
    padding: 1rem 2rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.header-content h1 {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 1.5rem;
    font-weight: normal;
}

.header-sub {
    font-size: 0.85rem;
    opacity: 0.85;
}

.logout-btn {
    color: white;
    text-decoration: none;
    padding: 0.4rem 1rem;
    border: 1px solid rgba(255, 255, 255, 0.5);
    border-radius: 6px;
    font-size: 0.85rem;
    transition: background-color 0.2s;
}

.logout-btn:hover {
    background-color: rgba(255, 255, 255, 0.15);
}

/* Main Content */

.main-content {
    max-width: 900px;
    margin: 0 auto;
    padding: 2rem 1.5rem;
}

What does this code do?

The universal selector * resets margins and padding across all elements so you start from a clean slate. box-sizing: border-box makes padding count inside an element's width, preventing layout surprises.

The body uses Noto Sans Devanagari as the default font for readability in UI text and tables. The login title uses Tiro Devanagari Sanskrit for a traditional literary feel. The color #8b4513 (saddlebrown) appears throughout as the primary accent, giving the app a warm tone appropriate for classical Sanskrit content.

Why two Devanagari fonts?

Tiro Devanagari Sanskrit was designed for the Murty Classical Library of India. It preserves traditional conjunct forms and has extended character support including Vedic signs. This makes it ideal for displaying slokas faithfully.

Noto Sans Devanagari is a clean sans-serif optimized for screen readability. It works better for body text, UI labels, and table content where clarity matters more than literary aesthetics.

  • Save style.css and refresh your browser on login.php.
  • You should see a centered login card with a warm gradient background, the title in a serif Devanagari font, and a styled password input with a brown submit button.

Login page still looks unstyled?

Check that the <link rel="stylesheet" href="style.css"> tag exists in your login.php file's <head> section. Also confirm that style.css is saved in the same folder as login.php.

Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear cached styles.

Help me debug why my CSS isn't loading on the login page.

Style the navigation, sloka display, and tabbed interface

The sloka viewer needs clear navigation buttons, a prominent sloka display area, and a well-organized tab system. These styles make each grammatical section easy to find and read.

  • Add the navigation and sloka display styles below your existing CSS in style.css:
/* Navigation */

.sloka-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 2rem;
}

.bottom-nav {
    margin-top: 2rem;
    margin-bottom: 0;
}

.nav-btn {
    padding: 0.5rem 1.2rem;
    background-color: #8b4513;
    color: white;
    text-decoration: none;
    border-radius: 6px;
    font-size: 0.9rem;
    transition: background-color 0.2s;
}

.nav-btn:hover {
    background-color: #6b3410;
}

.nav-btn.disabled {
    background-color: #ccc;
    color: #888;
    pointer-events: none;
}

.sloka-indicator {
    font-size: 0.95rem;
    color: #6b5744;
    font-weight: 700;
}

/* Sloka Display */

.sloka-display {
    background: white;
    border: 2px solid #e8ddd0;
    border-radius: 12px;
    padding: 2rem;
    text-align: center;
    margin-bottom: 2rem;
}

.sloka-text {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 1.5rem;
    line-height: 2.2;
    color: #2d1b0e;
}

.sloka-number {
    margin-top: 1rem;
    font-size: 1rem;
    color: #8b4513;
}

What does this code do?

  • .nav-btn styles the previous/next links as solid brown buttons. The .disabled class grays them out and removes click interaction with pointer-events: none.
  • .sloka-text uses Tiro Devanagari Sanskrit at 1.5rem with a generous 2.2 line-height. This extra vertical space prevents Devanagari conjuncts and vowel marks from colliding between lines.
  • .sloka-display creates a white card with a subtle border, making the sloka visually distinct from the page background.
  • Now add the tab bar and tab content styles below the sloka display styles:
/* Tabs */

.tabs-container {
    background: white;
    border-radius: 12px;
    border: 2px solid #e8ddd0;
    overflow: hidden;
}

.tab-bar {
    display: flex;
    flex-wrap: wrap;
    background-color: #f5ede3;
    border-bottom: 2px solid #e8ddd0;
}

.tab {
    padding: 0.75rem 1.2rem;
    border: none;
    background: none;
    font-family: 'Noto Sans Devanagari', sans-serif;
    font-size: 0.9rem;
    color: #6b5744;
    cursor: pointer;
    transition: all 0.2s;
    border-bottom: 3px solid transparent;
}

.tab:hover {
    background-color: #ebe2d6;
}

.tab.active {
    color: #8b4513;
    font-weight: 700;
    background-color: white;
    border-bottom-color: #8b4513;
}

.tab-panel {
    display: none;
    padding: 1.5rem 2rem;
}

.tab-panel.active {
    display: block;
}

/* Anvaya and Sandhi */

.anvaya-text,
.sandhi-text {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 1.2rem;
    line-height: 2;
    padding: 1rem;
    background-color: #fdf8f0;
    border-left: 4px solid #8b4513;
    border-radius: 0 8px 8px 0;
}

/* Tables */

.table-wrapper {
    overflow-x: auto;
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.95rem;
}

.data-table th {
    background-color: #8b4513;
    color: white;
    padding: 0.75rem 1rem;
    text-align: left;
    font-weight: 700;
}

.data-table td {
    padding: 0.65rem 1rem;
    border-bottom: 1px solid #e8ddd0;
}

.data-table tbody tr:nth-child(even) {
    background-color: #fdf8f0;
}

.data-table tbody tr:hover {
    background-color: #f5ede3;
}

.sanskrit-cell {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 1.1rem;
}

/* Bhava Spashtikaranam */

.bhava-list {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

.bhava-item {
    padding: 1rem;
    background-color: #fdf8f0;
    border-radius: 8px;
    border: 1px solid #e8ddd0;
}

.prashna {
    margin-bottom: 0.75rem;
    font-size: 1.05rem;
}

.uttaram {
    font-size: 1.05rem;
    padding-left: 1rem;
    border-left: 3px solid #8b4513;
}

.label {
    display: inline-block;
    font-weight: 700;
    color: #8b4513;
    margin-right: 0.5rem;
    font-size: 0.85rem;
    text-transform: uppercase;
}

/* Empty State */

.empty-state {
    text-align: center;
    color: #999;
    font-style: italic;
    padding: 2rem;
}

What does this code do?

  • The .tab.active rule highlights the selected tab with a brown bottom border and white background. The .tab-panel rules hide all panels by default and show only the active one.
  • .anvaya-text and .sandhi-text use a left border accent to visually separate these Sanskrit prose sections from the tab container background.
  • .table-wrapper with overflow-x: auto allows tables to scroll horizontally on smaller screens instead of breaking the page layout.
  • .data-table tbody tr:nth-child(even) gives alternating rows a light background, making multi-column data easier to read across a row.
  • Save style.css and log in to view index.php?sloka=1.
  • You should see a brown header bar with the chapter title, navigation buttons above and below the sloka, the sloka text prominently centered in a white card, and six tabs below it. Click the tabs to confirm each panel displays its content with proper styling.

Tabs not showing content when clicked?

The tab show/hide relies on CSS classes matching the JavaScript logic. Make sure .tab-panel has display: none and .tab-panel.active has display: block. Also confirm that script.js is still linked at the bottom of index.php.

Help me debug why my tab panels are not showing when I click the tab buttons.

Add responsive styles for mobile

Your students may access this app from their phones. A media query reduces font sizes and padding on small screens so everything remains readable without horizontal scrolling.

  • Add this responsive media query at the very bottom of style.css:
/* Responsive */

@media (max-width: 600px) {
    .site-header {
        padding: 1rem;
    }

    .header-content h1 {
        font-size: 1.2rem;
    }

    .main-content {
        padding: 1rem;
    }

    .sloka-text {
        font-size: 1.2rem;
    }

    .sloka-display {
        padding: 1.5rem 1rem;
    }

    .tab {
        padding: 0.6rem 0.8rem;
        font-size: 0.8rem;
    }

    .tab-panel {
        padding: 1rem;
    }

    .anvaya-text,
    .sandhi-text {
        font-size: 1rem;
    }

    .nav-btn {
        padding: 0.4rem 0.8rem;
        font-size: 0.8rem;
    }

    .data-table {
        font-size: 0.85rem;
    }

    .data-table th,
    .data-table td {
        padding: 0.5rem 0.6rem;
    }
}

What does this code do?

The @media (max-width: 600px) rule applies only when the browser window is 600 pixels wide or narrower. Inside it, font sizes shrink slightly, padding reduces, and table cells become more compact.

The .tab-bar already has flex-wrap: wrap from earlier. Combined with smaller tab padding here, tabs naturally stack onto multiple rows on narrow screens without needing additional layout changes.

  • Save style.css.
  • Refresh your browser on the sloka page. Resize the browser window to a narrow width (or use DevTools device mode). You should see the tabs wrap onto multiple rows, the sloka text shrink slightly, and the table remain scrollable within its container.

Responsive styles not kicking in?

Make sure the @media block is at the very bottom of your file. Media queries must come after the base styles they override. Also check that your HTML includes the viewport meta tag: <meta name="viewport" content="width=device-width, initial-scale=1.0">.

Help me understand why my media query styles are not applying on mobile widths.

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css and confirmed the login page, sloka display, tabs, and mobile view all look polished.

ⓧ I'd like to double check the full code

Here is the complete style.css file for comparison:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Noto Sans Devanagari', sans-serif;
    background-color: #fdf8f0;
    color: #2d1b0e;
    line-height: 1.8;
    min-height: 100vh;
}

/* Login Page */

.login-page {
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, #fdf8f0 0%, #f5e6d3 100%);
}

.login-container {
    background: white;
    padding: 3rem 2.5rem;
    border-radius: 12px;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
    text-align: center;
    max-width: 400px;
    width: 90%;
}

.login-title {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 2rem;
    color: #8b4513;
    margin-bottom: 0.5rem;
}

.login-subtitle {
    font-size: 0.95rem;
    color: #6b5744;
    margin-bottom: 2rem;
}

.login-form label {
    display: block;
    text-align: left;
    margin-bottom: 0.5rem;
    font-size: 0.9rem;
    color: #5a4030;
}

.login-form input {
    width: 100%;
    padding: 0.75rem 1rem;
    border: 2px solid #e0d5c8;
    border-radius: 8px;
    font-size: 1rem;
    margin-bottom: 1.5rem;
    transition: border-color 0.2s;
}

.login-form input:focus {
    outline: none;
    border-color: #8b4513;
}

.login-form button {
    width: 100%;
    padding: 0.75rem;
    background-color: #8b4513;
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 1.1rem;
    font-family: 'Noto Sans Devanagari', sans-serif;
    cursor: pointer;
    transition: background-color 0.2s;
}

.login-form button:hover {
    background-color: #6b3410;
}

.error-message {
    background-color: #fee;
    color: #c00;
    padding: 0.75rem;
    border-radius: 6px;
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

/* Header */

.site-header {
    background-color: #8b4513;
    color: white;
    padding: 1rem 2rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.header-content h1 {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 1.5rem;
    font-weight: normal;
}

.header-sub {
    font-size: 0.85rem;
    opacity: 0.85;
}

.logout-btn {
    color: white;
    text-decoration: none;
    padding: 0.4rem 1rem;
    border: 1px solid rgba(255, 255, 255, 0.5);
    border-radius: 6px;
    font-size: 0.85rem;
    transition: background-color 0.2s;
}

.logout-btn:hover {
    background-color: rgba(255, 255, 255, 0.15);
}

/* Main Content */

.main-content {
    max-width: 900px;
    margin: 0 auto;
    padding: 2rem 1.5rem;
}

/* Navigation */

.sloka-nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 2rem;
}

.bottom-nav {
    margin-top: 2rem;
    margin-bottom: 0;
}

.nav-btn {
    padding: 0.5rem 1.2rem;
    background-color: #8b4513;
    color: white;
    text-decoration: none;
    border-radius: 6px;
    font-size: 0.9rem;
    transition: background-color 0.2s;
}

.nav-btn:hover {
    background-color: #6b3410;
}

.nav-btn.disabled {
    background-color: #ccc;
    color: #888;
    pointer-events: none;
}

.sloka-indicator {
    font-size: 0.95rem;
    color: #6b5744;
    font-weight: 700;
}

/* Sloka Display */

.sloka-display {
    background: white;
    border: 2px solid #e8ddd0;
    border-radius: 12px;
    padding: 2rem;
    text-align: center;
    margin-bottom: 2rem;
}

.sloka-text {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 1.5rem;
    line-height: 2.2;
    color: #2d1b0e;
}

.sloka-number {
    margin-top: 1rem;
    font-size: 1rem;
    color: #8b4513;
}

/* Tabs */

.tabs-container {
    background: white;
    border-radius: 12px;
    border: 2px solid #e8ddd0;
    overflow: hidden;
}

.tab-bar {
    display: flex;
    flex-wrap: wrap;
    background-color: #f5ede3;
    border-bottom: 2px solid #e8ddd0;
}

.tab {
    padding: 0.75rem 1.2rem;
    border: none;
    background: none;
    font-family: 'Noto Sans Devanagari', sans-serif;
    font-size: 0.9rem;
    color: #6b5744;
    cursor: pointer;
    transition: all 0.2s;
    border-bottom: 3px solid transparent;
}

.tab:hover {
    background-color: #ebe2d6;
}

.tab.active {
    color: #8b4513;
    font-weight: 700;
    background-color: white;
    border-bottom-color: #8b4513;
}

.tab-panel {
    display: none;
    padding: 1.5rem 2rem;
}

.tab-panel.active {
    display: block;
}

/* Anvaya and Sandhi */

.anvaya-text,
.sandhi-text {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 1.2rem;
    line-height: 2;
    padding: 1rem;
    background-color: #fdf8f0;
    border-left: 4px solid #8b4513;
    border-radius: 0 8px 8px 0;
}

/* Tables */

.table-wrapper {
    overflow-x: auto;
}

.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.95rem;
}

.data-table th {
    background-color: #8b4513;
    color: white;
    padding: 0.75rem 1rem;
    text-align: left;
    font-weight: 700;
}

.data-table td {
    padding: 0.65rem 1rem;
    border-bottom: 1px solid #e8ddd0;
}

.data-table tbody tr:nth-child(even) {
    background-color: #fdf8f0;
}

.data-table tbody tr:hover {
    background-color: #f5ede3;
}

.sanskrit-cell {
    font-family: 'Tiro Devanagari Sanskrit', serif;
    font-size: 1.1rem;
}

/* Bhava Spashtikaranam */

.bhava-list {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

.bhava-item {
    padding: 1rem;
    background-color: #fdf8f0;
    border-radius: 8px;
    border: 1px solid #e8ddd0;
}

.prashna {
    margin-bottom: 0.75rem;
    font-size: 1.05rem;
}

.uttaram {
    font-size: 1.05rem;
    padding-left: 1rem;
    border-left: 3px solid #8b4513;
}

.label {
    display: inline-block;
    font-weight: 700;
    color: #8b4513;
    margin-right: 0.5rem;
    font-size: 0.85rem;
    text-transform: uppercase;
}

/* Empty State */

.empty-state {
    text-align: center;
    color: #999;
    font-style: italic;
    padding: 2rem;
}

/* Responsive */

@media (max-width: 600px) {
    .site-header {
        padding: 1rem;
    }

    .header-content h1 {
        font-size: 1.2rem;
    }

    .main-content {
        padding: 1rem;
    }

    .sloka-text {
        font-size: 1.2rem;
    }

    .sloka-display {
        padding: 1.5rem 1rem;
    }

    .tab {
        padding: 0.6rem 0.8rem;
        font-size: 0.8rem;
    }

    .tab-panel {
        padding: 1rem;
    }

    .anvaya-text,
    .sandhi-text {
        font-size: 1rem;
    }

    .nav-btn {
        padding: 0.4rem 0.8rem;
        font-size: 0.8rem;
    }

    .data-table {
        font-size: 0.85rem;
    }

    .data-table th,
    .data-table td {
        padding: 0.5rem 0.6rem;
    }
}

Secret mission

Add Student Progress Tracker

Your students can navigate through slokas, but they have no way to track which ones they have already studied. Can you add a progress tracker using browser localStorage so students can mark slokas as complete and see their overall progress?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your own PHP server. There are no cloud resources or paid services to worry about, so there are no ongoing costs.

Resources you used:

  • The gita-app project folder on your server (PHP files, CSS, JavaScript, JSON data).
  • Browser localStorage data (studiedSlokas key from the progress tracker).

Keep everything running

No action needed. Choose this if you are still actively building or your students are using the app.

  • Leave the gita-app folder on your server. Students can access the app anytime.
  • Update data/chapter16.json as you prepare content for slokas 5-24.
  • Browser localStorage persists automatically. Students will keep their progress across sessions.

Pause - I'll come back to this later

Since this project is just files on your server, there is nothing to pause. The app uses no background processes or running services beyond your PHP server itself.

  • If you want to temporarily hide the app from students, rename the gita-app folder or move it out of your web root.
  • Your files and content will remain intact when you move them back.

Delete - I don't want to use this again

Remove all project files and browser data to start fresh.

  • Delete the project folder by running this command from the folder that contains gita-app:

macOS

rm -rf gita-app

Windows

rmdir /s /q gita-app
  • Clear the browser localStorage data. Open your browser's developer console on the app page and run:
localStorage.removeItem('studiedSlokas');
  • Confirm the folder is gone by running ls in the parent directory. You should no longer see gita-app listed.

Well Done!

Well Done!

Nice work! You have built a fully functional, password-protected Sanskrit learning web app that serves Bhagavad Gita Chapter 16 content to your students in a structured, navigable format.

You've learned how to:

  • Build a PHP session-based authentication system that protects your content with password_hash() and password_verify().
  • Create a JSON-driven content architecture for Sanskrit sloka data that separates content from code, making it easy to update without touching PHP.
  • Build a tabbed, responsive interface displaying grammatical breakdowns with proper Devanagari web fonts (Tiro Devanagari Sanskrit for slokas, Noto Sans Devanagari for body text).
  • Secret Mission: Added a localStorage-based progress tracker so students can mark slokas as studied and see their completion status.

Ready to quiz yourself?