# Project export: BananaBreak

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2025
- Tagline: BananaBreak is built to help UCSC students find empty classrooms to relax, study, or hang out, during their breaks between classes.
- Devpost: https://devpost.com/software/slugspot
- GitHub: https://github.com/varunpalanisamy/BananaBreak
- Demo: http://bananabreak.tech/
- Video: https://www.youtube.com/embed/ZmNsxjhYp_A?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — varunpalanisamy (33 commits), sbelambe (15 commits)

## Devpost submission (written by the team)

### Inspiration

As students at UCSC that live off of campus, we always end up end up spending our time at one of the two main libraries; Science and Engineering, or Mchenry. While these are great locations to study, relax, and meet up with study groups/friends, we realized that there are times when we end up having to travel an absurd amount when these libraries are out of the way of our classes. We also realized that we have ended up limiting ourselves to taking advantage of all of the resources available on campus by going to only two study locations on campus that are booked up most of the time. Our goal is to find nearby available classrooms, food spots, and study locations on the UCSC campus to make the most of your time between classes, so we built BananaBreak!

### What it does

BananaBreak scrapes live UCSC class and discussions section data to figure out which classrooms are occupied and which ones are free at specific times within the day. Then it maps that data to easy to use frontend and a screen with a view of Google Maps that helps you find classrooms currently empty locate nearby study spots locate food options on campus find best placed on campus based on current location find study spots between your classes

### How we built it

Web scraping Selenium, BeautifulSoup to scrape live data from UCSC class search portal. Python (pandas) to preprocess the data Data Storage MongoDB Altas for easy access and history tracking Frontend Node.js and Express with EJS templates for easy navigation of classes Google Maps API for visualizing locations Backend render.com and .tech domain

### Challenges we ran into

Scraping Inconsisties: Scraping dynamic Javascript within the class search was difficult Data cleaning: took a long time to make sure the correct information was being parsed out, took a lot of trial and error Integrating both Google Maps API and the class search and free classroom finder was a struggle, but we were able to do it! We had lots of inconsistencies with the way we interpreted the scraped data and how we wanted to use it, so once we started connecting the backend with the frontend, we had to redo some parts so that all classes/spots were accounted for. Tried to use a different map visualization api, but found that there were a lot of inconsistencies with addresses and had locations that we could not find addresses for, so we had to improvise. Faced a similar problem again with Google Maps API, but we tinkered around with the commas within the dataset.

### Accomplishments we're proud of

Successfully scraped entire classroom availability from UCSC, and automated to MongoDB Built interactive map-based UI that shows live data (never used Google Maps API before) Deployed a fully working product(calendar, add/remove class, room schedule) Took a problem we face every day as students and turned it into a creative, deployable solution Encouraging students to discover more of campus rather than what they feel comfortable with

### What we learned

Applied what we learned about end-to-end systems with Python, Pandas, and MongoDB MODULARIZATION is so important and saved us when trying to integrate our individual parts and when trying to improve on features Learned more on node.js and express (we had never made such hefty applications with these tools before) DNS concepts such as A records, CNAME, and TTL values

### What's next

Focus on secondary lab locations for a small number of classes Add a social feature so students can see which spots are trending or where friends/study groups are meeting up between classes Improve upon our algorithm for finding close by classes/classes in between WRITE TESTS to verify the work we did, specifically with weird address locations Incorporate outdoor spaces and try to find either addresses for these places, or direct coordinates (effective for bench areas) Switch to MapBox from Google Maps API

## README (from the GitHub repository)

# BananaBreak: UCSC Classroom Finder

**BananaBreak** is a full-stack system built to help UCSC students find empty classrooms to relax, study, or hang out between classes.
---
![Screenshot of map page](screenshots/classmap.png)

## How to Run

### 1. Clone the repo & setup environment

```bash
git clone https://github.com/varunpalanisamy/BananaBreak.git
cd BananaBreak
python -m venv newenv
source newenv/bin/activate
pip install -r requirements.txt
npm install
```

---

### 2. Add Google Maps API Key

Create a file:

```
public/config.js
```

With the contents:

