From 539ef6568697c9dc1712fc7a2a379d4e3fb9235c Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Tue, 7 Oct 2025 13:47:34 -0700 Subject: Initial commit --- .gitignore | 5 + CMakeLists.txt | 40 +++ README.md | 4 + src/main.cpp | 87 ++++++ src/markdown_translator.cpp | 264 ++++++++++++++++++ src/markdown_translator.h | 35 +++ styles/ffxiv-style.css | 649 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1084 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 src/main.cpp create mode 100644 src/markdown_translator.cpp create mode 100644 src/markdown_translator.h create mode 100644 styles/ffxiv-style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7929f93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +*.o +*.exe +*.out +sample.md diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7c6221e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.16) +project(FFXIV-MD-GENERATOR VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if (MSVC) + add_compile_options(/W4 /permissive-) +else() + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# Debug information +message(STATUS "Source directory: ${CMAKE_SOURCE_DIR}") +message(STATUS "Binary directory: ${CMAKE_BINARY_DIR}") +message(STATUS "Source styles dir: ${CMAKE_SOURCE_DIR}/styles") +message(STATUS "Target styles dir: ${CMAKE_BINARY_DIR}/styles") + +# Main executable links to the library +add_executable(${PROJECT_NAME} + src/main.cpp + src/markdown_translator.cpp +) +# target_link_libraries(${PROJECT_NAME} PRIVATE mylib) + +# Create a custom target that will always be built +add_custom_target(copy_styles ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/styles + ${CMAKE_BINARY_DIR}/styles + COMMENT "Copying styles directory to build directory" + VERBATIM +) + +# Make sure the styles are copied before the main executable is built +add_dependencies(${PROJECT_NAME} copy_styles) + +# Create build/styles directory if it doesn't exist (as a fallback) +file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/styles) diff --git a/README.md b/README.md new file mode 100644 index 0000000..97590d4 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Wiki-MD2HTML (WIP) +A basic C++ parser that converts a specialized format of Markdown to a themed HTML style. + +Designed for presenting information on-screen in a more "markdown-like" Wiki format diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..caee08c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include "markdown_translator.h" + +bool parseArguments(int argc, char* argv[], std::unordered_map& params, std::string& inputFile) { + if (argc < 2) { + std::cerr << "Usage: COMMAND [-o ] [-css ] [other options]\n"; + return false; + } + params["-o"] = "index.html"; + params["-css"] = "styles/ffxiv-style.css"; + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (arg[0] == '-') { + if (i + 1 < argc) { + params[arg] = argv[i + 1]; + ++i; + } else { + std::cerr << "Error: Missing value for option " << arg << "\n"; + return false; + } + } else { + if (inputFile.empty()) { + inputFile = arg; + } else { + std::cerr << "Error: Multiple input files specified: " << inputFile << " and " << arg << "\n"; + return false; + } + } + } + + if (inputFile.empty()) { + std::cerr << "Error: No input file specified\n"; + return false; + } + + return true; +} + +int main(int argc, char* argv[]) { + std::unordered_map params; + std::string inputFile; + + if (!parseArguments(argc, argv, params, inputFile)) { + return 1; + } + + std::ifstream file(inputFile); + if (!file) { + std::cerr << "Error: Could not open file " << inputFile << "\n"; + return 1; + } + + // Read entire file content + std::stringstream buffer; + buffer << file.rdbuf(); + std::string markdownContent = buffer.str(); + file.close(); + + // Create translator and convert markdown to HTML + MarkdownTranslator translator; + + // Get CSS path from parameters or use default + std::string cssPath = "styles/ffxiv-style.css"; + if (params.find("-css") != params.end()) { + cssPath = params["-css"]; + } + + std::string htmlOutput = translator.translate(markdownContent, cssPath, "XIV Lore"); + + // Write to output file + std::string outputFile = params["-o"]; + std::ofstream outFile(outputFile); + if (!outFile) { + std::cerr << "Error: Could not open output file " << outputFile << "\n"; + return 1; + } + + outFile << htmlOutput; + outFile.close(); + + std::cout << "HTML output written to " << outputFile << std::endl; + return 0; +} diff --git a/src/markdown_translator.cpp b/src/markdown_translator.cpp new file mode 100644 index 0000000..d362f82 --- /dev/null +++ b/src/markdown_translator.cpp @@ -0,0 +1,264 @@ +#include "markdown_translator.h" +#include +#include +#include + +MarkdownTranslator::MarkdownTranslator(){ +} + +MarkdownTranslator::~MarkdownTranslator() { +} + +std::string MarkdownTranslator::translate(const std::string& markdownContent, const std::string& cssPath, const std::string& title) { + std::stringstream htmlOutput; + std::stringstream markdownStream(markdownContent); + std::string line; + std::vector headers; + + std::string currentLine; + + + while (std::getline(markdownStream, currentLine)) { + std::regex headerRegex("^(#{1,3})\\s+(.*)$"); + std::smatch matches; + if (std::regex_match(currentLine, matches, headerRegex)) { + int level = matches[1].length(); + std::string content = matches[2]; + if (level <= 3) { + headers.push_back(std::to_string(level) + ":" + content); + } + } + } + + // Reset the stream to start over + markdownStream.clear(); + markdownStream.str(markdownContent); + + // Start with basic HTML structure + htmlOutput << "\n"; + htmlOutput << "\n"; + htmlOutput << "\n"; + htmlOutput << " \n"; + htmlOutput << " \n"; + htmlOutput << " " + title + "\n"; + htmlOutput << " \n"; + htmlOutput << "\n"; + htmlOutput << "\n"; + + // Add navigation sidebar + generateSideBar(htmlOutput, headers, title); + + // Main content container + htmlOutput << "
\n"; + htmlOutput << "
\n"; + + // Process each line of markdown + + // State of current parse + bool inFigureBlock{false}; + std::vector figureLines; + + while (std::getline(markdownStream, line)) { + if(line.find(":::figure") == 0){ + inFigureBlock = true; + figureLines.clear(); + continue; + } + + if(inFigureBlock && line == ":::"){ + inFigureBlock = false; + htmlOutput << " " << processFigureBlock(figureLines) << "\n"; + continue; + } + else if(inFigureBlock){ + figureLines.push_back(line); + continue; + } + + htmlOutput << " " << processLine(line); + } + + // Add article meta information section + htmlOutput << "
\n"; + htmlOutput << "

Last updated: " << getCurrentDateTime() << "

\n"; + htmlOutput << "
\n"; + + htmlOutput << "
\n"; + htmlOutput << "
\n"; + htmlOutput << "\n"; + htmlOutput << "\n"; + return htmlOutput.str(); +} + +void MarkdownTranslator::generateSideBar(std::stringstream& output, const std::vector& headers, const std::string& title) { + output << "
\n"; + output << "
\n"; + output << "

" + title + "

\n"; + output << "
\n"; + output << "
    \n"; + + output << "

    Table of Contents

    \n"; + output << "
  • Home
  • \n"; + output << "
  • Recent Changes
  • \n"; + output << "
  • Random Page
  • \n"; + output << "
      \n"; + + // Generate navigation from headers + for (const auto& header : headers) { + size_t separatorPos = header.find(':'); + if (separatorPos != std::string::npos) { + int level = std::stoi(header.substr(0, separatorPos)); + std::string content = header.substr(separatorPos + 1); + + // Create anchor ID from header content + std::string anchorId = createAnchorId(content); + + // Add indentation based on header level + std::string indentation = ""; + for (int i = 1; i < level; ++i) { + indentation += " "; + } + + output << " " << indentation << "
    • " << content << "
    • \n"; + } + } + + output << "
    \n"; + output << "
\n"; + output << "
\n"; +} + +std::string MarkdownTranslator::createAnchorId(const std::string& text) { + std::string id = text; + // Convert to lowercase + std::transform(id.begin(), id.end(), id.begin(), ::tolower); + + // Replace spaces with hyphens + std::replace(id.begin(), id.end(), ' ', '-'); + + // Remove any non-alphanumeric characters except hyphens + id.erase( + std::remove_if( + id.begin(), + id.end(), + [](char c) { return !(std::isalnum(c) || c == '-'); } + ), + id.end() + ); + + return id; +} + +std::string MarkdownTranslator::getCurrentDateTime() { + std::time_t now = std::time(nullptr); + std::tm* localTime = std::localtime(&now); + + char buffer[80]; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", localTime); + + return std::string(buffer); +} + +std::string MarkdownTranslator::processLine(const std::string& line) { + std::string processed = line; + + // Check for headers first (they start at beginning of line) + processed = processHeaders(processed); + + // If it wasn't a header, process inline elements + if (processed == line) { + processed = processBold(processed); + processed = processItalic(processed); + processed = processLinks(processed); + + // Wrap in paragraph tags if it's regular text + if (!processed.empty() && processed[0] != '<') { + processed = processParagraph(processed); + } + } + + return processed + "\n"; +} + +std::string MarkdownTranslator::processHeaders(const std::string& line) { + // Check for H1-H6 + std::regex headerRegex("^(#{1,6})\\s+(.*)$"); + std::smatch matches; + if (std::regex_match(line, matches, headerRegex)) { + int level = matches[1].length(); + std::string content = matches[2]; + std::string anchorId = createAnchorId(content); + return "" + content + ""; + } + + return line; +} + +std::string MarkdownTranslator::processBold(const std::string& text) { + // Replace **text** or __text__ with text + std::string result = text; + std::regex boldRegex("\\*\\*([^\\*]+)\\*\\*|__([^_]+)__"); + result = std::regex_replace(result, boldRegex, "$1$2"); + return result; +} + +std::string MarkdownTranslator::processItalic(const std::string& text) { + // Replace *text* or _text_ with text + std::string result = text; + std::regex italicRegex("\\*([^\\*]+)\\*|_([^_]+)_"); + result = std::regex_replace(result, italicRegex, "$1$2"); + return result; +} + +std::string MarkdownTranslator::processLinks(const std::string& text) { + // Replace [text](url) with text + std::string result = text; + std::regex linkRegex("\\[([^\\]]+)\\]\\(([^\\)]+)\\)"); + result = std::regex_replace(result, linkRegex, "$1"); + return result; +} + +std::string MarkdownTranslator::processParagraph(const std::string& text) { + if (text.empty()) return ""; + return "

" + text + "

"; +} + +std::string MarkdownTranslator::processSingleFigure(const std::string& text) { + // Extract image details using regex + std::regex imageRegex("!\\[(.*?)\\]\\(([^\\s\"]+)(\\s+\"(.*?)\")?\\)"); + std::smatch matches; + + if (std::regex_search(text, matches, imageRegex)) { + std::string alt = matches[1].str(); + std::string src = matches[2].str(); + std::string title = matches.size() > 4 && matches[4].matched ? " title=\"" + matches[4].str() + "\"" : ""; + // Create the HTML for the image + return "\"""; + } + return text; +} + +std::string MarkdownTranslator::processFigureBlock(const std::vector& lines){ + std::stringstream html; + std::string imageHtml; + std::string caption; + html << "
\n"; + + // Process each line in the figure block + for (const auto& line : lines) { + if (line.find("![") == 0) { + // Process the image + imageHtml = processSingleFigure(line); + html << " " << imageHtml << "\n"; + } else if (line.find("Caption:") == 0) { + // Extract caption + caption = line.substr(8); + html << "
" << caption << "
\n"; + } else if (!line.empty()) { + // Process any other content + html << "

" << line << "

\n"; + } + } + html << "
"; + return html.str(); +} diff --git a/src/markdown_translator.h b/src/markdown_translator.h new file mode 100644 index 0000000..6d24ac7 --- /dev/null +++ b/src/markdown_translator.h @@ -0,0 +1,35 @@ +#ifndef MARKDOWN_TRANSLATOR_H +#define MARKDOWN_TRANSLATOR_H + +#include +#include +#include +#include + +class MarkdownTranslator { +public: + // Constructor + MarkdownTranslator(); + // Destructor + ~MarkdownTranslator(); + // Main translation function - takes markdown content and returns HTML + std::string translate(const std::string& markdownContent, const std::string& cssPath = "styles/ffxiv-style.css", const std::string& title = "Title"); + std::string processLine(const std::string& line); + +private: + // Helper functions for different markdown elements + std::string processHeaders(const std::string& line); + std::string processBold(const std::string& text); + std::string processItalic(const std::string& text); + std::string processLinks(const std::string& text); + std::string processParagraph(const std::string& text); + std::string processSingleFigure(const std::string& text); + std::string processFigureBlock(const std::vector& lines); + // Navigation and table of contents + void generateSideBar(std::stringstream& output, const std::vector& headers, const std::string& title); + std::string createAnchorId(const std::string& text); + // Utility functions + std::string getCurrentDateTime(); +}; + +#endif // MARKDOWN_TRANSLATOR_H diff --git a/styles/ffxiv-style.css b/styles/ffxiv-style.css new file mode 100644 index 0000000..2b1c0e9 --- /dev/null +++ b/styles/ffxiv-style.css @@ -0,0 +1,649 @@ +/* FFXIV Wiki-inspired CSS */ +/* Base typography and colors inspired by the Lodestone and Wiki style */ + +:root { + /* Color palette inspired by FFXIV UI */ + --primary-bg: #1a1a1a; + --secondary-bg: #252525; + --tertiary-bg: #2d2d2d; + --accent-gold: #c9aa71; + --accent-blue: #5b8fc7; + --text-primary: #e2e2e2; + --text-secondary: #b8b8b8; + --text-dim: #888888; + --border-color: #3a3a3a; + --link-color: #7db8e8; + --link-hover: #a5d0f0; + --header-gradient-start: #2a2a2a; + --header-gradient-end: #1a1a1a; + --code-bg: #0d0d0d; + --nav-bg: #1f1f1f; + --nav-hover: #333333; +} + +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Meiryo", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + background-color: var(--primary-bg); + color: var(--text-primary); + line-height: 1.7; + min-height: 100vh; + display: flex; + background-image: repeating-linear-gradient( + 45deg, + transparent, + transparent 10px, + rgba(255, 255, 255, 0.01) 10px, + rgba(255, 255, 255, 0.01) 20px + ); +} + +/* Navigation sidebar */ +.nav-sidebar { + width: 250px; + background-color: var(--nav-bg); + border-right: 1px solid var(--border-color); + height: 100vh; + position: fixed; + overflow-y: auto; + padding: 20px 0; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2); +} + +.nav-logo { + padding: 15px 20px; + margin-bottom: 20px; + border-bottom: 1px solid var(--border-color); + text-align: center; +} + +.nav-logo h3 { + color: var(--accent-gold); + font-size: 1.2em; + margin: 0; +} + +.nav-menu { + list-style-type: none; + padding: 0; + margin: 0; +} + +.nav-menu li { + margin: 0; +} + +.nav-menu a { + display: block; + padding: 10px 20px; + color: var(--text-secondary); + text-decoration: none; + border-left: 3px solid transparent; + transition: all 0.3s ease; +} + +.nav-menu a:hover { + background-color: var(--nav-hover); + color: var(--accent-gold); + border-left-color: var(--accent-gold); + text-shadow: none; +} + +.nav-menu a::after { + display: none; +} + +.nav-menu h4 { + padding: 15px 20px 5px; + color: var(--accent-gold); + font-size: 1em; + text-transform: uppercase; + letter-spacing: 1px; +} + +.nav-submenu { + list-style-type: none; + padding-left: 15px; +} + +/* Main content */ +.main-content { + flex: 1; + margin-left: 250px; + width: calc(100% - 250px); +} + +/* Container */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; + background-color: rgba(26, 26, 26, 0.95); + min-height: 100vh; + box-shadow: 0 0 50px rgba(0, 0, 0, 0.5); +} + +/* Headers with FFXIV styling */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Jupiter Pro", "Cinzel", "Georgia", serif; + color: var(--accent-gold); + margin-top: 1.5em; + margin-bottom: 0.8em; + font-weight: 500; + letter-spacing: 0.5px; + position: relative; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); +} + +h1 { + font-size: 2.2em; + padding-bottom: 0.5em; + border-bottom: 3px solid var(--accent-gold); + background: linear-gradient(90deg, var(--accent-gold), transparent 70%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: none; + margin-top: 0.5em; +} + +h1::after { + content: ""; + display: block; + position: absolute; + bottom: -3px; + left: 0; + width: 100%; + height: 3px; + background: linear-gradient(90deg, var(--accent-gold), transparent 70%); +} + +h2 { + font-size: 1.8em; + color: var(--accent-blue); + padding-left: 15px; + border-left: 4px solid var(--accent-blue); + margin-left: -15px; +} + +h3 { + font-size: 1.4em; + color: var(--text-primary); +} + +h4 { + font-size: 1.2em; + color: var(--text-secondary); +} + +h5, +h6 { + font-size: 1.1em; + color: var(--text-secondary); +} + +/* Paragraphs and text */ +p { + margin-bottom: 1.2em; + color: var(--text-secondary); + text-align: left; +} + +/* Links with FFXIV styling */ +a { + color: var(--link-color); + text-decoration: none; + position: relative; + transition: color 0.3s ease; + font-weight: 500; +} + +a:hover { + color: var(--link-hover); + text-shadow: 0 0 10px rgba(125, 184, 232, 0.5); +} + +a::after { + content: ""; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 1px; + background-color: var(--link-hover); + transition: width 0.3s ease; +} + +a:hover::after { + width: 100%; +} + +/* Bold and Italic */ +strong, +b { + color: var(--accent-gold); + font-weight: 600; +} + +em, +i { + color: var(--text-primary); + font-style: italic; +} + +/* Lists with FFXIV styling */ +ul, +ol { + margin-left: 30px; + margin-bottom: 1.2em; +} + +li { + margin-bottom: 0.5em; + color: var(--text-secondary); +} + +ul li::marker { + color: var(--accent-gold); +} + +ol li::marker { + color: var(--accent-blue); + font-weight: bold; +} + +/* Code blocks with FFXIV terminal style */ +pre { + background-color: var(--code-bg); + border: 1px solid var(--border-color); + border-left: 4px solid var(--accent-blue); + padding: 15px; + margin: 1.5em 0; + overflow-x: auto; + font-family: "Consolas", "Monaco", monospace; + font-size: 0.9em; + line-height: 1.5; + box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.5); +} + +code { + background-color: var(--code-bg); + color: var(--accent-blue); + padding: 3px 6px; + border-radius: 3px; + font-family: "Consolas", "Monaco", monospace; + font-size: 0.9em; + border: 1px solid var(--border-color); +} + +pre code { + background-color: transparent; + color: var(--text-primary); + padding: 0; + border: none; +} + +/* Blockquotes with FFXIV quest text styling */ +blockquote { + border-left: 4px solid var(--accent-gold); + background: linear-gradient(90deg, rgba(201, 170, 113, 0.1), transparent); + padding: 15px 20px; + margin: 1.5em 0; + font-style: italic; + color: var(--text-secondary); + position: relative; +} + +blockquote::before { + content: '"'; + font-size: 3em; + color: var(--accent-gold); + opacity: 0.3; + position: absolute; + top: -10px; + left: 10px; +} + +/* Tables with FFXIV UI styling */ +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin: 1.5em 0; + background-color: var(--secondary-bg); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +thead { + background: linear-gradient( + 180deg, + var(--tertiary-bg), + var(--secondary-bg) + ); +} + +th { + padding: 12px 15px; + text-align: left; + color: var(--accent-gold); + font-weight: 600; + border-bottom: 2px solid var(--accent-gold); + text-transform: uppercase; + font-size: 0.9em; + letter-spacing: 1px; +} + +td { + padding: 10px 15px; + color: var(--text-secondary); + border-bottom: 1px solid var(--border-color); +} + +tr:hover td { + background-color: rgba(201, 170, 113, 0.05); +} + +/* Horizontal rules with FFXIV ornamental style */ +hr { + border: none; + height: 2px; + background: linear-gradient( + 90deg, + transparent, + var(--accent-gold) 20%, + var(--accent-gold) 50%, + var(--accent-gold) 80%, + transparent + ); + margin: 2em 0; + position: relative; +} + +hr::before { + content: "◆"; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background-color: var(--primary-bg); + color: var(--accent-gold); + padding: 0 10px; + font-size: 1.2em; +} + +/* Images */ +img { + max-width: 100%; + height: auto; + display: block; + margin: 1.5em auto; + border: 2px solid var(--border-color); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5); +} + +/* Wiki-style additional elements */ +.wiki-infobox { + float: right; + width: 300px; + margin: 0 0 20px 20px; + background-color: var(--secondary-bg); + border: 1px solid var(--border-color); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.wiki-infobox-header { + background-color: var(--tertiary-bg); + color: var(--accent-gold); + padding: 10px 15px; + text-align: center; + font-weight: bold; + border-bottom: 1px solid var(--border-color); +} + +.wiki-infobox-content { + padding: 15px; +} + +.wiki-infobox-row { + display: flex; + margin-bottom: 10px; +} + +.wiki-infobox-label { + flex: 1; + font-weight: bold; + color: var(--accent-blue); +} + +.wiki-infobox-value { + flex: 2; + color: var(--text-secondary); +} + +/* Article meta information */ +.article-meta { + border-top: 1px solid var(--border-color); + margin-top: 40px; + padding-top: 20px; + font-size: 0.9em; + color: var(--text-dim); +} + +.article-categories { + margin-top: 10px; +} + +.article-category { + display: inline-block; + background-color: var(--tertiary-bg); + color: var(--accent-gold); + padding: 3px 10px; + margin-right: 5px; + margin-bottom: 5px; + border-radius: 3px; + font-size: 0.85em; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + background: var(--secondary-bg); + border: 1px solid var(--border-color); +} + +::-webkit-scrollbar-thumb { + background: var(--accent-gold); + border: 1px solid var(--border-color); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--accent-blue); +} + +/* Selection styling */ +::selection { + background-color: rgba(201, 170, 113, 0.3); + color: var(--text-primary); +} + +::-moz-selection { + background-color: rgba(201, 170, 113, 0.3); + color: var(--text-primary); +} + +/* Custom Figure styling */ +.custom-figure { + margin: 2.5rem auto; + padding: 1.5rem; + background-color: var(--secondary-bg); + border: 1px solid var(--border-color); + border-radius: 5px; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4); + position: relative; + overflow: hidden; + max-width: 95%; +} + +/* Image gallery layout for multiple images */ +.custom-figure .image-gallery { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + grid-gap: 1rem; + margin-bottom: 1.5rem; +} + +/* Single image in figure */ +.custom-figure img { + margin: 0 auto 1rem; + border: 1px solid var(--border-color); + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); + transition: + transform 0.3s ease, + box-shadow 0.3s ease; + max-height: 500px; + width: auto; +} + +/* Hover effect for images */ +.custom-figure img:hover { + transform: scale(1.02); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + border-color: var(--accent-gold); +} + +/* Image titles (from title attribute) */ +.custom-figure .image-title { + display: block; + text-align: center; + font-style: italic; + color: var(--accent-gold); + font-size: 0.9em; + margin-top: 0.5rem; + margin-bottom: 1rem; +} + +/* Caption styling */ +.custom-figure figcaption { + font-family: "Jupiter Pro", "Cinzel", "Georgia", serif; + text-align: center; + color: var(--text-primary); + font-size: 1.1em; + padding: 0.8rem 0; + border-top: 1px solid rgba(201, 170, 113, 0.3); + margin-top: 0.5rem; + position: relative; +} + +/* Special styling for captions to match FFXIV aesthetic */ +.custom-figure figcaption::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 4px; + background: linear-gradient(90deg, var(--accent-gold), var(--accent-blue)); +} + +/* Responsive design */ +@media (max-width: 1200px) { + .container { + max-width: 100%; + } +} + +@media (max-width: 992px) { + .nav-sidebar { + width: 200px; + } + + .main-content { + margin-left: 200px; + width: calc(100% - 200px); + } +} + +@media (max-width: 768px) { + body { + flex-direction: column; + } + + .nav-sidebar { + width: 100%; + height: auto; + position: relative; + border-right: none; + border-bottom: 1px solid var(--border-color); + } + + .main-content { + margin-left: 0; + width: 100%; + } + + .container { + padding: 20px 15px; + } + + .wiki-infobox { + float: none; + width: 100%; + margin: 1.5em 0; + } + + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.6em; + } + + h3 { + font-size: 1.3em; + } +} + +/* Animation for page load */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + + .custom-figure { + padding: 1rem; + margin: 2rem auto; + } + + .custom-figure .image-gallery { + grid-template-columns: 1fr; + } + + .custom-figure img { + max-height: 350px; + } +} + +.main-content > * { + animation: fadeIn 0.6s ease-out; +} -- cgit v1.2.3