diff --git a/.devcontainer/bashrc.override.sh b/.devcontainer/bashrc.override.sh deleted file mode 100644 index bedddf6..0000000 --- a/.devcontainer/bashrc.override.sh +++ /dev/null @@ -1,20 +0,0 @@ - -# -# .bashrc.override.sh -# - -# persistent bash history -HISTFILE=~/.bash_history -PROMPT_COMMAND="history -a; $PROMPT_COMMAND" - -# set some django env vars -source /entrypoint - -# restore default shell options -set +o errexit -set +o pipefail -set +o nounset - -# start ssh-agent -# https://code.visualstudio.com/docs/remote/troubleshooting -eval "$(ssh-agent -s)" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index e79d44d..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,68 +0,0 @@ -// For format details, see https://containers.dev/implementors/json_reference/ -{ - "name": "ether_dev", - "dockerComposeFile": [ - "../docker-compose.local.yml" - ], - "init": true, - "mounts": [ - { - "source": "./.devcontainer/bash_history", - "target": "/home/dev-user/.bash_history", - "type": "bind" - }, - { - "source": "~/.ssh", - "target": "/home/dev-user/.ssh", - "type": "bind" - } - ], - // Tells devcontainer.json supporting services / tools whether they should run - // /bin/sh -c "while sleep 1000; do :; done" when starting the container instead of the container’s default command - "overrideCommand": false, - "service": "django", - // "remoteEnv": {"PATH": "/home/dev-user/.local/bin:${containerEnv:PATH}"}, - "remoteUser": "dev-user", - "workspaceFolder": "/app", - // Set *default* container specific settings.json values on container create. - "customizations": { - "vscode": { - "settings": { - "editor.formatOnSave": true, - "[python]": { - "analysis.autoImportCompletions": true, - "analysis.typeCheckingMode": "basic", - "defaultInterpreterPath": "/usr/local/bin/python", - "editor.codeActionsOnSave": { - "source.organizeImports": "always" - }, - "editor.defaultFormatter": "charliermarsh.ruff", - "languageServer": "Pylance", - "linting.enabled": true, - "linting.mypyEnabled": true, - "linting.mypyPath": "/usr/local/bin/mypy", - } - }, - // https://code.visualstudio.com/docs/remote/devcontainerjson-reference#_vs-code-specific-properties - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "davidanson.vscode-markdownlint", - "mrmlnc.vscode-duplicate", - "visualstudioexptteam.vscodeintellicode", - "visualstudioexptteam.intellicode-api-usage-examples", - // python - "ms-python.python", - "ms-python.vscode-pylance", - "charliermarsh.ruff", - // django - "batisteo.vscode-django" - ] - } - }, - // Uncomment the next line if you want start specific services in your Docker Compose config. - // "runServices": [], - // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", - // Uncomment the next line to run commands after the container is created. - "postCreateCommand": "cat .devcontainer/bashrc.override.sh >> ~/.bashrc" -} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index a602416..0000000 --- a/.dockerignore +++ /dev/null @@ -1,12 +0,0 @@ -.editorconfig -.gitattributes -.github -.gitignore -.gitlab-ci.yml -.idea -.pre-commit-config.yaml -.readthedocs.yml -.travis.yml -venv -.git -.envs/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52855ed..adf6aca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,18 +1,11 @@ name: CI -# Enable Buildkit and let compose use it to speed up image building -env: - DOCKER_BUILDKIT: 1 - COMPOSE_DOCKER_CLI_BUILD: 1 - on: pull_request: branches: ['main'] - paths-ignore: ['docs/**'] push: branches: ['main'] - paths-ignore: ['docs/**'] concurrency: group: ${{ github.head_ref || github.run_id }} @@ -22,58 +15,48 @@ jobs: linter: runs-on: ubuntu-latest steps: - - name: Checkout Code Repository + - name: Checkout uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version-file: '.python-version' + - name: Run pre-commit uses: pre-commit/action@v3.0.1 - # With no caching at all the entire ci process takes 3m to complete! - pytest: + django: runs-on: ubuntu-latest steps: - - name: Checkout Code Repository + - name: Checkout uses: actions/checkout@v6 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Build and cache local backend - uses: docker/bake-action@v7 + - name: Install uv + uses: astral-sh/setup-uv@v6 with: - push: false - load: true - files: docker-compose.local.yml - targets: django - set: | - django.cache-from=type=gha,scope=django-cached-tests - django.cache-to=type=gha,scope=django-cached-tests,mode=max - postgres.cache-from=type=gha,scope=postgres-cached-tests - postgres.cache-to=type=gha,scope=postgres-cached-tests,mode=max + enable-cache: true - - name: Build and cache docs - uses: docker/bake-action@v7 + - name: Set up Python + uses: actions/setup-python@v6 with: - push: false - load: true - files: docker-compose.docs.yml - set: | - docs.cache-from=type=gha,scope=cached-docs - docs.cache-to=type=gha,scope=cached-docs,mode=max + python-version-file: '.python-version' - - name: Check DB Migrations - run: docker compose -f docker-compose.local.yml run --rm django python manage.py makemigrations --check + - name: Install dependencies + run: uv sync --frozen - - name: Run DB Migrations - run: docker compose -f docker-compose.local.yml run --rm django python manage.py migrate + - name: Django system check + env: + DJANGO_SECRET_KEY: ci-secret-key + run: uv run python manage.py check - - name: Run Django Tests - run: docker compose -f docker-compose.local.yml run django pytest + - name: Run migrations + env: + DJANGO_SECRET_KEY: ci-secret-key + run: uv run python manage.py migrate --run-syncdb - - name: Tear down the Stack - run: docker compose -f docker-compose.local.yml down + - name: Collectstatic + env: + DJANGO_SECRET_KEY: ci-secret-key + run: uv run python manage.py collectstatic --noinput diff --git a/.gitignore b/.gitignore index 2602aa1..5adbab6 100644 --- a/.gitignore +++ b/.gitignore @@ -278,4 +278,12 @@ ether/media/ .envs/* !.envs/.local/ +# Django +db.sqlite3 +db.sqlite3-journal +staticfiles/ + +# Vercel +.vercel + /.claude/worktrees diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index c237a0b..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -version: 2 - -build: - os: ubuntu-24.04 - tools: - python: "3.13" - jobs: - pre_create_environment: - - asdf plugin add uv - - asdf install uv latest - - asdf global uv latest - create_environment: - - uv venv "${READTHEDOCS_VIRTUALENV_PATH}" - install: - - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --frozen --no-dev --only-group docs - -sphinx: - configuration: docs/conf.py diff --git a/README.md b/README.md index 44e7f8c..f6da51d 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,66 @@ -# Ether Organization Website +# Ether合同会社 / Ether LLC — Website -Organization website for Ether LLC +[https://ether-llc.com](https://ether-llc.com) で公開している、Ether合同会社の組織サイトです。 -[![Built with Cookiecutter Django](https://img.shields.io/badge/built%20with-Cookiecutter%20Django-ff69b4.svg?logo=cookiecutter)](https://github.com/cookiecutter/cookiecutter-django/) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +## スタック -## Settings +- Python 3.12+ / Django 6 +- uv (依存管理) +- Vercel Python Runtime (`manage.py` 自動検出によるデプロイ) +- Bootstrap 5 + 自前 CSS (Material 3 風) -Moved to [settings](https://cookiecutter-django.readthedocs.io/en/latest/1-getting-started/settings.html). +## ローカル開発 -## Basic Commands +```bash +uv sync +uv run python manage.py migrate +uv run python manage.py runserver +``` -### Setting Up Your Users +`http://localhost:8000/` にアクセス。 -- To create a **normal user account**, just go to Sign Up and fill out the form. Once you submit it, you'll see a "Verify Your E-mail Address" page. Go to your console to see a simulated email verification message. Copy the link into your browser. Now the user's email should be verified and ready to go. +## デプロイ (Vercel) -- To create a **superuser account**, use this command: +`vercel.json` は不要です。Vercel が `manage.py` を自動検出し、WSGI と静的ファイルを構成します。 - uv run python manage.py createsuperuser +初回のみ、Django 用の Secret Key を環境変数として登録します: -For convenience, you can keep your normal user logged in on Chrome and your superuser logged in on Firefox (or similar), so that you can see how the site behaves for both kinds of users. +```bash +uv run python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())' | vercel env add -y DJANGO_SECRET_KEY prod +``` -### Type checks +以降は `git push` で `main` ブランチがマージされるたびに自動デプロイされます。 -Running type checks with mypy: +## ディレクトリ構成 - uv run mypy ether +``` +config/ — Django プロジェクト (settings.py, urls.py, wsgi.py) +ether/ + templates/ — JA テンプレート + en/ サブディレクトリ + static/ — CSS/JS/画像 +manage.py +pyproject.toml — 依存定義 (django のみ、dev に djlint と ruff) +``` -### Test coverage +## URL 構成 -To run the tests, check your test coverage, and generate an HTML coverage report: +| パス | 内容 | +|---|---| +| `/`, `/en/` | ホーム | +| `/vision/`, `/en/vision/` | 会社のビジョン | +| `/holdings/`, `/en/holdings/` | Ethereum 保有方針 | +| `/software/`, `/en/software/` | プロダクト紹介 (GratefulMoments) | +| `/business/`, `/en/business/` | 定款の事業目的 23 項目 | +| `/company/`, `/en/company/` | 法定の会社情報 | +| `/contact/`, `/en/contact/` | お問い合わせ窓口 | +| `/legal/privacy/`, `/legal/cookies/` | プライバシー/Cookieポリシー | +| `/robots.txt`, `/sitemap.xml` | クローラ向け | +| `/admin/` | Django 管理画面 | - uv run coverage run -m pytest - uv run coverage html - uv run open htmlcov/index.html +## Lint / Format -#### Running tests with pytest - - uv run pytest - -### Live reloading and Sass CSS compilation - -Moved to [Live reloading and SASS compilation](https://cookiecutter-django.readthedocs.io/en/latest/2-local-development/developing-locally.html#using-webpack-or-gulp). - -### Email Server - -In development, it is often nice to be able to see emails that are being sent from your application. For that reason local SMTP server [Mailpit](https://github.com/axllent/mailpit) with a web interface is available as docker container. - -Container mailpit will start automatically when you will run all docker containers. -Please check [cookiecutter-django Docker documentation](https://cookiecutter-django.readthedocs.io/en/latest/2-local-development/developing-locally-docker.html) for more details how to start all containers. - -With Mailpit running, to view messages that are sent by your application, open your browser and go to `http://127.0.0.1:8025` - -## Deployment - -The following details how to deploy this application. - -### Docker - -See detailed [cookiecutter-django Docker documentation](https://cookiecutter-django.readthedocs.io/en/latest/3-deployment/deployment-with-docker.html). +```bash +uv run ruff check . # Python +uv run djlint ether/templates/ # Django テンプレート +uv run djlint ether/templates/ --reformat +``` diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile deleted file mode 100644 index b714f62..0000000 --- a/compose/local/django/Dockerfile +++ /dev/null @@ -1,59 +0,0 @@ -# define an alias for the specific python version used in this file. -FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS python - -# Python build stage -FROM python AS python-build-stage - -ARG APP_HOME=/app - -WORKDIR ${APP_HOME} - -# we need to move the virtualenv outside of the $APP_HOME directory because it will be overriden by the docker compose mount -ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=0 - -# Install apt packages -RUN apt-get update && apt-get install --no-install-recommends -y \ - # dependencies for building Python packages - build-essential \ - # psycopg dependencies - libpq-dev \ - gettext \ - wait-for-it - -# Requirements are installed here to ensure they will be cached. -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - --mount=type=bind,source=uv.lock,target=uv.lock:rw \ - uv sync --no-install-project - -COPY . ${APP_HOME} - -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - --mount=type=bind,source=uv.lock,target=uv.lock:rw \ - uv sync - -# devcontainer dependencies and utils -RUN apt-get update && apt-get install --no-install-recommends -y \ - sudo git bash-completion nano ssh - -# Create devcontainer user and add it to sudoers -RUN groupadd --gid 1000 dev-user \ - && useradd --uid 1000 --gid dev-user --shell /bin/bash --create-home dev-user \ - && echo dev-user ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/dev-user \ - && chmod 0440 /etc/sudoers.d/dev-user - -ENV PATH="/${APP_HOME}/.venv/bin:$PATH" -ENV PYTHONPATH="${APP_HOME}/.venv/lib/python3.13/site-packages:$PYTHONPATH" - -COPY ./compose/production/django/entrypoint /entrypoint -RUN sed -i 's/\r$//g' /entrypoint -RUN chmod +x /entrypoint - -COPY ./compose/local/django/start /start -RUN sed -i 's/\r$//g' /start -RUN chmod +x /start - - - -ENTRYPOINT ["/entrypoint"] diff --git a/compose/local/django/start b/compose/local/django/start deleted file mode 100644 index ba96db4..0000000 --- a/compose/local/django/start +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o pipefail -set -o nounset - - -python manage.py migrate -exec python manage.py runserver_plus 0.0.0.0:8000 diff --git a/compose/local/docs/Dockerfile b/compose/local/docs/Dockerfile deleted file mode 100644 index 82dbeb1..0000000 --- a/compose/local/docs/Dockerfile +++ /dev/null @@ -1,65 +0,0 @@ -# define an alias for the specific python version used in this file. -FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS python - - -# Python build stage -FROM python AS python-build-stage - -ARG APP_HOME=/app - -WORKDIR ${APP_HOME} - -RUN apt-get update && apt-get install --no-install-recommends -y \ - # dependencies for building Python packages - build-essential \ - # psycopg dependencies - libpq-dev \ - # cleaning up unused files - && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ - && rm -rf /var/lib/apt/lists/* - -# Requirements are installed here to ensure they will be cached. -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --no-install-project - -COPY . ${APP_HOME} - -RUN --mount=type=cache,target=/root/.cache/uv \ - uv sync - - -# Python 'run' stage -FROM python AS python-run-stage - -ARG BUILD_ENVIRONMENT -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 - -RUN apt-get update && apt-get install --no-install-recommends -y \ - # To run the Makefile - make \ - # psycopg dependencies - libpq-dev \ - # Translations dependencies - gettext \ - # Uncomment below lines to enable Sphinx output to latex and pdf - # texlive-latex-recommended \ - # texlive-fonts-recommended \ - # texlive-latex-extra \ - # latexmk \ - # cleaning up unused files - && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ - && rm -rf /var/lib/apt/lists/* - -# copy python dependency wheels from python-build-stage -COPY --from=python-build-stage --chown=app:app /app /app - -COPY ./compose/local/docs/start /start-docs -RUN sed -i 's/\r$//g' /start-docs -RUN chmod +x /start-docs - -ENV PATH="/app/.venv/bin:$PATH" - -WORKDIR /docs diff --git a/compose/local/docs/start b/compose/local/docs/start deleted file mode 100644 index 96a94f5..0000000 --- a/compose/local/docs/start +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o pipefail -set -o nounset - -exec make livehtml diff --git a/compose/production/django/Dockerfile b/compose/production/django/Dockerfile deleted file mode 100644 index 7768727..0000000 --- a/compose/production/django/Dockerfile +++ /dev/null @@ -1,81 +0,0 @@ - -# define an alias for the specific python version used in this file. -FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS python-build-stage - -ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=0 - -ARG APP_HOME=/app - -WORKDIR ${APP_HOME} - -# Install apt packages -RUN apt-get update && apt-get install --no-install-recommends -y \ - # dependencies for building Python packages - build-essential \ - # psycopg dependencies - libpq-dev - - -# Requirements are installed here to ensure they will be cached. -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --locked --no-install-project --no-dev -COPY . ${APP_HOME} - -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - uv sync --locked --no-dev - -# Python 'run' stage -FROM python:3.13.13-slim-bookworm AS python-run-stage - -ARG APP_HOME=/app - -WORKDIR ${APP_HOME} - -RUN addgroup --system django \ - && adduser --system --ingroup django django - - -# Install required system dependencies -RUN apt-get update && apt-get install --no-install-recommends -y \ - # psycopg dependencies - libpq-dev \ - # Translations dependencies - gettext \ - # entrypoint - wait-for-it \ - # cleaning up unused files - && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ - && rm -rf /var/lib/apt/lists/* - - -COPY --chown=django:django ./compose/production/django/entrypoint /entrypoint -RUN sed -i 's/\r$//g' /entrypoint -RUN chmod +x /entrypoint - - -COPY --chown=django:django ./compose/production/django/start /start -RUN sed -i 's/\r$//g' /start -RUN chmod +x /start - -# Copy the application from the builder -COPY --from=python-build-stage --chown=django:django ${APP_HOME} ${APP_HOME} -# explicitly create the media folder before changing ownership below -RUN mkdir -p ${APP_HOME}/ether/media - -# make django owner of the WORKDIR directory as well. -RUN chown django:django ${APP_HOME} - -# Place executables in the environment at the front of the path -ENV PATH="/app/.venv/bin:$PATH" - -USER django - -RUN DATABASE_URL="" \ - DJANGO_SETTINGS_MODULE="config.settings.test" \ - python manage.py compilemessages - -ENTRYPOINT ["/entrypoint"] diff --git a/compose/production/django/entrypoint b/compose/production/django/entrypoint deleted file mode 100644 index fe9a013..0000000 --- a/compose/production/django/entrypoint +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o pipefail -set -o nounset - -if [ -z "${POSTGRES_USER}" ]; then - base_postgres_image_default_user='postgres' - export POSTGRES_USER="${base_postgres_image_default_user}" -fi -export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" - -wait-for-it "${POSTGRES_HOST}:${POSTGRES_PORT}" -t 30 - ->&2 echo 'PostgreSQL is available' - -exec "$@" diff --git a/compose/production/django/start b/compose/production/django/start deleted file mode 100644 index 53bf12a..0000000 --- a/compose/production/django/start +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o pipefail -set -o nounset - - -python /app/manage.py collectstatic --noinput - -exec gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app diff --git a/compose/production/nginx/Dockerfile b/compose/production/nginx/Dockerfile deleted file mode 100644 index 412c6a7..0000000 --- a/compose/production/nginx/Dockerfile +++ /dev/null @@ -1,2 +0,0 @@ -FROM docker.io/nginx:1.29.8-alpine -COPY ./compose/production/nginx/default.conf /etc/nginx/conf.d/default.conf diff --git a/compose/production/nginx/default.conf b/compose/production/nginx/default.conf deleted file mode 100644 index 562dba8..0000000 --- a/compose/production/nginx/default.conf +++ /dev/null @@ -1,7 +0,0 @@ -server { - listen 80; - server_name localhost; - location /media/ { - alias /usr/share/nginx/media/; - } -} diff --git a/compose/production/postgres/Dockerfile b/compose/production/postgres/Dockerfile deleted file mode 100644 index a6ac0eb..0000000 --- a/compose/production/postgres/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM docker.io/postgres:18 - -COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance -RUN chmod +x /usr/local/bin/maintenance/* -RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ - && rmdir /usr/local/bin/maintenance diff --git a/compose/production/postgres/maintenance/_sourced/constants.sh b/compose/production/postgres/maintenance/_sourced/constants.sh deleted file mode 100644 index 6ca4f0c..0000000 --- a/compose/production/postgres/maintenance/_sourced/constants.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - - -BACKUP_DIR_PATH='/backups' -BACKUP_FILE_PREFIX='backup' diff --git a/compose/production/postgres/maintenance/_sourced/countdown.sh b/compose/production/postgres/maintenance/_sourced/countdown.sh deleted file mode 100644 index e6cbfb6..0000000 --- a/compose/production/postgres/maintenance/_sourced/countdown.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - - -countdown() { - declare desc="A simple countdown. Source: https://superuser.com/a/611582" - local seconds="${1}" - local d=$(($(date +%s) + "${seconds}")) - while [ "$d" -ge `date +%s` ]; do - echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; - sleep 0.1 - done -} diff --git a/compose/production/postgres/maintenance/_sourced/messages.sh b/compose/production/postgres/maintenance/_sourced/messages.sh deleted file mode 100644 index f6be756..0000000 --- a/compose/production/postgres/maintenance/_sourced/messages.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - - -message_newline() { - echo -} - -message_debug() -{ - echo -e "DEBUG: ${@}" -} - -message_welcome() -{ - echo -e "\e[1m${@}\e[0m" -} - -message_warning() -{ - echo -e "\e[33mWARNING\e[0m: ${@}" -} - -message_error() -{ - echo -e "\e[31mERROR\e[0m: ${@}" -} - -message_info() -{ - echo -e "\e[37mINFO\e[0m: ${@}" -} - -message_suggestion() -{ - echo -e "\e[33mSUGGESTION\e[0m: ${@}" -} - -message_success() -{ - echo -e "\e[32mSUCCESS\e[0m: ${@}" -} diff --git a/compose/production/postgres/maintenance/_sourced/yes_no.sh b/compose/production/postgres/maintenance/_sourced/yes_no.sh deleted file mode 100644 index fd9cae1..0000000 --- a/compose/production/postgres/maintenance/_sourced/yes_no.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - - -yes_no() { - declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." - local arg1="${1}" - - local response= - read -r -p "${arg1} (y/[n])? " response - if [[ "${response}" =~ ^[Yy]$ ]] - then - exit 0 - else - exit 1 - fi -} diff --git a/compose/production/postgres/maintenance/backup b/compose/production/postgres/maintenance/backup deleted file mode 100644 index f72304c..0000000 --- a/compose/production/postgres/maintenance/backup +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash - - -### Create a database backup. -### -### Usage: -### $ docker compose -f .yml (exec |run --rm) postgres backup - - -set -o errexit -set -o pipefail -set -o nounset - - -working_dir="$(dirname ${0})" -source "${working_dir}/_sourced/constants.sh" -source "${working_dir}/_sourced/messages.sh" - - -message_welcome "Backing up the '${POSTGRES_DB}' database..." - - -if [[ "${POSTGRES_USER}" == "postgres" ]]; then - message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." - exit 1 -fi - -export PGHOST="${POSTGRES_HOST}" -export PGPORT="${POSTGRES_PORT}" -export PGUSER="${POSTGRES_USER}" -export PGPASSWORD="${POSTGRES_PASSWORD}" -export PGDATABASE="${POSTGRES_DB}" - -backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" -pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" - - -message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." diff --git a/compose/production/postgres/maintenance/backups b/compose/production/postgres/maintenance/backups deleted file mode 100644 index a18937d..0000000 --- a/compose/production/postgres/maintenance/backups +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash - - -### View backups. -### -### Usage: -### $ docker compose -f .yml (exec |run --rm) postgres backups - - -set -o errexit -set -o pipefail -set -o nounset - - -working_dir="$(dirname ${0})" -source "${working_dir}/_sourced/constants.sh" -source "${working_dir}/_sourced/messages.sh" - - -message_welcome "These are the backups you have got:" - -ls -lht "${BACKUP_DIR_PATH}" diff --git a/compose/production/postgres/maintenance/restore b/compose/production/postgres/maintenance/restore deleted file mode 100644 index c68f17d..0000000 --- a/compose/production/postgres/maintenance/restore +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - - -### Restore database from a backup. -### -### Parameters: -### <1> filename of an existing backup. -### -### Usage: -### $ docker compose -f .yml (exec |run --rm) postgres restore <1> - - -set -o errexit -set -o pipefail -set -o nounset - - -working_dir="$(dirname ${0})" -source "${working_dir}/_sourced/constants.sh" -source "${working_dir}/_sourced/messages.sh" - - -if [[ -z ${1+x} ]]; then - message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." - exit 1 -fi -backup_filename="${BACKUP_DIR_PATH}/${1}" -if [[ ! -f "${backup_filename}" ]]; then - message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." - exit 1 -fi - -message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." - -if [[ "${POSTGRES_USER}" == "postgres" ]]; then - message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." - exit 1 -fi - -export PGHOST="${POSTGRES_HOST}" -export PGPORT="${POSTGRES_PORT}" -export PGUSER="${POSTGRES_USER}" -export PGPASSWORD="${POSTGRES_PASSWORD}" -export PGDATABASE="${POSTGRES_DB}" - -message_info "Dropping the database..." -dropdb "${PGDATABASE}" - -message_info "Creating a new database..." -createdb --owner="${POSTGRES_USER}" - -message_info "Applying the backup to the new database..." -gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" - -message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." diff --git a/compose/production/postgres/maintenance/rmbackup b/compose/production/postgres/maintenance/rmbackup deleted file mode 100644 index fdfd20e..0000000 --- a/compose/production/postgres/maintenance/rmbackup +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -### Remove a database backup. -### -### Parameters: -### <1> filename of a backup to remove. -### -### Usage: -### $ docker-compose -f .yml (exec |run --rm) postgres rmbackup <1> - - -set -o errexit -set -o pipefail -set -o nounset - - -working_dir="$(dirname ${0})" -source "${working_dir}/_sourced/constants.sh" -source "${working_dir}/_sourced/messages.sh" - - -if [[ -z ${1+x} ]]; then - message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." - exit 1 -fi -backup_filename="${BACKUP_DIR_PATH}/${1}" -if [[ ! -f "${backup_filename}" ]]; then - message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." - exit 1 -fi - -message_welcome "Removing the '${backup_filename}' backup file..." - -rm -r "${backup_filename}" - -message_success "The '${backup_filename}' database backup has been removed." diff --git a/compose/production/traefik/Dockerfile b/compose/production/traefik/Dockerfile deleted file mode 100644 index b774dca..0000000 --- a/compose/production/traefik/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM docker.io/traefik:v3.6.15 -RUN mkdir -p /etc/traefik/acme \ - && touch /etc/traefik/acme/acme.json \ - && chmod 600 /etc/traefik/acme/acme.json -COPY ./compose/production/traefik/traefik.yml /etc/traefik diff --git a/compose/production/traefik/traefik.yml b/compose/production/traefik/traefik.yml deleted file mode 100644 index 5136741..0000000 --- a/compose/production/traefik/traefik.yml +++ /dev/null @@ -1,73 +0,0 @@ -log: - level: INFO - -entryPoints: - web: - # http - address: ':80' - http: - # https://doc.traefik.io/traefik/routing/entrypoints/#entrypoint - redirections: - entryPoint: - to: web-secure - - web-secure: - # https - address: ':443' - -certificatesResolvers: - letsencrypt: - # https://doc.traefik.io/traefik/https/acme/#lets-encrypt - acme: - email: 'info@ether.local' - storage: /etc/traefik/acme/acme.json - # https://doc.traefik.io/traefik/https/acme/#httpchallenge - httpChallenge: - entryPoint: web - -http: - routers: - web-secure-router: - rule: 'Host(`ether.local`) || Host(`www.ether.local`)' - entryPoints: - - web-secure - middlewares: - - csrf - service: django - tls: - # https://doc.traefik.io/traefik/routing/routers/#certresolver - certResolver: letsencrypt - - web-media-router: - rule: '(Host(`ether.local`) || Host(`www.ether.local`)) && PathPrefix(`/media/`)' - entryPoints: - - web-secure - middlewares: - - csrf - service: django-media - tls: - certResolver: letsencrypt - - middlewares: - csrf: - # https://doc.traefik.io/traefik/master/middlewares/http/headers/#hostsproxyheaders - # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax - headers: - hostsProxyHeaders: ['X-CSRFToken'] - - services: - django: - loadBalancer: - servers: - - url: http://django:5000 - - django-media: - loadBalancer: - servers: - - url: http://nginx:80 - -providers: - # https://doc.traefik.io/traefik/master/providers/file/ - file: - filename: /etc/traefik/traefik.yml - watch: true diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..28f1d9d --- /dev/null +++ b/config/settings.py @@ -0,0 +1,89 @@ +""" +Django settings for the Ether合同会社 website. + +Designed to deploy on Vercel via the Python Runtime, which detects manage.py +to find the WSGI entrypoint and the configuration for static files. +""" + +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +SECRET_KEY = os.environ.get( + "DJANGO_SECRET_KEY", + "django-insecure-change-me-in-production", +) + +DEBUG = os.environ.get("DJANGO_DEBUG") == "1" + +ALLOWED_HOSTS = [ + "127.0.0.1", + "localhost", + ".vercel.app", + "ether-llc.com", + "www.ether-llc.com", +] + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "ether", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "ether" / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + }, +} + +_PW_PREFIX = "django.contrib.auth.password_validation" +AUTH_PASSWORD_VALIDATORS = [ + {"NAME": f"{_PW_PREFIX}.UserAttributeSimilarityValidator"}, + {"NAME": f"{_PW_PREFIX}.MinimumLengthValidator"}, + {"NAME": f"{_PW_PREFIX}.CommonPasswordValidator"}, + {"NAME": f"{_PW_PREFIX}.NumericPasswordValidator"}, +] + +LANGUAGE_CODE = "ja" +TIME_ZONE = "Asia/Tokyo" +USE_I18N = True +USE_TZ = True + +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR / "staticfiles" +STATICFILES_DIRS = [BASE_DIR / "ether" / "static"] diff --git a/config/settings/__init__.py b/config/settings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/config/settings/base.py b/config/settings/base.py deleted file mode 100644 index 2bfe444..0000000 --- a/config/settings/base.py +++ /dev/null @@ -1,283 +0,0 @@ -# ruff: noqa: ERA001, E501 -"""Base settings to build other settings files upon.""" - -from pathlib import Path - -import environ - -BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent -# ether/ -APPS_DIR = BASE_DIR / "ether" -env = environ.Env() - -READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) -if READ_DOT_ENV_FILE: - # OS environment variables take precedence over variables from .env - env.read_env(str(BASE_DIR / ".env")) - -# GENERAL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = env.bool("DJANGO_DEBUG", False) -# Local time zone. Choices are -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# though not all of them may be available with every OS. -# In Windows, this must be set to your system time zone. -TIME_ZONE = "Asia/Tokyo" -# https://docs.djangoproject.com/en/dev/ref/settings/#language-code -LANGUAGE_CODE = "ja" -# https://docs.djangoproject.com/en/dev/ref/settings/#languages -# from django.utils.translation import gettext_lazy as _ -# LANGUAGES = [ -# ('en', _('English')), -# ('fr-fr', _('French')), -# ('pt-br', _('Portuguese')), -# ] -# https://docs.djangoproject.com/en/dev/ref/settings/#site-id -SITE_ID = 1 -# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n -USE_I18N = True -# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz -USE_TZ = True -# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths -LOCALE_PATHS = [str(BASE_DIR / "locale")] - -# DATABASES -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#databases -DATABASES = {"default": env.db("DATABASE_URL")} -DATABASES["default"]["ATOMIC_REQUESTS"] = True - -# URLS -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf -ROOT_URLCONF = "config.urls" -# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application -WSGI_APPLICATION = "config.wsgi.application" - -# APPS -# ------------------------------------------------------------------------------ -DJANGO_APPS = [ - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.sites", - "django.contrib.messages", - "django.contrib.staticfiles", - # "django.contrib.humanize", # Handy template tags - "django.contrib.admin", - "django.forms", -] -THIRD_PARTY_APPS = [ - "crispy_forms", - "crispy_bootstrap5", - "allauth", - "allauth.account", - "allauth.mfa", - "allauth.socialaccount", -] - -LOCAL_APPS = [ - "ether.users", - # Your stuff: custom apps go here -] -# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps -INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS - -# MIGRATIONS -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules -MIGRATION_MODULES = {"sites": "ether.contrib.sites.migrations"} - -# AUTHENTICATION -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - "allauth.account.auth_backends.AuthenticationBackend", -] -# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model -AUTH_USER_MODEL = "users.User" -# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url -LOGIN_REDIRECT_URL = "users:redirect" -# https://docs.djangoproject.com/en/dev/ref/settings/#login-url -LOGIN_URL = "account_login" - -# PASSWORDS -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers -PASSWORD_HASHERS = [ - # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django - "django.contrib.auth.hashers.Argon2PasswordHasher", - "django.contrib.auth.hashers.PBKDF2PasswordHasher", - "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", - "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", -] -# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, - {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, -] - -# MIDDLEWARE -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#middleware -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.locale.LocaleMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "allauth.account.middleware.AccountMiddleware", -] - -# STATIC -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#static-root -STATIC_ROOT = str(BASE_DIR / "staticfiles") -# https://docs.djangoproject.com/en/dev/ref/settings/#static-url -STATIC_URL = "/static/" -# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS -STATICFILES_DIRS = [str(APPS_DIR / "static")] -# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders -STATICFILES_FINDERS = [ - "django.contrib.staticfiles.finders.FileSystemFinder", - "django.contrib.staticfiles.finders.AppDirectoriesFinder", -] - -# MEDIA -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#media-root -MEDIA_ROOT = str(APPS_DIR / "media") -# https://docs.djangoproject.com/en/dev/ref/settings/#media-url -MEDIA_URL = "/media/" - -# TEMPLATES -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#templates -TEMPLATES = [ - { - # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND - "BACKEND": "django.template.backends.django.DjangoTemplates", - # https://docs.djangoproject.com/en/dev/ref/settings/#dirs - "DIRS": [str(APPS_DIR / "templates")], - # https://docs.djangoproject.com/en/dev/ref/settings/#app-dirs - "APP_DIRS": True, - "OPTIONS": { - # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.template.context_processors.i18n", - "django.template.context_processors.media", - "django.template.context_processors.static", - "django.template.context_processors.tz", - "django.contrib.messages.context_processors.messages", - "ether.users.context_processors.allauth_settings", - ], - }, - }, -] - -# https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer -FORM_RENDERER = "django.forms.renderers.TemplatesSetting" - -# http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs -CRISPY_TEMPLATE_PACK = "bootstrap5" -CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" - -# FIXTURES -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs -FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),) - -# SECURITY -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly -SESSION_COOKIE_HTTPONLY = True -# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly -CSRF_COOKIE_HTTPONLY = True -# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options -X_FRAME_OPTIONS = "DENY" - -# EMAIL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = env( - "DJANGO_EMAIL_BACKEND", - default="django.core.mail.backends.smtp.EmailBackend", -) -# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout -EMAIL_TIMEOUT = 5 - -# ADMIN -# ------------------------------------------------------------------------------ -# Django Admin URL. -ADMIN_URL = "admin/" -# https://docs.djangoproject.com/en/dev/ref/settings/#admins -ADMINS = ['"Ether LLC" '] -# https://docs.djangoproject.com/en/dev/ref/settings/#managers -MANAGERS = ADMINS -# https://cookiecutter-django.readthedocs.io/en/latest/settings.html#other-environment-settings -# Force the `admin` sign in process to go through the `django-allauth` workflow -DJANGO_ADMIN_FORCE_ALLAUTH = env.bool("DJANGO_ADMIN_FORCE_ALLAUTH", default=False) - -# LOGGING -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#logging -# See https://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", - }, - }, - "handlers": { - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - }, - "root": {"level": "INFO", "handlers": ["console"]}, -} - -REDIS_URL = env("REDIS_URL", default="redis://redis:6379/0") -REDIS_SSL = REDIS_URL.startswith("rediss://") - - -# django-allauth -# ------------------------------------------------------------------------------ -ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) -# https://docs.allauth.org/en/latest/account/configuration.html -ACCOUNT_LOGIN_METHODS = {"email"} -# https://docs.allauth.org/en/latest/account/configuration.html -ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"] -# https://docs.allauth.org/en/latest/account/configuration.html -ACCOUNT_USER_MODEL_USERNAME_FIELD = None -# https://docs.allauth.org/en/latest/account/configuration.html -ACCOUNT_EMAIL_VERIFICATION = "mandatory" -# https://docs.allauth.org/en/latest/account/configuration.html -ACCOUNT_ADAPTER = "ether.users.adapters.AccountAdapter" -# https://docs.allauth.org/en/latest/account/forms.html -ACCOUNT_FORMS = {"signup": "ether.users.forms.UserSignupForm"} -# https://docs.allauth.org/en/latest/socialaccount/configuration.html -SOCIALACCOUNT_ADAPTER = "ether.users.adapters.SocialAccountAdapter" -# https://docs.allauth.org/en/latest/socialaccount/configuration.html -SOCIALACCOUNT_FORMS = {"signup": "ether.users.forms.UserSocialSignupForm"} - - -# Your stuff... -# ------------------------------------------------------------------------------ diff --git a/config/settings/local.py b/config/settings/local.py deleted file mode 100644 index ad6b796..0000000 --- a/config/settings/local.py +++ /dev/null @@ -1,71 +0,0 @@ -from .base import * # noqa: F403 -from .base import INSTALLED_APPS -from .base import MIDDLEWARE -from .base import env - -# GENERAL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = True -# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key -SECRET_KEY = env( - "DJANGO_SECRET_KEY", - default="k3B7WAkzq0MhHG3ATI4Jm6cj1HM0q4zSHGh3mZ76PXBtMq1iXIjPlyMymA02bR8t", -) -# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] # noqa: S104 - -# CACHES -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#caches -CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", - "LOCATION": "", - }, -} - -# EMAIL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#email-host -EMAIL_HOST = env("EMAIL_HOST", default="mailpit") -# https://docs.djangoproject.com/en/dev/ref/settings/#email-port -EMAIL_PORT = 1025 - -# WhiteNoise -# ------------------------------------------------------------------------------ -# http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development -INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS] - - -# django-debug-toolbar -# ------------------------------------------------------------------------------ -# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites -INSTALLED_APPS += ["debug_toolbar"] -# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware -MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] -# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config -DEBUG_TOOLBAR_CONFIG = { - "DISABLE_PANELS": [ - "debug_toolbar.panels.redirects.RedirectsPanel", - # Disable profiling panel due to an issue with Python 3.12+: - # https://github.com/jazzband/django-debug-toolbar/issues/1875 - "debug_toolbar.panels.profiling.ProfilingPanel", - ], - "SHOW_TEMPLATE_CONTEXT": True, -} -# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips -INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] -if env("USE_DOCKER") == "yes": - import socket - - hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) - INTERNAL_IPS += [".".join([*ip.split(".")[:-1], "1"]) for ip in ips] - -# django-extensions -# ------------------------------------------------------------------------------ -# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration -INSTALLED_APPS += ["django_extensions"] - -# Your stuff... -# ------------------------------------------------------------------------------ diff --git a/config/settings/production.py b/config/settings/production.py deleted file mode 100644 index b3a9852..0000000 --- a/config/settings/production.py +++ /dev/null @@ -1,154 +0,0 @@ -# ruff: noqa: E501 -from .base import * # noqa: F403 -from .base import DATABASES -from .base import INSTALLED_APPS -from .base import REDIS_URL -from .base import env - -# GENERAL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key -SECRET_KEY = env("DJANGO_SECRET_KEY") -# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["ether.local"]) - -# DATABASES -# ------------------------------------------------------------------------------ -DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) - -# CACHES -# ------------------------------------------------------------------------------ -CACHES = { - "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": REDIS_URL, - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - # Mimicking memcache behavior. - # https://github.com/jazzband/django-redis#memcached-exceptions-behavior - "IGNORE_EXCEPTIONS": True, - }, - }, -} - -# SECURITY -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header -SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect -SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) -# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure -SESSION_COOKIE_SECURE = True -# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-name -SESSION_COOKIE_NAME = "__Secure-sessionid" -# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure -CSRF_COOKIE_SECURE = True -# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-name -CSRF_COOKIE_NAME = "__Secure-csrftoken" -# https://docs.djangoproject.com/en/dev/topics/security/#ssl-https -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds -# TODO: set this to 60 seconds first and then to 518400 once you prove the former works -SECURE_HSTS_SECONDS = 60 -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains -SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( - "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", - default=True, -) -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload -SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) -# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff -SECURE_CONTENT_TYPE_NOSNIFF = env.bool( - "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", - default=True, -) - -# STATIC & MEDIA -# ------------------------ -STORAGES = { - "default": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - }, - "staticfiles": { - "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", - }, -} - -# EMAIL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email -DEFAULT_FROM_EMAIL = env( - "DJANGO_DEFAULT_FROM_EMAIL", - default="Ether Organization Website ", -) -# https://docs.djangoproject.com/en/dev/ref/settings/#server-email -SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) -# https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix -EMAIL_SUBJECT_PREFIX = env( - "DJANGO_EMAIL_SUBJECT_PREFIX", - default="[Ether Organization Website] ", -) -ACCOUNT_EMAIL_SUBJECT_PREFIX = EMAIL_SUBJECT_PREFIX - -# ADMIN -# ------------------------------------------------------------------------------ -# Django Admin URL regex. -ADMIN_URL = env("DJANGO_ADMIN_URL") - -# Anymail -# ------------------------------------------------------------------------------ -# https://anymail.readthedocs.io/en/stable/installation/#installing-anymail -INSTALLED_APPS += ["anymail"] -# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference -# https://anymail.readthedocs.io/en/stable/esps -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -ANYMAIL = {} - - -# LOGGING -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#logging -# See https://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error when DEBUG=False. -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, - "formatters": { - "verbose": { - "format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s", - }, - }, - "handlers": { - "mail_admins": { - "level": "ERROR", - "filters": ["require_debug_false"], - "class": "django.utils.log.AdminEmailHandler", - }, - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - }, - "root": {"level": "INFO", "handlers": ["console"]}, - "loggers": { - "django.request": { - "handlers": ["mail_admins"], - "level": "ERROR", - "propagate": True, - }, - "django.security.DisallowedHost": { - "level": "ERROR", - "handlers": ["console", "mail_admins"], - "propagate": True, - }, - }, -} - - -# Your stuff... -# ------------------------------------------------------------------------------ diff --git a/config/settings/test.py b/config/settings/test.py deleted file mode 100644 index cf26dfd..0000000 --- a/config/settings/test.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -With these settings, tests run faster. -""" - -from .base import * # noqa: F403 -from .base import TEMPLATES -from .base import env - -# GENERAL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key -SECRET_KEY = env( - "DJANGO_SECRET_KEY", - default="ZORKsoJTGuQ8UzeUL2sl6MiwSUDqxS1ny7FmEW2ElkBAtWQRCsSeFgfa3fUy9b6V", -) -# https://docs.djangoproject.com/en/dev/ref/settings/#test-runner -TEST_RUNNER = "django.test.runner.DiscoverRunner" - -# PASSWORDS -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers -PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] - -# EMAIL -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend -EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" - -# DEBUGGING FOR TEMPLATES -# ------------------------------------------------------------------------------ -TEMPLATES[0]["OPTIONS"]["debug"] = True # type: ignore[index] - -# MEDIA -# ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#media-url -MEDIA_URL = "http://media.testserver/" -# Your stuff... -# ------------------------------------------------------------------------------ diff --git a/config/urls.py b/config/urls.py index fe48b61..e53ad32 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,70 +1,117 @@ -from django.conf import settings -from django.conf.urls.static import static from django.contrib import admin -from django.urls import include from django.urls import path -from django.views import defaults as default_views from django.views.generic import TemplateView + +def _page(template: str, lang: str, page_name: str, alt_url: str) -> TemplateView: + return TemplateView.as_view( + template_name=template, + extra_context={ + "lang": lang, + "page_name": page_name, + "alt_lang_url": alt_url, + }, + ) + + urlpatterns = [ - path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), + path("", _page("pages/home.html", "ja", "home", "/en/"), name="home"), path( - "services/", - TemplateView.as_view(template_name="pages/services.html"), - name="services", + "vision/", + _page("pages/vision.html", "ja", "vision", "/en/vision/"), + name="vision", ), path( - "company/", - TemplateView.as_view(template_name="pages/company.html"), - name="company", + "software/", + _page("pages/software.html", "ja", "software", "/en/software/"), + name="software", ), path( - "about/", - TemplateView.as_view(template_name="pages/company.html"), - name="about", + "holdings/", + _page("pages/holdings.html", "ja", "holdings", "/en/holdings/"), + name="holdings", + ), + path( + "business/", + _page("pages/business.html", "ja", "business", "/en/business/"), + name="business", + ), + path( + "company/", + _page("pages/company.html", "ja", "company", "/en/company/"), + name="company", ), path( "contact/", - TemplateView.as_view(template_name="pages/contact.html"), + _page("pages/contact.html", "ja", "contact", "/en/contact/"), name="contact", ), - # Django Admin, use {% url 'admin:index' %} - path(settings.ADMIN_URL, admin.site.urls), - # User management - path("users/", include("ether.users.urls", namespace="users")), - path("accounts/", include("allauth.urls")), - # Your stuff: custom urls includes go here - # ... - # Media files - *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), -] - - -if settings.DEBUG: - # This allows the error pages to be debugged during development, just visit - # these url in browser to see how these error pages look like. - urlpatterns += [ - path( - "400/", - default_views.bad_request, - kwargs={"exception": Exception("Bad Request!")}, - ), - path( - "403/", - default_views.permission_denied, - kwargs={"exception": Exception("Permission Denied")}, + path( + "legal/privacy/", + _page("pages/legal/privacy.html", "ja", "privacy", "/en/legal/privacy/"), + name="privacy", + ), + path( + "legal/cookies/", + _page("pages/legal/cookies.html", "ja", "cookies", "/en/legal/cookies/"), + name="cookies", + ), + path("en/", _page("pages/en/home.html", "en", "home", "/"), name="home_en"), + path( + "en/vision/", + _page("pages/en/vision.html", "en", "vision", "/vision/"), + name="vision_en", + ), + path( + "en/software/", + _page("pages/en/software.html", "en", "software", "/software/"), + name="software_en", + ), + path( + "en/holdings/", + _page("pages/en/holdings.html", "en", "holdings", "/holdings/"), + name="holdings_en", + ), + path( + "en/business/", + _page("pages/en/business.html", "en", "business", "/business/"), + name="business_en", + ), + path( + "en/company/", + _page("pages/en/company.html", "en", "company", "/company/"), + name="company_en", + ), + path( + "en/contact/", + _page("pages/en/contact.html", "en", "contact", "/contact/"), + name="contact_en", + ), + path( + "en/legal/privacy/", + _page("pages/en/legal/privacy.html", "en", "privacy", "/legal/privacy/"), + name="privacy_en", + ), + path( + "en/legal/cookies/", + _page("pages/en/legal/cookies.html", "en", "cookies", "/legal/cookies/"), + name="cookies_en", + ), + path( + "robots.txt", + TemplateView.as_view( + template_name="robots.txt", + content_type="text/plain", ), - path( - "404/", - default_views.page_not_found, - kwargs={"exception": Exception("Page not Found")}, + name="robots", + ), + path( + "sitemap.xml", + TemplateView.as_view( + template_name="sitemap.xml", + content_type="application/xml", ), - path("500/", default_views.server_error), - ] - if "debug_toolbar" in settings.INSTALLED_APPS: - import debug_toolbar - - urlpatterns = [ - path("__debug__/", include(debug_toolbar.urls)), - *urlpatterns, - ] + name="sitemap", + ), + path("admin/", admin.site.urls), +] diff --git a/config/wsgi.py b/config/wsgi.py index 826b2d7..a390bbe 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -1,32 +1,13 @@ """ -WSGI config for Ether Organization Website project. - -This module contains the WSGI application used by Django's development server -and any production WSGI deployments. It should expose a module-level variable -named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover -this application via the ``WSGI_APPLICATION`` setting. - -Usually you will have the standard Django WSGI application here, but it also -might make sense to replace the whole Django WSGI application with a custom one -that later delegates to the Django one. For example, you could introduce WSGI -middleware here, or combine a Django application with an application of another -framework. +WSGI config for Ether合同会社 website. +It exposes the WSGI callable as a module-level variable named ``application``. """ import os -import sys -from pathlib import Path from django.core.wsgi import get_wsgi_application -# This allows easy placement of apps within the interior -# ether directory. -BASE_DIR = Path(__file__).resolve(strict=True).parent.parent -sys.path.append(str(BASE_DIR / "ether")) -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") -# This application object is used by any WSGI server configured to use this -# file. This includes Django's development server, if the WSGI_APPLICATION -# setting points here. application = get_wsgi_application() diff --git a/docker-compose.docs.yml b/docker-compose.docs.yml deleted file mode 100644 index cf7de53..0000000 --- a/docker-compose.docs.yml +++ /dev/null @@ -1,17 +0,0 @@ -services: - docs: - image: ether_local_docs - container_name: ether_local_docs - build: - context: . - dockerfile: ./compose/local/docs/Dockerfile - env_file: - - ./.envs/.local/.django - volumes: - - /app/.venv - - ./docs:/docs:z - - ./config:/app/config:z - - ./ether:/app/ether:z - ports: - - '9000:9000' - command: /start-docs diff --git a/docker-compose.local.yml b/docker-compose.local.yml deleted file mode 100644 index 41104d9..0000000 --- a/docker-compose.local.yml +++ /dev/null @@ -1,42 +0,0 @@ -volumes: - ether_local_postgres_data: {} - ether_local_postgres_data_backups: {} - - -services: - django: - build: - context: . - dockerfile: ./compose/local/django/Dockerfile - image: ether_local_django - container_name: ether_local_django - depends_on: - - postgres - - mailpit - volumes: - - /app/.venv - - .:/app:z - env_file: - - ./.envs/.local/.django - - ./.envs/.local/.postgres - ports: - - '8000:8000' - command: /start - - postgres: - build: - context: . - dockerfile: ./compose/production/postgres/Dockerfile - image: ether_production_postgres - container_name: ether_local_postgres - volumes: - - ether_local_postgres_data:/var/lib/postgresql/18/docker - - ether_local_postgres_data_backups:/backups - env_file: - - ./.envs/.local/.postgres - - mailpit: - image: docker.io/axllent/mailpit:latest - container_name: ether_local_mailpit - ports: - - "8025:8025" diff --git a/docker-compose.production.yml b/docker-compose.production.yml deleted file mode 100644 index d1e3275..0000000 --- a/docker-compose.production.yml +++ /dev/null @@ -1,62 +0,0 @@ -volumes: - production_postgres_data: {} - production_postgres_data_backups: {} - production_traefik: {} - production_django_media: {} - - - -services: - django: - build: - context: . - dockerfile: ./compose/production/django/Dockerfile - - image: ether_production_django - volumes: - - production_django_media:/app/ether/media - depends_on: - - postgres - - redis - env_file: - - ./.envs/.production/.django - - ./.envs/.production/.postgres - command: /start - - postgres: - build: - context: . - dockerfile: ./compose/production/postgres/Dockerfile - image: ether_production_postgres - volumes: - - production_postgres_data:/var/lib/postgresql/18/docker - - production_postgres_data_backups:/backups - env_file: - - ./.envs/.production/.postgres - - traefik: - build: - context: . - dockerfile: ./compose/production/traefik/Dockerfile - image: ether_production_traefik - depends_on: - - django - volumes: - - production_traefik:/etc/traefik/acme - ports: - - '0.0.0.0:80:80' - - '0.0.0.0:443:443' - - redis: - image: docker.io/redis:8.6 - - - nginx: - build: - context: . - dockerfile: ./compose/production/nginx/Dockerfile - image: ether_production_nginx - depends_on: - - django - volumes: - - production_django_media:/usr/share/nginx/media:ro diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 6957700..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,29 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = ./_build -APP = /app - -.PHONY: help livehtml apidocs Makefile - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -c . - -# Build, watch and serve docs with live reload -livehtml: - sphinx-autobuild -b html --host 0.0.0.0 --port 9000 --watch $(APP) -c . $(SOURCEDIR) $(BUILDDIR)/html - -# Outputs rst files from django application code -apidocs: - sphinx-apidoc -o $(SOURCEDIR)/api $(APP) - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -c . diff --git a/docs/__init__.py b/docs/__init__.py deleted file mode 100644 index 8772c82..0000000 --- a/docs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Included so that Django's startproject comment runs against the docs directory diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 4dec010..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,64 +0,0 @@ -# ruff: noqa: ERA001, PTH100 -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. - -import os -import sys - -import django - -if os.getenv("READTHEDOCS", default="False") == "True": - sys.path.insert(0, os.path.abspath("..")) - os.environ["DJANGO_READ_DOT_ENV_FILE"] = "True" - os.environ["USE_DOCKER"] = "no" -else: - sys.path.insert(0, os.path.abspath("/app")) -os.environ["DATABASE_URL"] = "sqlite:///readthedocs.db" -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") -django.setup() - -# -- Project information ----------------------------------------------------- - -project = "Ether Organization Website" -copyright = """2026, Ether LLC""" # noqa: A001 -author = "Ether LLC" - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.napoleon", -] - -# Add any paths that contain templates here, relative to this directory. -# templates_path = ["_templates"] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "alabaster" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ["_static"] diff --git a/docs/howto.rst b/docs/howto.rst deleted file mode 100644 index 20a0bb4..0000000 --- a/docs/howto.rst +++ /dev/null @@ -1,38 +0,0 @@ -How To - Project Documentation -====================================================================== - -Get Started ----------------------------------------------------------------------- - -Documentation can be written as rst files in `ether/docs`. - - -To build and serve docs, use the commands:: - - docker compose -f docker-compose.docs.yml up - - - -Changes to files in `docs/_source` will be picked up and reloaded automatically. - -`Sphinx `_ is the tool used to build documentation. - -Docstrings to Documentation ----------------------------------------------------------------------- - -The sphinx extension `apidoc `_ is used to automatically document code using signatures and docstrings. - -Numpy or Google style docstrings will be picked up from project files and available for documentation. See the `Napoleon `_ extension for details. - -For an in-use example, see the `page source <_sources/users.rst.txt>`_ for :ref:`users`. - -To compile all docstrings automatically into documentation source files, use the command: - :: - - uv run make apidocs - - -This can be done in the docker container: - :: - - docker run --rm docs make apidocs diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 5291c7a..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,23 +0,0 @@ -.. Ether Organization Website documentation master file, created by - sphinx-quickstart. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Ether Organization Website's documentation! -====================================================================== - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - howto - users - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 72db9e6..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,46 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -c . -) -set SOURCEDIR=_source -set BUILDDIR=_build -set APP=..\ether - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.Install sphinx-autobuild for live serving. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -b %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:livehtml -sphinx-autobuild -b html --open-browser -p 9000 --watch %APP% -c . %SOURCEDIR% %BUILDDIR%/html -GOTO :EOF - -:apidocs -sphinx-apidoc -o %SOURCEDIR%/api %APP% -GOTO :EOF - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/users.rst b/docs/users.rst deleted file mode 100644 index 2f543c3..0000000 --- a/docs/users.rst +++ /dev/null @@ -1,15 +0,0 @@ - .. _users: - -Users -====================================================================== - -Starting a new project, it’s highly recommended to set up a custom user model, -even if the default User model is sufficient for you. - -This model behaves identically to the default user model, -but you’ll be able to customize it in the future if the need arises. - -.. automodule:: ether.users.models - :members: - :noindex: - diff --git a/ether/conftest.py b/ether/conftest.py deleted file mode 100644 index 75e0b2e..0000000 --- a/ether/conftest.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from ether.users.models import User -from ether.users.tests.factories import UserFactory - - -@pytest.fixture(autouse=True) -def _media_storage(settings, tmpdir) -> None: - settings.MEDIA_ROOT = tmpdir.strpath - - -@pytest.fixture -def user(db) -> User: - return UserFactory.create() diff --git a/ether/contrib/__init__.py b/ether/contrib/__init__.py deleted file mode 100644 index 6b59124..0000000 --- a/ether/contrib/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -To understand why this file is here, please read: - -https://cookiecutter-django.readthedocs.io/en/latest/5-help/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django -""" diff --git a/ether/contrib/sites/__init__.py b/ether/contrib/sites/__init__.py deleted file mode 100644 index 6b59124..0000000 --- a/ether/contrib/sites/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -To understand why this file is here, please read: - -https://cookiecutter-django.readthedocs.io/en/latest/5-help/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django -""" diff --git a/ether/contrib/sites/migrations/0001_initial.py b/ether/contrib/sites/migrations/0001_initial.py deleted file mode 100644 index fd76afb..0000000 --- a/ether/contrib/sites/migrations/0001_initial.py +++ /dev/null @@ -1,43 +0,0 @@ -import django.contrib.sites.models -from django.contrib.sites.models import _simple_domain_name_validator -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="Site", - fields=[ - ( - "id", - models.AutoField( - verbose_name="ID", - serialize=False, - auto_created=True, - primary_key=True, - ), - ), - ( - "domain", - models.CharField( - max_length=100, - verbose_name="domain name", - validators=[_simple_domain_name_validator], - ), - ), - ("name", models.CharField(max_length=50, verbose_name="display name")), - ], - options={ - "ordering": ("domain",), - "db_table": "django_site", - "verbose_name": "site", - "verbose_name_plural": "sites", - }, - bases=(models.Model,), - managers=[("objects", django.contrib.sites.models.SiteManager())], - ), - ] diff --git a/ether/contrib/sites/migrations/0002_alter_domain_unique.py b/ether/contrib/sites/migrations/0002_alter_domain_unique.py deleted file mode 100644 index 4a44a6a..0000000 --- a/ether/contrib/sites/migrations/0002_alter_domain_unique.py +++ /dev/null @@ -1,21 +0,0 @@ -import django.contrib.sites.models -from django.db import migrations -from django.db import models - - -class Migration(migrations.Migration): - - dependencies = [("sites", "0001_initial")] - - operations = [ - migrations.AlterField( - model_name="site", - name="domain", - field=models.CharField( - max_length=100, - unique=True, - validators=[django.contrib.sites.models._simple_domain_name_validator], - verbose_name="domain name", - ), - ) - ] diff --git a/ether/contrib/sites/migrations/0003_set_site_domain_and_name.py b/ether/contrib/sites/migrations/0003_set_site_domain_and_name.py deleted file mode 100644 index 7d42a22..0000000 --- a/ether/contrib/sites/migrations/0003_set_site_domain_and_name.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -To understand why this file is here, please read: - -https://cookiecutter-django.readthedocs.io/en/latest/5-help/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django -""" -from django.conf import settings -from django.db import migrations - - -def _update_or_create_site_with_sequence(site_model, connection, domain, name): - """Update or create the site with default ID and keep the DB sequence in sync.""" - site, created = site_model.objects.update_or_create( - id=settings.SITE_ID, - defaults={ - "domain": domain, - "name": name, - }, - ) - if created: - # We provided the ID explicitly when creating the Site entry, therefore the DB - # sequence to auto-generate them wasn't used and is now out of sync. If we - # don't do anything, we'll get a unique constraint violation the next time a - # site is created. - # To avoid this, we need to manually update DB sequence and make sure it's - # greater than the maximum value. - max_id = site_model.objects.order_by("-id").first().id - with connection.cursor() as cursor: - cursor.execute("SELECT last_value from django_site_id_seq") - (current_id,) = cursor.fetchone() - if current_id <= max_id: - cursor.execute( - "alter sequence django_site_id_seq restart with %s", - [max_id + 1], - ) - - -def update_site_forward(apps, schema_editor): - """Set site domain and name.""" - Site = apps.get_model("sites", "Site") - _update_or_create_site_with_sequence( - Site, - schema_editor.connection, - "ether.local", - "Ether Organization Website", - ) - - -def update_site_backward(apps, schema_editor): - """Revert site domain and name to default.""" - Site = apps.get_model("sites", "Site") - _update_or_create_site_with_sequence( - Site, - schema_editor.connection, - "example.com", - "example.com", - ) - - -class Migration(migrations.Migration): - - dependencies = [("sites", "0002_alter_domain_unique")] - - operations = [migrations.RunPython(update_site_forward, update_site_backward)] diff --git a/ether/contrib/sites/migrations/0004_alter_options_ordering_domain.py b/ether/contrib/sites/migrations/0004_alter_options_ordering_domain.py deleted file mode 100644 index f7118ca..0000000 --- a/ether/contrib/sites/migrations/0004_alter_options_ordering_domain.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.1.7 on 2021-02-04 14:49 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("sites", "0003_set_site_domain_and_name"), - ] - - operations = [ - migrations.AlterModelOptions( - name="site", - options={ - "ordering": ["domain"], - "verbose_name": "site", - "verbose_name_plural": "sites", - }, - ), - ] diff --git a/ether/contrib/sites/migrations/__init__.py b/ether/contrib/sites/migrations/__init__.py deleted file mode 100644 index 6b59124..0000000 --- a/ether/contrib/sites/migrations/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -To understand why this file is here, please read: - -https://cookiecutter-django.readthedocs.io/en/latest/5-help/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django -""" diff --git a/ether/static/css/project.css b/ether/static/css/project.css index a082441..fd54212 100644 --- a/ether/static/css/project.css +++ b/ether/static/css/project.css @@ -1,25 +1,71 @@ /* ========================================================= - Ether dark editorial system + Ether Design System + Inspired by Material 3 — user-centred, calm, accessible. ========================================================= */ :root { - --color-black: #000000; - --color-ink: #f5f5f5; - --color-muted: #a3a3a3; - --color-subtle: #737373; - --color-panel: #1f1f1f; - --color-panel-soft: #151515; - --color-panel-raised: #2e2e2e; - --color-border: #333333; - --color-border-soft: #242424; - --color-accent: #f5f5f5; - --color-focus: #ffffff; - --font-sans: - "Inter", "Noto Sans JP", "Hiragino Sans", "Yu Gothic", "Helvetica Neue", - Arial, sans-serif; - --radius-sm: 6px; - --radius-md: 10px; - --radius-pill: 999px; + /* ------- Color tokens (Material 3 style) ------- */ + --md-primary: #00696b; + --md-on-primary: #ffffff; + --md-primary-container: #6ff7f6; + --md-on-primary-container: #002021; + + --md-secondary: #4a6364; + --md-on-secondary: #ffffff; + --md-secondary-container: #cce8e8; + --md-on-secondary-container: #051f20; + + --md-tertiary: #4b5f7d; + --md-tertiary-container: #d3e4ff; + --md-on-tertiary-container: #001c38; + + --md-error: #ba1a1a; + --md-on-error: #ffffff; + --md-error-container: #ffdad6; + --md-on-error-container: #410002; + + --md-surface: #f5fafa; + --md-surface-dim: #d5dbdb; + --md-surface-bright: #ffffff; + --md-surface-container-lowest: #ffffff; + --md-surface-container-low: #eff5f5; + --md-surface-container: #e9efef; + --md-surface-container-high: #e3eaea; + --md-surface-container-highest: #dee4e4; + + --md-on-surface: #161d1d; + --md-on-surface-variant: #3f4948; + --md-outline: #6f7979; + --md-outline-variant: #bec9c8; + --md-inverse-surface: #2b3231; + --md-inverse-on-surface: #eff1f0; + + --md-scrim: rgba(0, 0, 0, 0.45); + + /* ------- Elevation (Material 3) ------- */ + --md-elevation-1: + 0 1px 2px rgba(0, 32, 33, 0.30), + 0 1px 3px 1px rgba(0, 32, 33, 0.10); + --md-elevation-2: + 0 1px 2px rgba(0, 32, 33, 0.30), + 0 2px 6px 2px rgba(0, 32, 33, 0.10); + --md-elevation-3: + 0 4px 8px 3px rgba(0, 32, 33, 0.10), + 0 1px 3px rgba(0, 32, 33, 0.20); + --md-elevation-4: + 0 6px 10px 4px rgba(0, 32, 33, 0.10), + 0 2px 3px rgba(0, 32, 33, 0.18); + + /* ------- Shape ------- */ + --radius-xs: 4px; + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 24px; + --radius-2xl: 28px; + --radius-full: 9999px; + + /* ------- Spacing scale (8pt grid) ------- */ --space-1: 4px; --space-2: 8px; --space-3: 12px; @@ -29,11 +75,23 @@ --space-7: 48px; --space-8: 64px; --space-9: 96px; - --space-10: 128px; - --ease: cubic-bezier(0.2, 0, 0, 1); - --duration: 180ms; + + /* ------- Type ------- */ + --font-sans: + "Inter", "Noto Sans JP", "Hiragino Sans", "Yu Gothic", "Helvetica Neue", + Arial, sans-serif; + --font-display: var(--font-sans); + + /* ------- Motion ------- */ + --ease-emphasized: cubic-bezier(0.2, 0, 0, 1); + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --duration-short: 150ms; + --duration-medium: 250ms; + --duration-long: 400ms; } +/* ----------- Base ----------- */ + *, *::before, *::after { @@ -47,30 +105,18 @@ html { } body { - min-width: 320px; margin: 0; - color: var(--color-ink); - background: var(--color-black); + color: var(--md-on-surface); + background: var(--md-surface); font-family: var(--font-sans); - font-feature-settings: "palt"; + font-feature-settings: "palt", "ss01"; + font-weight: 400; line-height: 1.6; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -body.bg-light { - color: #161d1d; -} - -body.bg-light a:not(.btn) { - color: #00696b; -} - -body.bg-light a:not(.btn):hover { - color: #003f40; -} - img, svg { display: block; @@ -78,131 +124,106 @@ svg { } a { - color: var(--color-ink); + color: var(--md-primary); text-decoration: none; - transition: - color var(--duration) var(--ease), - opacity var(--duration) var(--ease), - background-color var(--duration) var(--ease), - border-color var(--duration) var(--ease); + transition: color var(--duration-short) var(--ease-standard); } a:hover { - color: var(--color-ink); - opacity: 0.72; -} - -button, -input, -textarea, -select { - font: inherit; + color: var(--md-on-primary-container); } :focus-visible { - outline: 2px solid var(--color-focus); - outline-offset: 4px; + outline: 3px solid var(--md-primary); + outline-offset: 3px; + border-radius: 6px; } ::selection { - color: var(--color-black); - background: var(--color-ink); + color: var(--md-on-primary-container); + background: var(--md-primary-container); } .material-symbols-rounded { - font-variation-settings: "FILL" 0, "wght" 350, "GRAD" 0, "opsz" 24; - font-size: 18px; + font-variation-settings: "FILL" 0, "wght" 500, "GRAD" 0, "opsz" 24; + font-size: 20px; line-height: 1; vertical-align: middle; } +/* Skip link */ .skip-link { position: absolute; top: -100px; - left: 24px; + left: 16px; z-index: 1000; - padding: 10px 16px; - color: var(--color-black); - background: var(--color-ink); - border-radius: var(--radius-pill); - font-size: 0.9rem; + padding: 12px 20px; + color: var(--md-on-primary); + background: var(--md-primary); + border-radius: var(--radius-full); font-weight: 600; + transition: top var(--duration-medium) var(--ease-emphasized); } .skip-link:focus { - top: 18px; -} - -.container { - max-width: 1248px; - padding-right: 28px; - padding-left: 28px; -} - -.container-narrow { - max-width: 1040px; + top: 16px; + color: var(--md-on-primary); } -.container-reading { - max-width: 940px; -} - -/* Header */ +/* ----------- Header / Nav ----------- */ .site-header { position: sticky; top: 0; z-index: 50; - background: rgba(0, 0, 0, 0.72); - border-bottom: 1px solid transparent; - backdrop-filter: blur(18px); - -webkit-backdrop-filter: blur(18px); + background: rgba(245, 250, 250, 0.78); + backdrop-filter: saturate(180%) blur(18px); + -webkit-backdrop-filter: saturate(180%) blur(18px); transition: - background-color var(--duration) var(--ease), - border-color var(--duration) var(--ease); + background var(--duration-medium) var(--ease-standard), + box-shadow var(--duration-medium) var(--ease-standard), + border-color var(--duration-medium) var(--ease-standard); + border-bottom: 1px solid transparent; } .site-header.is-scrolled { - background: rgba(0, 0, 0, 0.88); - border-bottom-color: rgba(255, 255, 255, 0.08); + background: rgba(245, 250, 250, 0.92); + border-bottom-color: var(--md-outline-variant); + box-shadow: var(--md-elevation-1); } .navbar { - min-height: 68px; + min-height: 72px; padding: 0; } -.navbar-brand, -.footer-brand { +.navbar-brand { display: inline-flex; align-items: center; - gap: 10px; - color: var(--color-ink); - font-size: 0.94rem; + gap: var(--space-3); + color: var(--md-on-surface); + font-size: 1rem; font-weight: 700; letter-spacing: 0; } .navbar-brand:hover, -.navbar-brand:focus, -.footer-brand:hover, -.footer-brand:focus { - color: var(--color-ink); - opacity: 1; +.navbar-brand:focus { + color: var(--md-on-surface); } .brand-mark { - display: inline-block; - width: 28px; - height: 28px; - color: var(--color-ink); - background: - linear-gradient(currentColor, currentColor) 5px 8px / 3px 12px no-repeat, - linear-gradient(currentColor, currentColor) 11px 4px / 3px 20px no-repeat, - linear-gradient(currentColor, currentColor) 17px 6px / 3px 16px no-repeat, - linear-gradient(currentColor, currentColor) 23px 8px / 3px 12px no-repeat; - border-radius: 999px; - font-size: 0; + display: inline-grid; + width: 36px; + height: 36px; + place-items: center; + color: var(--md-on-primary); + background: linear-gradient(135deg, var(--md-primary), #00838a); + border-radius: var(--radius-md); + font-weight: 800; + font-size: 1rem; + letter-spacing: 0; + box-shadow: var(--md-elevation-1); } .brand-name { @@ -210,736 +231,950 @@ select { } .navbar-nav { - gap: var(--space-2); + gap: var(--space-1); } -.navbar .nav-link { +.nav-link { display: inline-flex; align-items: center; - min-height: 36px; - padding: 0.45rem 0.7rem !important; - color: var(--color-muted) !important; - border-radius: var(--radius-pill); - font-size: 0.82rem; + padding: 0.55rem 0.95rem !important; + color: var(--md-on-surface-variant); + font-size: 0.92rem; font-weight: 600; letter-spacing: 0; + border-radius: var(--radius-full); + transition: + color var(--duration-short) var(--ease-standard), + background-color var(--duration-short) var(--ease-standard); } -.navbar .nav-link:hover, -.navbar .nav-link.active { - color: var(--color-ink) !important; - background: rgba(255, 255, 255, 0.08); - opacity: 1; +.nav-link:hover { + color: var(--md-on-surface); + background-color: rgba(0, 105, 107, 0.08); +} + +.nav-link.active { + color: var(--md-on-primary-container); + background-color: var(--md-primary-container); +} + +.nav-link:focus-visible { + outline-offset: 2px; } .nav-cta { + display: inline-flex; + align-items: center; + gap: 0.4rem; margin-left: var(--space-2); } .navbar-toggler { display: inline-grid; - width: 40px; - height: 40px; + width: 44px; + height: 44px; place-items: center; - color: var(--color-ink); + color: var(--md-on-surface); background: transparent; border: 0; - border-radius: var(--radius-pill); + border-radius: var(--radius-full); + transition: background-color var(--duration-short) var(--ease-standard); } .navbar-toggler:hover, .navbar-toggler:focus { - color: var(--color-ink); - background: rgba(255, 255, 255, 0.08); + background-color: rgba(0, 105, 107, 0.10); box-shadow: none; } -/* Buttons and links */ +.navbar-toggler:focus-visible { + outline-offset: 2px; +} + +/* ----------- Buttons ----------- */ .btn { display: inline-flex; align-items: center; justify-content: center; - gap: 8px; - min-height: 42px; - padding: 0.63rem 1.15rem; - color: var(--color-ink); - background: var(--color-panel); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: var(--radius-pill); + gap: 0.5rem; + min-height: 44px; + padding: 0.65rem 1.4rem; + border-radius: var(--radius-full); font-family: var(--font-sans); - font-size: 0.9rem; + font-size: 0.95rem; font-weight: 600; letter-spacing: 0; text-decoration: none; + border: 1px solid transparent; + transition: + transform var(--duration-short) var(--ease-emphasized), + box-shadow var(--duration-short) var(--ease-standard), + background-color var(--duration-short) var(--ease-standard), + color var(--duration-short) var(--ease-standard), + border-color var(--duration-short) var(--ease-standard); } -.btn:hover, -.btn:focus { - color: var(--color-ink); - background: #2a2a2a; - border-color: rgba(255, 255, 255, 0.16); - opacity: 1; +.btn:focus-visible { + outline-offset: 3px; +} + +.btn .material-symbols-rounded { + font-size: 20px; } .btn-filled, .btn-primary { - color: var(--color-black); - background: var(--color-ink); - border-color: var(--color-ink); + color: var(--md-on-primary); + background: var(--md-primary); + border-color: var(--md-primary); } .btn-filled:hover, .btn-primary:hover, .btn-filled:focus, .btn-primary:focus { - color: var(--color-black); - background: #dcdcdc; - border-color: #dcdcdc; + color: var(--md-on-primary); + background: #00585a; + border-color: #00585a; + box-shadow: var(--md-elevation-2); + transform: translateY(-1px); } -.btn-muted, -.btn-tonal, -.btn-outline-light { - color: var(--color-ink); - background: #2a2a2a; - border-color: #343434; +.btn-tonal { + color: var(--md-on-secondary-container); + background: var(--md-secondary-container); + border-color: transparent; } -.btn-muted:hover, .btn-tonal:hover, +.btn-tonal:focus { + color: var(--md-on-secondary-container); + background: #b6d3d3; + box-shadow: var(--md-elevation-1); + transform: translateY(-1px); +} + +.btn-outline-light { + color: #ffffff; + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.45); + backdrop-filter: blur(8px); +} + .btn-outline-light:hover, -.btn-muted:focus, -.btn-tonal:focus, .btn-outline-light:focus { - color: var(--color-ink); - background: #383838; - border-color: #474747; + color: var(--md-on-primary-container); + background: rgba(255, 255, 255, 0.95); + border-color: rgba(255, 255, 255, 0.95); + transform: translateY(-1px); } -.btn-text, -.text-link { - display: inline-flex; - align-items: center; - gap: 6px; - color: var(--color-ink); +.btn-text { + padding: 0.6rem 0.9rem; + color: var(--md-primary); background: transparent; - border: 0; - font-weight: 600; } -.text-link::after { - content: ">"; - display: inline-block; - margin-left: 6px; +.btn-text:hover, +.btn-text:focus { + color: var(--md-on-primary-container); + background: rgba(0, 105, 107, 0.08); } -.inline-actions { - display: flex; - flex-wrap: wrap; - gap: var(--space-3); - margin-top: var(--space-5); -} +/* ----------- Layout primitives ----------- */ -/* Type and layout */ +.container { + max-width: 1200px; +} .section { - padding: var(--space-10) 0; + padding: var(--space-9) 0; } -.section-feature { - padding-top: var(--space-8); - padding-bottom: var(--space-8); +.section-tight { + padding: var(--space-8) 0; } -.hero-section, -.page-hero { - padding: 128px 0 104px; - text-align: center; +.section-muted { + background: var(--md-surface-container-low); } -.page-hero { - padding-bottom: var(--space-7); +.section-emphasis { + background: var(--md-surface-container); } -.editorial-hero { - display: flex; - min-height: min(600px, 68vh); - align-items: flex-start; - padding-top: 132px; - padding-bottom: var(--space-8); +.section-heading { + max-width: 760px; + margin-bottom: var(--space-7); +} + +.section-heading h2 { + margin: 0; +} + +.section-heading .lead { + margin-top: var(--space-4); } -.section-label, .eyebrow { - display: block; - margin: 0 0 var(--space-5); - color: var(--color-muted); - background: transparent; - border: 0; - border-radius: 0; - font-size: 0.86rem; - font-weight: 600; - letter-spacing: 0; - text-transform: none; + display: inline-flex; + align-items: center; + gap: 0.5rem; + margin: 0 0 var(--space-4); + padding: 0.35rem 0.85rem; + color: var(--md-on-primary-container); + background: var(--md-primary-container); + border-radius: var(--radius-full); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; } .eyebrow .dot { - display: none; + display: inline-block; + width: 6px; + height: 6px; + background: var(--md-primary); + border-radius: var(--radius-full); } +/* ----------- Typography ----------- */ + h1, h2, h3, h4 { - margin-top: 0; - color: var(--color-ink); - font-family: var(--font-sans); - font-weight: 500; - letter-spacing: 0; + font-family: var(--font-display); + color: var(--md-on-surface); + letter-spacing: -0.01em; + font-weight: 800; } -.hero-section h1, -.page-hero h1 { - max-width: 820px; - margin: 0 auto; - font-size: clamp(3rem, 5.2vw, 4.3rem); - line-height: 1.12; +.display-1 { + margin: 0; + font-size: clamp(2.6rem, 5.5vw, 4.5rem); + line-height: 1.06; + letter-spacing: -0.02em; + font-weight: 800; } -.hero-lead, -.page-hero p { - max-width: 760px; - margin: var(--space-5) auto 0; - color: var(--color-ink); - font-size: clamp(1.08rem, 1.8vw, 1.35rem); - line-height: 1.75; +.headline { + margin: 0; + font-size: clamp(1.9rem, 3.4vw, 2.6rem); + line-height: 1.18; + letter-spacing: -0.015em; + font-weight: 800; } -.page-hero p { - color: var(--color-muted); - font-size: 1.06rem; +.title { + margin: 0; + font-size: 1.25rem; + line-height: 1.4; + font-weight: 700; } -.story-block { - margin-top: 0; +.lead { + margin: 0; + color: var(--md-on-surface-variant); + font-size: 1.1rem; + line-height: 1.85; } -.story-block h2, -.section-text h2, -.contact-panel h2 { - max-width: 760px; - margin-bottom: var(--space-4); - font-size: 2.2rem; - line-height: 1.25; +.body-lg { + color: var(--md-on-surface-variant); + font-size: 1.02rem; + line-height: 1.95; } -.story-block p, -.section-text p, -.contact-panel p { - max-width: 780px; +.section h2, +.compact-hero h1, +.contact-panel h2 { margin: 0; - color: var(--color-muted); + font-size: clamp(1.9rem, 3.4vw, 2.6rem); + line-height: 1.18; + letter-spacing: -0.015em; + font-weight: 800; +} + +.section p, +.large-copy, +.page-hero p, +.contact-panel p, +.service-detail p { + color: var(--md-on-surface-variant); font-size: 1.05rem; - line-height: 1.9; + line-height: 1.95; } -.section-text .text-link { - margin-top: var(--space-5); +/* ----------- Hero ----------- */ + +.hero-section { + position: relative; + isolation: isolate; + overflow: hidden; + padding: clamp(80px, 12vh, 140px) 0 clamp(72px, 10vh, 120px); + background: var(--md-surface); } -.statement-section { - padding: 176px 0 168px; - text-align: center; +.hero-section::before, +.hero-section::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + z-index: -1; } -.statement-section h2 { - margin: 0 auto; - font-size: 2.35rem; - line-height: 1.5; - font-weight: 500; +.hero-section::before { + background: + radial-gradient(60% 50% at 12% 0%, rgba(0, 131, 138, 0.22), transparent 70%), + radial-gradient(45% 50% at 95% 30%, rgba(111, 247, 246, 0.45), transparent 70%), + radial-gradient(40% 60% at 70% 100%, rgba(75, 95, 125, 0.18), transparent 70%); } -.center-copy { - max-width: 760px; - margin: 0 auto; - text-align: center; +.hero-section::after { + background: + linear-gradient(180deg, rgba(245, 250, 250, 0) 70%, rgba(245, 250, 250, 1) 100%); } -.center-copy h2 { - margin-bottom: var(--space-4); - font-size: 2.25rem; - line-height: 1.28; +.hero-grid { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); + gap: clamp(2rem, 5vw, 4.5rem); + align-items: center; } -.center-copy p { - margin: 0 auto var(--space-5); - color: var(--color-muted); - font-size: 1.05rem; +.hero-content { + max-width: 640px; } -.info-section { - padding-top: var(--space-9); - padding-bottom: 184px; +.hero-content h1 { + margin: 0; + font-size: clamp(2.4rem, 5.4vw, 4.25rem); + line-height: 1.05; + letter-spacing: -0.025em; + font-weight: 800; } -.abstract-field { - position: relative; - min-height: 520px; - overflow: hidden; - background: - linear-gradient(90deg, transparent 0, transparent 31px, rgba(255, 255, 255, 0.04) 32px), - linear-gradient(180deg, #222222, #181818); - background-size: 32px 32px, auto; - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); +.hero-content h1 .accent { + background: linear-gradient(120deg, var(--md-primary) 0%, #00838a 50%, #4b5f7d 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; } -.abstract-field::before, -.abstract-field::after { - content: ""; - position: absolute; - inset: 12%; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 50%; - transform: rotate(-8deg); +.hero-lead { + max-width: 540px; + margin: var(--space-5) 0 0; + color: var(--md-on-surface-variant); + font-size: clamp(1.05rem, 1.6vw, 1.22rem); + line-height: 1.85; } -.abstract-field::after { - inset: 22% 8%; - transform: rotate(12deg); +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + margin-top: var(--space-6); } -.field-line { - position: absolute; - right: 8%; - left: 8%; - height: 1px; - background: rgba(245, 245, 245, 0.32); - transform-origin: center; +.hero-meta { + display: flex; + flex-wrap: wrap; + gap: var(--space-5) var(--space-7); + margin-top: var(--space-7); + padding-top: var(--space-5); + border-top: 1px solid var(--md-outline-variant); } -.field-line-a { - top: 42%; - transform: rotate(8deg); +.hero-meta-item { + display: flex; + flex-direction: column; + gap: var(--space-1); } -.field-line-b { - top: 52%; - transform: rotate(-5deg); +.hero-meta-label { + color: var(--md-on-surface-variant); + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; } -.field-line-c { - top: 62%; - transform: rotate(2deg); +.hero-meta-value { + color: var(--md-on-surface); + font-size: 1rem; + font-weight: 700; } -.field-node { - position: absolute; - width: 8px; - height: 8px; - background: var(--color-ink); - border-radius: 50%; +/* Decorative panel on the hero right side */ +.hero-visual { + position: relative; + display: grid; + gap: var(--space-4); + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-auto-rows: 1fr; } -.field-node-a { - top: 33%; - left: 34%; +.hero-card { + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: var(--space-4); + min-height: 170px; + padding: var(--space-5); + background: var(--md-surface-container-lowest); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-xl); + box-shadow: var(--md-elevation-1); + transition: + transform var(--duration-medium) var(--ease-emphasized), + box-shadow var(--duration-medium) var(--ease-standard); } -.field-node-b { - right: 28%; - bottom: 28%; +.hero-card:hover { + transform: translateY(-4px); + box-shadow: var(--md-elevation-3); } -.signal-grid { - position: absolute; - inset: 14%; - display: grid; - grid-template-columns: repeat(6, 1fr); - gap: var(--space-4); +.hero-card.span-2 { + grid-column: span 2; } -.signal-grid span { - align-self: end; - min-height: 80px; - background: #343434; - border-radius: var(--radius-sm); +.hero-card.is-primary { + color: var(--md-on-primary-container); + background: linear-gradient(140deg, var(--md-primary-container), #aef0ee); + border-color: transparent; } -.signal-grid span:nth-child(2), -.signal-grid span:nth-child(5) { - min-height: 240px; +.hero-card.is-secondary { + color: var(--md-on-secondary-container); + background: var(--md-secondary-container); + border-color: transparent; } -.signal-grid span:nth-child(3) { - min-height: 340px; +.hero-card.is-tertiary { + color: var(--md-on-tertiary-container); + background: var(--md-tertiary-container); + border-color: transparent; } -.signal-grid span:nth-child(4) { - min-height: 180px; +.hero-card .card-icon { + display: inline-grid; + width: 44px; + height: 44px; + place-items: center; + background: rgba(255, 255, 255, 0.55); + border-radius: var(--radius-md); } -/* Learn more / services */ +.hero-card .card-icon .material-symbols-rounded { + font-size: 22px; + color: var(--md-on-primary-container); +} -.section-heading { - display: flex; - align-items: end; - justify-content: space-between; - gap: var(--space-5); - margin-bottom: var(--space-6); +.hero-card h3 { + margin: 0; + font-size: 1.05rem; + font-weight: 700; + letter-spacing: -0.005em; } -.section-heading h2 { +.hero-card p { margin: 0; - font-size: 1.55rem; - line-height: 1.35; + font-size: 0.88rem; + line-height: 1.55; + opacity: 0.82; } -.section-tabs { - display: inline-flex; - gap: var(--space-5); - color: var(--color-muted); - font-size: 0.94rem; +/* ----------- Section: split layout ----------- */ + +.split-layout { + display: grid; + grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr); + gap: clamp(2rem, 6vw, 5rem); + align-items: start; } -.section-tabs span:first-child { - color: var(--color-ink); +.split-layout .lead-block h2 { + margin: 0; } +/* ----------- Service grid (Material cards) ----------- */ + .service-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: var(--space-6); + gap: var(--space-4); } .service-item { - min-width: 0; -} - -.service-thumb { position: relative; - display: grid; - min-height: 190px; - margin-bottom: var(--space-4); - place-items: center; - color: var(--color-muted); - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent), - var(--color-panel-raised); - border: 1px solid rgba(255, 255, 255, 0.055); - border-radius: var(--radius-sm); + display: flex; + flex-direction: column; + gap: var(--space-4); + min-height: 280px; + padding: var(--space-6); + background: var(--md-surface-container-lowest); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-xl); + box-shadow: var(--md-elevation-1); overflow: hidden; + transition: + transform var(--duration-medium) var(--ease-emphasized), + box-shadow var(--duration-medium) var(--ease-standard), + border-color var(--duration-medium) var(--ease-standard); } -.service-thumb .material-symbols-rounded { - position: relative; - z-index: 1; - font-size: 34px; -} - -.service-thumb::before, -.service-thumb::after { +.service-item::after { content: ""; position: absolute; - pointer-events: none; -} - -.service-thumb::before { - inset: 18%; - opacity: 0.62; + right: -40px; + bottom: -40px; + width: 160px; + height: 160px; + background: radial-gradient(closest-side, rgba(0, 105, 107, 0.10), transparent 70%); + border-radius: var(--radius-full); + opacity: 0; + transition: opacity var(--duration-medium) var(--ease-standard); } -.service-item:nth-child(1) .service-thumb::before { - background: repeating-linear-gradient( - 90deg, - transparent 0 10px, - rgba(245, 245, 245, 0.18) 10px 12px, - transparent 12px 20px - ); - mask-image: radial-gradient(ellipse at center, #000 0 54%, transparent 72%); +.service-item:hover, +.service-item:focus-within { + transform: translateY(-4px); + border-color: transparent; + box-shadow: var(--md-elevation-3); } -.service-item:nth-child(2) .service-thumb::before { - inset: 20% 14%; - background: - radial-gradient(circle at 18% 46%, rgba(245, 245, 245, 0.22) 0 3px, transparent 4px), - radial-gradient(circle at 42% 34%, rgba(245, 245, 245, 0.2) 0 3px, transparent 4px), - radial-gradient(circle at 68% 42%, rgba(245, 245, 245, 0.18) 0 3px, transparent 4px), - linear-gradient(180deg, transparent 61%, rgba(245, 245, 245, 0.18) 62%, transparent 64%); +.service-item:hover::after, +.service-item:focus-within::after { + opacity: 1; } -.service-item:nth-child(3) .service-thumb::before { - inset: 24% 22%; - background: - linear-gradient(rgba(245, 245, 245, 0.22), rgba(245, 245, 245, 0.22)) 0 0 / 82% 2px no-repeat, - linear-gradient(rgba(245, 245, 245, 0.14), rgba(245, 245, 245, 0.14)) 0 28% / 62% 2px no-repeat, - linear-gradient(rgba(245, 245, 245, 0.18), rgba(245, 245, 245, 0.18)) 0 56% / 92% 2px no-repeat, - linear-gradient(rgba(245, 245, 245, 0.12), rgba(245, 245, 245, 0.12)) 0 84% / 52% 2px no-repeat; +.service-icon { + display: inline-grid; + width: 52px; + height: 52px; + place-items: center; + color: var(--md-on-primary-container); + background: var(--md-primary-container); + border-radius: var(--radius-lg); } -.service-item:nth-child(4) .service-thumb::before { - inset: 20%; - background: - linear-gradient(30deg, transparent 48%, rgba(245, 245, 245, 0.16) 49%, rgba(245, 245, 245, 0.16) 51%, transparent 52%), - linear-gradient(150deg, transparent 48%, rgba(245, 245, 245, 0.12) 49%, rgba(245, 245, 245, 0.12) 51%, transparent 52%), - radial-gradient(circle at 24% 30%, rgba(245, 245, 245, 0.34) 0 3px, transparent 4px), - radial-gradient(circle at 52% 20%, rgba(245, 245, 245, 0.28) 0 3px, transparent 4px), - radial-gradient(circle at 78% 48%, rgba(245, 245, 245, 0.28) 0 3px, transparent 4px), - radial-gradient(circle at 36% 74%, rgba(245, 245, 245, 0.22) 0 3px, transparent 4px); +.service-icon .material-symbols-rounded { + font-size: 26px; } .service-number { - margin: 0 0 var(--space-2); - color: var(--color-muted); - font-size: 0.86rem; - font-weight: 600; - letter-spacing: 0; + display: inline-block; + margin: 0; + color: var(--md-primary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; } .service-item h3 { - margin: 0 0 var(--space-2); - color: var(--color-ink); - font-size: 1.25rem; - font-weight: 500; + margin: 0; + font-size: 1.2rem; line-height: 1.35; + font-weight: 800; + letter-spacing: -0.005em; + color: var(--md-on-surface); } -.service-item p:last-child { +.service-item p { margin: 0; - color: var(--color-muted); - font-size: 0.92rem; - line-height: 1.7; -} - -.read-more { - display: flex; - justify-content: center; - margin-top: var(--space-8); + color: var(--md-on-surface-variant); + font-size: 0.97rem; + line-height: 1.75; } -/* Service detail rows */ - -.service-detail-list { - border-top: 1px solid var(--color-border); -} +/* ----------- CTA card ----------- */ -.service-detail { +.callout-band, +.contact-panel { display: grid; - grid-template-columns: 96px minmax(0, 1fr); - gap: var(--space-6); - padding: var(--space-6) 0; - border-bottom: 1px solid var(--color-border); + grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); + gap: clamp(1.5rem, 4vw, 3rem); + align-items: center; + padding: clamp(2rem, 4vw, 3rem); + background: linear-gradient(135deg, var(--md-primary), #00838a 60%, #4b5f7d); + color: var(--md-on-primary); + border: 0; + border-radius: var(--radius-2xl); + box-shadow: var(--md-elevation-3); + overflow: hidden; + position: relative; } -.detail-index { - color: var(--color-muted); - font-size: 1.05rem; +.callout-band::before, +.contact-panel::before { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(40% 60% at 100% 0%, rgba(255, 255, 255, 0.18), transparent 70%), + radial-gradient(40% 50% at 0% 100%, rgba(0, 0, 0, 0.18), transparent 70%); + pointer-events: none; } -.service-detail h2 { - margin: 0 0 var(--space-3); - font-size: 1.65rem; - line-height: 1.3; +.callout-band > *, +.contact-panel > * { + position: relative; + z-index: 1; } -.service-detail p:last-child { - max-width: 740px; - margin: 0; - color: var(--color-muted); - font-size: 1rem; - line-height: 1.85; +.callout-band h2, +.callout-band p, +.contact-panel h2, +.contact-panel p { + color: var(--md-on-primary); } -/* Company and contact */ +.callout-band .eyebrow, +.contact-panel .eyebrow { + color: var(--md-on-primary); + background: rgba(255, 255, 255, 0.18); +} -.company-list { - display: grid; - margin: 0; - border-top: 1px solid var(--color-border); +.callout-band .eyebrow .dot, +.contact-panel .eyebrow .dot { + background: var(--md-on-primary); } -.company-list > div { +.callout-band p, +.contact-panel p { + color: rgba(255, 255, 255, 0.88); + margin-top: var(--space-3); +} + +.callout-actions { + display: flex; + gap: var(--space-3); + flex-wrap: wrap; + justify-content: flex-end; +} + +@media (max-width: 768px) { + .callout-actions { + justify-content: flex-start; + } +} + +/* ----------- Page hero (sub pages) ----------- */ + +.page-hero { + position: relative; + isolation: isolate; + overflow: hidden; + padding: clamp(80px, 14vh, 144px) 0 clamp(56px, 8vh, 88px); + background: var(--md-surface); +} + +.page-hero::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + background: + radial-gradient(50% 60% at 0% 0%, rgba(0, 131, 138, 0.18), transparent 70%), + radial-gradient(40% 50% at 100% 30%, rgba(111, 247, 246, 0.36), transparent 70%); +} + +.page-hero h1 { + margin: 0; + font-size: clamp(2.2rem, 4.6vw, 3.4rem); + line-height: 1.1; + letter-spacing: -0.02em; + font-weight: 800; +} + +.page-hero p { + max-width: 720px; + margin: var(--space-5) 0 0; + color: var(--md-on-surface-variant); + font-size: 1.1rem; + line-height: 1.85; +} + +/* ----------- Service detail list ----------- */ + +.service-detail-list { display: grid; - grid-template-columns: minmax(180px, 0.34fr) minmax(0, 1fr); + gap: var(--space-4); +} + +.service-detail { + display: grid; + grid-template-columns: 96px minmax(0, 1fr); gap: var(--space-5); - padding: var(--space-5) 0; - border-bottom: 1px solid var(--color-border); + padding: var(--space-6); + background: var(--md-surface-container-lowest); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-xl); + transition: + transform var(--duration-medium) var(--ease-emphasized), + box-shadow var(--duration-medium) var(--ease-standard), + border-color var(--duration-medium) var(--ease-standard); } -.company-list dt { - color: var(--color-muted); - font-size: 0.95rem; - font-weight: 600; +.service-detail:hover { + transform: translateY(-2px); + border-color: transparent; + box-shadow: var(--md-elevation-2); } -.company-list dd { +.detail-index { + display: grid; + width: 64px; + height: 64px; + place-items: center; + color: var(--md-on-primary-container); + background: var(--md-primary-container); + border-radius: var(--radius-lg); + font-size: 1.25rem; + font-weight: 800; + letter-spacing: 0; +} + +.service-detail h2 { + margin: 0 0 var(--space-3); + font-size: 1.4rem; + line-height: 1.3; + font-weight: 800; + letter-spacing: -0.005em; +} + +.service-detail p { margin: 0; - color: var(--color-ink); - font-size: 1.05rem; + color: var(--md-on-surface-variant); + font-size: 1rem; + line-height: 1.85; } -.contact-panel { +/* ----------- Company information ----------- */ + +.company-panel { + max-width: 880px; +} + +.company-list { display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: var(--space-6); + grid-template-columns: 1fr; + gap: 1px; + margin: 0; + background: var(--md-outline-variant); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-xl); + overflow: hidden; + box-shadow: var(--md-elevation-1); +} + +.company-list > div { + display: grid; + grid-template-columns: 220px minmax(0, 1fr); + gap: var(--space-5); + padding: var(--space-5) var(--space-6); + background: var(--md-surface-container-lowest); + transition: background-color var(--duration-short) var(--ease-standard); +} + +.company-list > div:hover { + background: var(--md-surface-container-low); +} + +.company-list dt { + display: inline-flex; align-items: center; - padding: var(--space-7); - background: var(--color-panel); - border: 1px solid var(--color-border); - border-radius: var(--radius-sm); + gap: var(--space-2); + color: var(--md-on-surface-variant); + font-size: 0.92rem; + font-weight: 700; + letter-spacing: 0; } -.contact-panel h2 { - margin-bottom: var(--space-3); - font-size: 2rem; +.company-list dt .material-symbols-rounded { + font-size: 20px; + color: var(--md-primary); +} + +.company-list dd { + margin: 0; + color: var(--md-on-surface); + font-size: 1rem; + font-weight: 600; } +/* ----------- Contact ----------- */ + .contact-channels { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: var(--space-5); + gap: var(--space-4); margin-top: var(--space-7); } .contact-channel { - padding-top: var(--space-5); - border-top: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: var(--space-3); + padding: var(--space-6); + background: var(--md-surface-container-lowest); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-xl); + transition: + transform var(--duration-medium) var(--ease-emphasized), + box-shadow var(--duration-medium) var(--ease-standard), + border-color var(--duration-medium) var(--ease-standard); } -.contact-channel h3 { - margin-bottom: var(--space-3); - font-size: 1.35rem; - font-weight: 500; +.contact-channel:hover { + transform: translateY(-2px); + border-color: transparent; + box-shadow: var(--md-elevation-2); } -.contact-channel p { - margin-bottom: var(--space-4); - color: var(--color-muted); +.contact-channel .channel-icon { + display: inline-grid; + width: 48px; + height: 48px; + place-items: center; + color: var(--md-on-primary-container); + background: var(--md-primary-container); + border-radius: var(--radius-md); } -/* CTA */ - -.cta-section { - padding-top: var(--space-8); +.contact-channel h3 { + margin: 0; + color: var(--md-on-surface); + font-size: 1.1rem; + font-weight: 700; } -.callout-band { - display: grid; - min-height: 300px; - place-items: center; - padding: var(--space-8); - text-align: center; - background: var(--color-panel); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-sm); +.contact-channel p { + margin: 0; + color: var(--md-on-surface-variant); + font-size: 0.95rem; + line-height: 1.7; } -.callout-band h2 { - max-width: 720px; - margin: 0 auto var(--space-5); - font-size: 2.8rem; - line-height: 1.24; - font-weight: 500; +.contact-channel a { + display: inline-flex; + align-items: center; + gap: 0.4rem; + margin-top: auto; + font-weight: 700; } -/* Footer */ +/* ----------- Footer ----------- */ .site-footer { margin-top: var(--space-9); padding: var(--space-9) 0 var(--space-6); - color: var(--color-muted); - background: var(--color-black); + color: rgba(239, 241, 240, 0.78); + background: linear-gradient(180deg, #16201f 0%, #0e1717 100%); } .footer-grid { display: grid; - grid-template-columns: minmax(220px, 1.45fr) repeat(5, minmax(120px, 1fr)); - gap: var(--space-6); + grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr)); + gap: clamp(1.5rem, 4vw, 3.5rem); align-items: start; } +.footer-brand-block .footer-brand { + display: inline-flex; + align-items: center; + gap: var(--space-3); + color: #ffffff; + font-weight: 800; + text-decoration: none; +} + .footer-brand-block p { max-width: 360px; margin: var(--space-4) 0 0; - color: var(--color-muted); - font-size: 0.9rem; + color: rgba(239, 241, 240, 0.65); + font-size: 0.95rem; line-height: 1.75; } .footer-heading { margin: 0 0 var(--space-4); - color: var(--color-subtle); - font-size: 0.88rem; - font-weight: 600; - letter-spacing: 0; + color: #ffffff; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; } .footer-nav ul { display: grid; - gap: 10px; + gap: var(--space-3); margin: 0; padding: 0; list-style: none; } .footer-nav a { - color: var(--color-ink); - font-size: 0.92rem; + color: rgba(239, 241, 240, 0.78); + font-size: 0.95rem; font-weight: 500; + text-decoration: none; + transition: color var(--duration-short) var(--ease-standard); +} + +.footer-nav a:hover { + color: #ffffff; } .footer-meta { display: flex; justify-content: space-between; - gap: var(--space-4); - margin-top: var(--space-9); - color: var(--color-muted); - font-size: 0.84rem; + gap: var(--space-3); + margin-top: var(--space-7); + padding-top: var(--space-5); + color: rgba(239, 241, 240, 0.55); + border-top: 1px solid rgba(255, 255, 255, 0.08); + font-size: 0.85rem; } -/* Messages and forms */ +/* ----------- Alerts ----------- */ .message-container { margin-top: var(--space-5); } .alert { - color: var(--color-ink); - background: var(--color-panel); - border-color: var(--color-border); - border-radius: var(--radius-sm); + border-radius: var(--radius-lg); + border: 1px solid var(--md-outline-variant); } -.alert-error { - color: #ffd6d6; - background: #2a1111; - border-color: #633333; -} - -.form-control, -.form-select { - color: var(--color-ink); - background: var(--color-panel); - border-color: var(--color-border); +.alert-debug { + color: var(--md-on-surface); + background-color: var(--md-surface-container-low); + border-color: var(--md-outline-variant); } -.form-control:focus, -.form-select:focus { - color: var(--color-ink); - background: var(--color-panel); - border-color: #666666; - box-shadow: 0 0 0 0.2rem rgba(245, 245, 245, 0.12); +.alert-error { + color: var(--md-on-error-container); + background-color: var(--md-error-container); + border-color: var(--md-error); } -/* Reveal animation */ +/* ----------- Reveal animation ----------- */ [data-reveal] { opacity: 0; - transform: translateY(14px); + transform: translateY(16px); transition: - opacity 520ms var(--ease), - transform 520ms var(--ease); + opacity var(--duration-long) var(--ease-emphasized), + transform var(--duration-long) var(--ease-emphasized); } [data-reveal].is-visible { @@ -953,8 +1188,8 @@ h4 { *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; - scroll-behavior: auto !important; transition-duration: 0.01ms !important; + scroll-behavior: auto !important; } [data-reveal] { @@ -963,40 +1198,38 @@ h4 { } } -/* Responsive */ +/* ----------- Responsive breakpoints ----------- */ -@media (max-width: 1099.98px) { - .footer-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); +@media (max-width: 991.98px) { + .hero-grid { + grid-template-columns: 1fr; + gap: var(--space-7); } - .footer-brand-block { - grid-column: 1 / -1; - } -} - -@media (max-width: 991.98px) { - .hero-section h1, - .page-hero h1 { - font-size: 3.3rem; + .hero-visual { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .editorial-hero { - min-height: 560px; + .split-layout, + .callout-band, + .contact-panel { + grid-template-columns: 1fr; } .service-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} + .footer-grid { + grid-template-columns: 1fr 1fr; + } -@media (max-width: 767.98px) { - .container { - padding-right: 20px; - padding-left: 20px; + .footer-brand-block { + grid-column: 1 / -1; } +} +@media (max-width: 767.98px) { .navbar { min-height: 64px; } @@ -1004,109 +1237,546 @@ h4 { .navbar-collapse { margin-top: var(--space-3); padding: var(--space-4); - background: var(--color-panel-soft); - border: 1px solid var(--color-border-soft); - border-radius: var(--radius-md); + background: var(--md-surface-container-low); + border-radius: var(--radius-xl); + box-shadow: var(--md-elevation-1); } .navbar-nav { + gap: var(--space-2); align-items: stretch !important; } - .nav-link, - .nav-cta { + .nav-link { width: 100%; - justify-content: flex-start; + padding: 0.75rem 1rem !important; } .nav-cta { - margin: var(--space-2) 0 0; + margin-left: 0; + margin-top: var(--space-2); } - .section { - padding: var(--space-8) 0; + .hero-content h1 { + font-size: clamp(2rem, 9vw, 2.8rem); } - .hero-section, - .page-hero { - padding: 88px 0 var(--space-7); + .hero-meta { + gap: var(--space-4); } - .editorial-hero { - min-height: auto; - padding-top: 96px; - padding-bottom: var(--space-8); + .service-grid { + grid-template-columns: 1fr; } - .hero-section h1, - .page-hero h1 { - font-size: 2.55rem; + .service-item { + min-height: auto; } - .hero-lead, - .page-hero p { - font-size: 1.02rem; + .service-detail { + grid-template-columns: 56px minmax(0, 1fr); + gap: var(--space-4); + padding: var(--space-5); } - .abstract-field { - min-height: 320px; + .detail-index { + width: 48px; + height: 48px; + font-size: 1rem; } - .statement-section, - .info-section { - padding-top: var(--space-8); - padding-bottom: var(--space-8); + .company-list > div { + grid-template-columns: 1fr; + gap: var(--space-1); + padding: var(--space-4) var(--space-5); } - .story-block h2, - .section-text h2, - .statement-section h2, - .center-copy h2, - .contact-panel h2 { - font-size: 1.8rem; + .contact-channels { + grid-template-columns: 1fr; } - .section-heading { - display: block; + .footer-grid { + grid-template-columns: 1fr; } - .section-tabs { - margin-top: var(--space-4); + .footer-meta { + flex-direction: column; + align-items: flex-start; } +} - .service-grid, - .contact-channels { - grid-template-columns: 1fr; - } +/* ---------------------------------------------------------------- + Additions: language switcher, product card, approach, roadmap, + policy grid, active grid, charter list, narrow-prose, contact-note + ---------------------------------------------------------------- */ - .service-thumb { - min-height: 160px; - } +.nav-lang-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.9rem; + letter-spacing: 0.01em; + opacity: 0.85; +} - .service-detail { - grid-template-columns: 1fr; - gap: var(--space-3); - } +.nav-lang-link:hover, +.nav-lang-link:focus-visible { + opacity: 1; +} - .company-list > div, - .contact-panel { - grid-template-columns: 1fr; - } +.nav-lang-link .material-symbols-rounded { + font-size: 1rem; +} - .callout-band { - min-height: 240px; - padding: var(--space-6); - } +.service-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + margin-top: var(--space-3); + color: var(--md-primary); + font-weight: 600; + text-decoration: none; + border-bottom: 1px solid transparent; + transition: border-color 180ms ease; +} - .callout-band h2 { - font-size: 2rem; - } +.service-link:hover, +.service-link:focus-visible { + border-bottom-color: currentColor; +} - .footer-grid { +.service-link .material-symbols-rounded { + font-size: 1.05rem; +} + +/* Narrow prose for vision/legal/holdings */ +.narrow-prose { + max-width: 760px; +} + +.prose + .prose { + margin-top: var(--space-7); +} + +.prose h2 { + font-family: var(--font-display); + font-size: clamp(1.4rem, 2.6vw, 1.85rem); + font-weight: 700; + margin-bottom: var(--space-4); + letter-spacing: -0.01em; +} + +.prose h3 { + font-family: var(--font-display); + font-size: 1.1rem; + font-weight: 700; + margin: var(--space-5) 0 var(--space-2); +} + +.prose p, +.prose li { + font-size: 1rem; + line-height: 1.85; + color: var(--md-on-surface-variant); +} + +.prose p + p { + margin-top: var(--space-3); +} + +.prose ul { + padding-left: 1.2rem; + margin: var(--space-2) 0; +} + +.prose a { + color: var(--md-primary); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; +} + +.prose a:hover, +.prose a:focus-visible { + text-decoration-thickness: 2px; +} + +/* Software product card */ +.product-card { + background: var(--md-surface-container-low); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-xl); + overflow: hidden; + box-shadow: 0 12px 36px rgba(0, 60, 64, 0.08); +} + +.product-card-body { + padding: clamp(1.5rem, 4vw, 3rem); + display: grid; + gap: var(--space-4); +} + +.product-tagline { + display: inline-flex; + align-self: start; + align-items: center; + gap: 0.4rem; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--md-primary); + background: var(--md-primary-container); + padding: 0.45rem 0.8rem; + border-radius: 999px; +} + +.product-card-body h3 { + font-family: var(--font-display); + font-size: clamp(1.4rem, 3vw, 1.9rem); + font-weight: 700; + letter-spacing: -0.01em; + margin: 0; +} + +.product-feature-list { + display: grid; + gap: var(--space-2); + margin: var(--space-3) 0 0; + padding: 0; + list-style: none; +} + +.product-feature-list li { + display: grid; + grid-template-columns: 28px minmax(0, 1fr); + align-items: start; + gap: var(--space-2); + font-size: 0.97rem; + line-height: 1.6; + color: var(--md-on-surface-variant); +} + +.product-feature-list .material-symbols-rounded { + color: var(--md-primary); + font-size: 1.2rem; + margin-top: 2px; +} + +.product-meta { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-3); + margin-top: var(--space-3); + padding-top: var(--space-4); + border-top: 1px solid var(--md-outline-variant); +} + +.product-meta dt { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--md-on-surface-variant); + margin-bottom: 0.3rem; +} + +.product-meta dd { + margin: 0; + font-size: 0.95rem; + font-weight: 600; + color: var(--md-on-surface); +} + +.product-actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + margin-top: var(--space-3); +} + +.btn.disabled { + opacity: 0.55; + pointer-events: none; +} + +/* Approach grid */ +.approach-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: var(--space-4); +} + +.approach-item { + background: var(--md-surface-container-lowest); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-lg); + padding: var(--space-5); + display: grid; + gap: var(--space-2); +} + +.approach-num { + font-family: var(--font-display); + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.18em; + color: var(--md-primary); +} + +.approach-item h3 { + font-family: var(--font-display); + font-size: 1.1rem; + font-weight: 700; + margin: 0; +} + +.approach-item p { + font-size: 0.95rem; + line-height: 1.7; + color: var(--md-on-surface-variant); + margin: 0; +} + +/* Roadmap list */ +.roadmap-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: var(--space-3); +} + +.roadmap-list li { + display: grid; + grid-template-columns: minmax(110px, max-content) minmax(0, 1fr); + align-items: start; + gap: var(--space-4); + padding: var(--space-4) var(--space-5); + background: var(--md-surface-container-lowest); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-lg); +} + +.roadmap-list p { + margin: 0; + font-size: 0.97rem; + line-height: 1.65; + color: var(--md-on-surface); +} + +.roadmap-tag { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--md-primary); + background: var(--md-primary-container); + padding: 0.35rem 0.7rem; + border-radius: 999px; + text-align: center; + align-self: start; +} + +/* Holdings policy grid */ +.policy-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--space-4); +} + +.policy-item { + background: var(--md-surface-container-lowest); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-lg); + padding: var(--space-5); + display: grid; + gap: var(--space-2); +} + +.policy-num { + font-family: var(--font-display); + font-size: 0.85rem; + font-weight: 700; + letter-spacing: 0.18em; + color: var(--md-primary); +} + +.policy-item h3 { + font-family: var(--font-display); + font-size: 1.1rem; + font-weight: 700; + margin: 0; +} + +.policy-item p { + font-size: 0.95rem; + line-height: 1.7; + color: var(--md-on-surface-variant); + margin: 0; +} + +/* Business page: active items and charter list */ +.active-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--space-4); +} + +.active-item { + background: var(--md-surface); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-lg); + padding: var(--space-5); + display: grid; + gap: var(--space-2); + position: relative; +} + +.active-icon { + display: inline-grid; + place-items: center; + width: 48px; + height: 48px; + border-radius: var(--radius-md); + background: var(--md-primary-container); + color: var(--md-primary); +} + +.active-icon .material-symbols-rounded { + font-size: 1.6rem; +} + +.active-item h3 { + font-family: var(--font-display); + font-size: 1.15rem; + font-weight: 700; + margin: var(--space-1) 0 0; +} + +.active-item p { + font-size: 0.95rem; + line-height: 1.65; + color: var(--md-on-surface-variant); + margin: 0; +} + +.active-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + margin-top: var(--space-2); + color: var(--md-primary); + font-weight: 600; + text-decoration: none; +} + +.active-link:hover, +.active-link:focus-visible { + text-decoration: underline; + text-underline-offset: 3px; +} + +.charter-list { + display: grid; + gap: var(--space-4); +} + +.charter-group { + background: var(--md-surface-container-lowest); + border: 1px solid var(--md-outline-variant); + border-radius: var(--radius-lg); + padding: var(--space-5); +} + +.charter-group.is-active { + border-color: var(--md-primary); + box-shadow: 0 0 0 1px var(--md-primary) inset; + background: var(--md-surface-container-low); +} + +.charter-group header { + display: flex; + align-items: center; + gap: var(--space-3); + margin-bottom: var(--space-3); +} + +.charter-letter { + display: inline-grid; + place-items: center; + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--md-primary-container); + color: var(--md-primary); + font-family: var(--font-display); + font-weight: 800; + font-size: 0.95rem; +} + +.charter-group header h3 { + font-family: var(--font-display); + font-size: 1.05rem; + font-weight: 700; + margin: 0; + flex: 1; +} + +.charter-badge { + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: #ffffff; + background: var(--md-primary); + padding: 0.3rem 0.65rem; + border-radius: 999px; +} + +.charter-group ul { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: var(--space-2); +} + +.charter-group li { + font-size: 0.95rem; + line-height: 1.7; + color: var(--md-on-surface-variant); + padding-left: 1.1rem; + position: relative; +} + +.charter-group li::before { + content: "·"; + position: absolute; + left: 0; + top: 0; + color: var(--md-primary); + font-weight: 700; +} + +.contact-note { + margin-top: var(--space-5); + padding: var(--space-4) var(--space-5); + background: var(--md-surface-container-lowest); + border: 1px dashed var(--md-outline-variant); + border-radius: var(--radius-md); + font-size: 0.9rem; + color: var(--md-on-surface-variant); + line-height: 1.7; +} + +@media (max-width: 720px) { + .roadmap-list li { grid-template-columns: 1fr; + gap: var(--space-2); } - .footer-meta { - flex-direction: column; + .charter-group header { + flex-wrap: wrap; } } diff --git a/ether/templates/base.html b/ether/templates/base.html index 78f801e..05d5fe1 100644 --- a/ether/templates/base.html +++ b/ether/templates/base.html @@ -1,22 +1,68 @@ -{% load static i18n %} +{% load static %} -{% get_current_language as LANGUAGE_CODE %} - + {% block title %} - Ether合同会社 + {% if lang == 'en' %} + Ether LLC + {% else %} + Ether合同会社 + {% endif %} {% endblock title %} + content="{% block meta_description %}{% if lang == 'en' %}Ether LLC is a Tokyo-based Web3 holding company centered on Ethereum, building software products and supporting culture-meets-technology projects.{% else %}Ether合同会社は、東京を拠点に、Ethereumを基軸資産としてソフトウェアと文化事業を進めるWeb3ホールディングです。{% endif %}{% endblock meta_description %}" /> - + + {% if alt_lang_url %} + + {% endif %} + + + + + + + + {% block css %} @@ -40,15 +86,28 @@ {% block body %} - +