```js
"GOOGLE_MAPS_API_KEY": "YOUR_GOOGLE_MAPS_API_KEY"
```
Replace with your own API key. Make sure you enable:
- Maps JavaScript API
- Geocoding API
- Distance Matrix API

---

### 3. Run Website

```bash
node index.js
```

Visit `http://localhost:3000` in your browser.

---

## Dependencies

All dependencies are listed in `requirements.txt`:

- `selenium`
- `webdriver-manager`
- `pandas`
- `bs4`
- `pymongo`
- `streamlit`
- `urllib3`

Install all at once:

```bash
pip install -r requirements.txt
```

---

## Made by Varun Palanisamy and Shivani Belambe

*Project for UCSC – helping students find quiet classrooms and reduce stress during breaks between classes.*


## Detected evidence (automated analysis)

Indexed codebase: 14 recognized source files, 55 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- Streamlit (technology) — detected in the code

## Codebase structure (from repository index)

### Files (31 of 31)

```
.gitignore
index.js
package.json
public/data/all_discussion_sections_cleaned.csv
public/data/all_discussion_sections.csv
public/data/occupied_rooms.csv
public/data/ucsc_class_data_preprocessed.csv
public/data/ucsc_class_data.csv
public/mapboxCode/.gitignore
public/mapboxCode/classes_addresses.csv
public/mapboxCode/distance_finder.js
public/mapboxCode/find_addresses.py
public/mapboxCode/food_addresses.csv
public/mapboxCode/main.html
public/mapboxCode/public_addresses.csv
public/mapboxCode/studySpots.js
public/mapboxCode/test.csv
public/styles.css
README.md
requirements.txt
slugtime_api/app.py
slugtime_api/class_scraper.py
slugtime_api/combine_datasets.py
slugtime_api/preprocess_classes.py
slugtime_api/preprocess_discussions.py
slugtime_api/scrape_discussion.py
slugtime_api/store_in_mongo.py
views/home.ejs
views/map.ejs
views/pickaroom.ejs
views/schedule.ejs
```

### Dependencies

- package.json: csv-parser@^3.2.0, dayjs@^1.11.13, ejs@^3.1.10, express@^4.21.2
- requirements.txt: bs4, pandas@==1.5.3, pymongo, selenium, streamlit, urllib3@==1.26.20, webdriver-manager

### Recent commits (newest first)

- Update README.md
- Merge pull request #17 from varunpalanisamy/locationstudy
- images
- Merge pull request #16 from varunpalanisamy/locationstudy
- deployment
- Update README.md
- Update README.md
- Update README.md
- Update README.md
- Merge pull request #15 from varunpalanisamy/locationstudy
- formatting
- Merge pull request #14 from varunpalanisamy/locationstudy
- implemented mongodb database for slugtime
- Merge pull request #13 from varunpalanisamy/locationstudy
- final touches
- Merge pull request #12 from varunpalanisamy/locationstudy
- added a find study near me tool
- Merge pull request #11 from varunpalanisamy/fixed-map
- Merge branch 'main' into fixed-map
- fixed calendar home

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
selenium
webdriver-manager
pandas==1.5.3
bs4
pymongo
streamlit
urllib3==1.26.20
```

### package.json

```
{
    "name": "ucsc-classroom-finder",
    "version": "1.0.0",
    "description": "A UCSC free classroom finder built with Node.js and Express",
    "main": "index.js",
    "scripts": {
        "start": "node index.js"
    },
    "dependencies": {
        "csv-parser": "^3.2.0",
        "dayjs": "^1.11.13",
        "ejs": "^3.1.10",
        "express": "^4.21.2"
    }
}

```

### index.js

```javascript
const express = require("express");
const path = require("path");
const fs = require("fs");
const csv = require("csv-parser");
const dayjs = require("dayjs");
const customParseFormat = require("dayjs/plugin/customParseFormat");
dayjs.extend(customParseFormat);

const app = express();
const PORT = process.env.PORT || 3000;

app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.use(express.static(path.join(__dirname, "public")));

app.use(express.urlencoded({ extended: true }));

let classData = [];
fs.createReadStream(
  path.join(__dirname, "public", "data", "occupied_rooms.csv")
)
  .pipe(csv())
  .on("data", (row) => {
    classData.push(row);
  })
  .on("end", () => {
    console.log("CSV loaded for pick-a-room, total rows:", classData.length);
  });

let userClasses = [];

function parseDayTime(dtStr) {
  const regex = /([A-Za-z]+)\s+(\d{1,2}:\d{2}[AP]M)-(\d{1,2}:\d{2}[AP]M)/;
  const match = dtStr.match(regex);
  if (match) {
    return { days: match[1], start: match[2], end: match[3] };
  }
  return null;
}

function timeToMinutes(timeStr) {
  let parsed = dayjs(timeStr, "hh:mmA");
  if (!parsed.isValid()) {
    parsed = dayjs(timeStr, "h:mmA");
  }
  if (!parsed.isValid()) {
    console.log("Time parse fail:", timeStr);
    return 0;
  }
  const minutesFromMidnight = parsed.hour() * 60 + parsed.minute();
  return minutesFromMidnight - 420; 
}



// Root redirect to /home
app.get("/", (req, res) => {
  res.redirect("/home");
});


app.get("/home", (req, res) => {
  const day = req.query.day || "Monday";
  const dayMapping = {
    Monday: "M",
    Tuesday: "Tu",
    Wednesday: "W",
    Thursday: "Th",
    Friday: "F",
  };
  const dayAbbr = dayMapping[day] || "M";

  const scale = 0.75; 
  const totalMinutes = 1320 - 420;
  const calendarHeight = totalMinutes * scale;


  const bookings = userClasses

  .filter(c => c.days.includes(dayAbbr))
  .map(c => {
    const startOffset = timeToMinutes(c.start);
    const endOffset = timeToMinutes(c.end);
    const rawHeight = (endOffset - startOffset) * scale;
    const adjustedHeight = rawHeight - 5;
    return {
      className: c.className,
      timeRange: `${c.start} - ${c.end}`,
      top: startOffset * scale,
      height: adjustedHeight > 0 ? adjustedHeight : 0
    };
  });



  res.render("home", { day, bookings, scale, calendarHeight });
});


app.get("/config.js", (req, res) => {
  res.set("Content-Type", "application/javascript");
  res.send(`window.GOOGLE_MAPS_API_KEY = "${process.env.GOOGLE_MAPS_API_KEY}";`);
});

app.get("/map", (req, res) => {
  res.render("map");
});


// Returns a JSON array of the user's classes for Monday
app.get("/api/myclasses", (req, res) => {

  const day = "Monday";
  const dayMapping = {
    Monday: "M",
    Tuesday: "Tu",
    Wednesday: "W",
    Thursday: "Th",
    Friday: "F",
  };
  const dayAbbr = dayMapping[day] || "M";


  const mondayClasses = userClasses.filter((c) => c.days.includes(dayAbbr));

  res.json(mondayClasses);
});


app.get("/home/freerooms", (req, res) => {
  const day = req.query.day || "Monday";
  const timeStr = req.query.time; // e.g. "01:30PM"
  if (!timeStr) {
    return res.json([]);
  }

  const dayMapping = {
    Monday: "M",
    Tuesday: "Tu",
    Wednesday: "W",
    Thursday: "Th",
    Friday: "F",
  };
  const dayAbbr = dayMapping[day] || "M";
  const clickOffset = timeToMinutes(timeStr);

  const classesForDay = classData.filter((row) => {
    const dt = parseDayTime(row["Day/Time"]);
    if (!dt) return false;
    return dt.days.includes(dayAbbr);
  });

  let allRooms = new Set();
  for (let row of classesForDay) {
    let loc = row["Location"].replace(/^[A-Z]+:\s*/i, "").trim();
    allRooms.add(loc);
  }

  // Remove rooms that are occupied at the clicked time.
  for (let row of classesForDay) {
    const dt = parseDayTime(row["Day/Time"]);
    if (!dt) continue;
    const startMins = timeToMinutes(dt.start);
    const endMins = timeToMinutes(dt.end);
    if (clickOffset >= startMins && clickOffset < endMins) {
      let loc = row["Location"].replace(/^[A-Z]+:\s*/i, "").trim();
      allRooms.delete(loc);
    }
  }

  // Filter out bad room names.
  const disallowedPrefixes = [
    /^TA\s/i,
    /^CoastBio/i,
    /^Music Center/i,
    /^LEC/i,
    /^STU/i,
    /^SiliconValleyCtr/i,
    /^50 Mtr Pool/i,
    /^Harbor/i,
    /^East Gym/i,
    /^West Tennis Ct/i,
    /^East Field/i,
    /^OPERS Multi Purpose/i,
    /^Martial Arts/i,
    /^McHenry Lib/i,
    /^Sci & Engr Library/i,
    /^Ocean/i,
    /^LG/i,
    /^Biomed/i,
  ];
  function isDisallowed(room) {
    return disallowedPrefixes.some((pattern) => pattern.test(room));
  }
  let filteredRooms = Array.from(allRooms).filter(
    (room) => !isDisallowed(room)
  );
  filteredRooms.sort((a, b) =>
    a.localeCompare(b, "en", { sensitivity: "base" })
  );

  return res.json(filteredRooms);
});

app.get("/schedule", (req, res) => {
  const allClasses = Array.from(
    new Set(classData.map((row) => row["Class"].trim()))
  ).sort();
  res.render("schedule", { allClasses, userClasses });
});

app.post("/schedule", (req, res) => {
  const classCode = req.body.classCode.trim();
  if (!classCode) {
    return res.send("Please enter a class code.");
  }
  const record = classData.find((r) => r["Class"].trim().startsWith(classCode));
  if (!record) {
    return res.send("Could not find a class matching that code.");
  }
  const dt = parseDayTime(record["Day/Time"]);
  if (!dt) {
    return res.send("Found class, but could not parse its Day/Time.");
  }
  userClasses.push({
    className: record["Class"],
    location: record["Location"],
    days: dt.days,
    start: dt.start,
    end: dt.end,
  });

  res.redirect("/schedule");
});

app.post("/schedule/remove", (req, res) => {
  const classToRemove = req.body.classToRemove;
  userClasses = userClasses.filter((c) => c.className !== classToRemove);
  res.redirect("/schedule");
});

app.get("/pickaroom", (req, res) => {
  const day = req.query.
[truncated — 1664 more characters]
```

### slugtime_api/app.py

```python
import streamlit as st
import pandas as pd
import re

def parse_day_time(dt_str):
    pattern = r"([A-Za-z]+)\s+(\d{1,2}:\d{2}[AP]M)-(\d{1,2}:\d{2}[AP]M)"
    match = re.match(pattern, dt_str)
    if match:
        days, start, end = match.groups()
        return days, start, end
    return None, None, None

def load_and_normalize_data():

    df = pd.read_csv("ucsc_class_data_preprocessed.csv")
    
    parsed = df["Day/Time"].apply(lambda x: pd.Series(parse_day_time(x)) if isinstance(x, str) else pd.Series([None, None, None]))
    parsed.columns = ["Days", "Start Time", "End Time"]
    
    df = pd.concat([df, parsed], axis=1)
    return df

def main():
    st.title("UCSC Free Classroom Finder")
    st.write("Use the sidebar filters to view free classrooms by day and/or location.")
    
    df = load_and_normalize_data()
    
    # st.write("### Full Dataset Preview (first 10 rows)")
    # st.dataframe(df.head(10))
 
    st.sidebar.header("Filters")
    
    days_options = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
    selected_days = st.sidebar.multiselect("Select day(s) to filter on", days_options, default=days_options)
    
    day_mapping = {
        "Monday": "M",
        "Tuesday": "Tu",
        "Wednesday": "W",
        "Thursday": "Th",
        "Friday": "F"
    }
    
    location_filter = st.sidebar.text_input("Filter by location (text match):", "")
    
    filtered_df = df.copy()
    
    if selected_days:
        mask = pd.Series(True, index=filtered_df.index)
        for day in selected_days:
            abbr = day_mapping.get(day)
            mask = mask & filtered_df["Days"].fillna("").str.contains(abbr, case=False, na=False)
        filtered_df = filtered_df[mask]

    normalized_location = filtered_df["Location"].str.replace(r"^[A-Z]+:\s*", "", regex=True)

    if location_filter:
        filtered_df = filtered_df[normalized_location.str.contains(location_filter, case=False, na=False)]

    
    st.write("### Filtered Results")
    st.dataframe(filtered_df)
    
    csv = filtered_df.to_csv(index=False).encode('utf-8')
    st.download_button(
        label="Download filtered data as CSV",
        data=csv,
        file_name="filtered_ucsc_class_data.csv",
        mime="text/csv",
    )

if __name__ == "__main__":
    main()

```

### slugtime_api/combine_datasets.py

```python
import pandas as pd

classes_df = pd.read_csv("data/ucsc_class_data_preprocessed.csv")

discussions_df = pd.read_csv("all_discussion_sections_cleaned.csv")

if "Section" in discussions_df.columns:
    discussions_df.rename(columns={"Section": "Class"}, inplace=True)

occupied_rooms = pd.concat([classes_df, discussions_df], ignore_index=True)

occupied_rooms.sort_values(by="Class", inplace=True)
occupied_rooms.reset_index(drop=True, inplace=True)

occupied_rooms.to_csv("occupied_rooms.csv", index=False)

print("Combined master schedule saved to 'occupied_rooms.csv'.")

```

### slugtime_api/store_in_mongo.py

```python
import pandas as pd
from pymongo import MongoClient
from urllib.parse import quote_plus  

def store_csv_in_mongo(csv_file, quarter_label):
    username = "" # protected
    password = ""  # protected

    encoded_password = quote_plus(password)

    uri = f"mongodb+srv://{username}:{encoded_password}@spring2025ucsc.r8ppykq.mongodb.net/?retryWrites=true&w=majority&appName=Spring2025UCSC"

    client = MongoClient(uri)

    db = client["SlugtimeDB"]
    coll = db["occupiedRoomsHistory"]

    df = pd.read_csv(csv_file)
    records = df.to_dict(orient="records")

    for r in records:
        r["quarter"] = quarter_label

    if records:
        coll.insert_many(records)
        print(f"Uploaded {len(records)} records to MongoDB Atlas for {quarter_label}")
    else:
        print("No records found in CSV.")

if __name__ == "__main__":
    store_csv_in_mongo("occupied_rooms.csv", "Spring Quarter 2025")

```

### slugtime_api/preprocess_discussions.py

```python
import pandas as pd
import os

def preprocess_discussion_sections(input_csv="all_discussion_sections.csv",
                                   output_csv="all_discussion_sections_cleaned.csv"):
    df = pd.read_csv(input_csv)
    
    df.rename(columns={"Section": "Class"}, inplace=True)
    
    for col in ['Class', 'Location', 'Day/Time']:
        df[col] = df[col].astype(str).str.strip()
    
    df = df.dropna(subset=["Class", "Location", "Day/Time"])
    
    for col in ["Class", "Location", "Day/Time"]:
        df = df[df[col].str.lower() != "nan"]
    
    df = df[(df['Day/Time'] != "") & (df['Location'] != "")]
    
    df = df[~df['Location'].str.contains("remote", case=False, na=False)]
    
    df = df[~df['Class'].str.contains("cancelled|tbd", case=False, na=False)]
    df = df[~df['Location'].str.contains("cancelled|tbd", case=False, na=False)]
    df = df[~df['Day/Time'].str.contains("cancelled|tbd", case=False, na=False)]
    
    df.to_csv(output_csv, index=False)
    print(f"Preprocessing complete. Cleaned data saved to {output_csv}")

if __name__ == "__main__":
    preprocess_discussion_sections()

```

### slugtime_api/preprocess_classes.py

```python
import pandas as pd

def preprocess_classes():
    input_file = "ucsc_class_data.csv"
    output_file = "ucsc_class_data_preprocessed.csv"
    
    df = pd.read_csv(input_file)
    
    def should_remove(row):
        location = row["Location"] if pd.notna(row["Location"]) else ""
        day_time = row["Day/Time"] if pd.notna(row["Day/Time"]) else ""
        
        loc_lower = location.lower()
        dt_lower = day_time.lower().strip()
        
        if "online" in loc_lower or "remote instruction" in loc_lower or "digital arts" in loc_lower:
            return True
        
        if "tbd in person" in loc_lower:
            return True
        
        if "cancelled" in dt_lower:
            return True
        
        if dt_lower == "day and time:":
            return True
        
        return False

    df_filtered = df[~df.apply(should_remove, axis=1)]
    
    df_filtered.to_csv(output_file, index=False)
    
    return df_filtered

if __name__ == "__main__":
    df_preprocessed = preprocess_classes()
    print("Preprocessing complete.")
    print("New dataset has", len(df_preprocessed), "rows.")
    print(df_preprocessed.head())

```

### slugtime_api/class_scraper.py

```python
import time
import pandas as pd
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
from selenium.common.exceptions import NoSuchElementException, TimeoutException

def scrape_ucsc_classes():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless") 
    options.add_argument("--disable-gpu")

    driver = webdriver.Chrome(
        service=ChromeService(ChromeDriverManager().install()),
        options=options
    )

    wait = WebDriverWait(driver, 10)

    driver.get("https://pisa.ucsc.edu/class_search/")

    term_dropdown = Select(wait.until(EC.element_to_be_clickable((By.ID, "term_dropdown"))))
    term_dropdown.select_by_value("2252")
    # term_dropdown.select_by_value(term_code)


    reg_status_dropdown = Select(wait.until(EC.element_to_be_clickable((By.ID, "reg_status"))))
    reg_status_dropdown.select_by_value("all")

    search_button = wait.until(
        EC.element_to_be_clickable((By.XPATH, "//input[@type='submit' and @value='Search']"))
    )
    search_button.click()

    time.sleep(3)

    data = []
    page_count = 1

    while True:
        print(f"Scraping page {page_count}...")

        try:
            panel_divs = wait.until(
                EC.presence_of_all_elements_located((By.XPATH, "//div[starts-with(@id,'rowpanel_')]"))
            )
        except TimeoutException:
            print("No panels found on the page.")
            break

        for i, panel in enumerate(panel_divs):

            try:
                class_elem = panel.find_element(By.XPATH, ".//div[contains(@class,'panel-heading-custom')]//h2/a")
                class_name = class_elem.text.strip()
            except NoSuchElementException:
                class_name = "N/A"

            location_text = "N/A"
            day_time_text = "N/A"
            try:
                info_container = panel.find_element(
                    By.XPATH,
                    ".//div[contains(@class,'panel-body')]//div[contains(@class,'col-xs-12') and contains(@class,'col-sm-6')]"
                )
                sub_divs = info_container.find_elements(
                    By.XPATH,
                    ".//div[contains(@class,'col-xs-6') and contains(@class,'col-sm-6')]"
                )
                if len(sub_divs) >= 2:
                    location_text = sub_divs[0].text.strip().replace("Location:\n", "")
                    day_time_text = sub_divs[1].text.strip().replace("Day and Time:\n", "")
            except NoSuchElementException:
                pass

            data.append({
                "Class": class_name,
                "Location": location_text,
                "Day/Time": day_time_text
            })

        try:
            next_button = wait.until(
                EC.element_to_be_clickable(
                    (By.XPATH, "//div[contains(@class,'row hide-print')]//a[contains(., 'next')]")
                )
            )
            next_button.click()
            page_count += 1
            time.sleep(3)
        except (NoSuchElementException, TimeoutException):
            print("No more pages found. Ending scrape.")
            break

    driver.quit()

    df = pd.DataFrame(data)
    return df

if __name__ == "__main__":
    df_classes = scrape_ucsc_classes()
    print(df_classes)
    df_classes.to_csv("ucsc_class_data.csv", index=False)

```

### public/styles.css

```css
/* Base styles */
:root {
  --primary: #13315c;
  --primary-light: #134074;
  --secondary: #8da9c4;
  --accent: #0077cc;
  --background: #f8fafc;
  --surface: #ffffff;
  --text: #1a1a1a;
  --text-light: #666666;
  --border: #e2e8f0;
  --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  background-color: var(--background);
  color: var(--text);
  line-height: 1.5;
  margin: 0;
  padding: 0;
}

/* Navigation */
.top-nav {
  background: linear-gradient(to right, var(--primary), var(--primary-light));
  padding: 1rem 2rem;
  box-shadow: var(--shadow);
}

.top-nav a {
  color: var(--surface);
  text-decoration: none;
  padding: 0.5rem 1rem;
  border-radius: 0.375rem;
  transition: background-color 0.2s;
}

.top-nav a:hover {
  background-color: rgba(255, 255, 255, 0.1);
}

/* Main Content */
.main-content {
  max-width: 1400px;
  margin: 0 auto;
  padding: 2rem;
}

/* Calendar Styling */
.calendar-wrapper {
  background: var(--surface);
  border-radius: 0.75rem;
  box-shadow: var(--shadow);
  overflow: hidden;
}

.time-axis {
  background-color: var(--background);
  border-right: 1px solid var(--border);
  padding: 0.5rem;
}

.time-label {
  color: var(--text-light);
  font-size: 0.875rem;
}

.calendar-container {
  background-color: var(--surface);
  border: none;
  border-radius: 0;
}

.booking {
  background-color: rgba(19, 49, 92, 0.1);
  border: 1px solid var(--primary);
  border-radius: 0.375rem;
  color: var(--primary);
  font-size: 0.875rem;
  transition: transform 0.2s;
}

.booking:hover {
  transform: scale(1.005);
}

.hour-line {
  border-top: 1px solid var(--border);
}

/* Form Elements */
select, input, button {
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0.375rem;
  padding: 0.5rem 1rem;
  font-size: 1rem;
  transition: all 0.2s;
}

select:hover, input:hover {
  border-color: var(--secondary);
}

select:focus, input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(0, 119, 204, 0.1);
}

button {
  background-color: var(--accent);
  color: white;
  border: none;
  cursor: pointer;
  font-weight: 500;
}

button:hover {
  background-color: #0066b3;
}

/* Details Panel */
.details-panel {
  background: var(--surface);
  border-radius: 0.75rem;
  box-shadow: var(--shadow);
  padding: 1.5rem;
  border: none;
}

details {
  margin-bottom: 1rem;
}

details summary {
  padding: 0.75rem;
  background-color: var(--background);
  border-radius: 0.375rem;
  cursor: pointer;
  font-weight: 500;
  transition: background-color 0.2s;
}

details summary:hover {
  background-color: #edf2f7;
}

details[open] summary {
  margin-bottom: 0.75rem;
}

.room-link {
  color: var(--accent);
  text-decoration: none;
  display: block;
  padding: 0.5rem;
  border-radius: 0.375rem;
  transition: background-color 0.2s;
}

.room-link:hover {
  background-color: var(--background);
  text-decoration: none;
}

/* Split Container */
.split-container {
  display: grid;
  grid-template-columns: 350px 1fr;
  gap: 2rem;
  margin-top: 2rem;
}

/* Responsive Design */
@media (max-width: 1024px) {
  .split-container {
    grid-template-columns: 1fr;
  }
  
  .calendar-container {
    width: 100%;
  }
}

/* Class List */
.class-list {
  list-style: none;
  padding: 0;
}

.class-list li {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem;
  background-color: var(--background);
  border-radius: 0.375rem;
  margin-bottom: 0.5rem;
}

.remove-form button {
  background-color: #ef4444;
  padding: 0.25rem 0.75rem;
  font-size: 0.875rem;
}

.remove-form button:hover {
  background-color: #dc2626;
}

/* Headers */
h1, h2, h3 {
  color: var(--primary);
  margin-bottom: 1.5rem;
}

h1 {
  font-size: 2rem;
  font-weight: 700;
}

h2 {
  font-size: 1.5rem;
  font-weight: 600;
}

/* Form Groups */
.form-group {
  margin-bottom: 1.5rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  color: var(--text-light);
  font-weight: 500;
}

/* Add loading state */
.loading {
  opacity: 0.7;
  pointer-events: none;
}

/* Add smooth transitions */
* {
  transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
}
```

[5 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]