diff --git a/SVSim.EmulatedEntrypoint/Services/RankingPeriodSchedule.cs b/SVSim.EmulatedEntrypoint/Services/RankingPeriodSchedule.cs
new file mode 100644
index 0000000..18e5980
--- /dev/null
+++ b/SVSim.EmulatedEntrypoint/Services/RankingPeriodSchedule.cs
@@ -0,0 +1,95 @@
+using System.Globalization;
+
+namespace SVSim.EmulatedEntrypoint.Services;
+
+///
+/// Pure deterministic monthly period schedule for /ranking/*. Each ranking family
+/// (RankMatch, MasterPoint, TwoPick, Sealed) launched in a different month on the
+/// live server; id=1 in each family is its launch month. id=current means "this
+/// month in JST." The generator returns descending-by-id (newest first).
+///
+/// Anchor dates derived from prod capture 2026-06-09 17:00 UTC:
+/// RankMatch current id = 122 → launch month = 2026-06 minus 121 months = 2016-05
+/// MasterPoint current id = 120 → 2016-07
+/// TwoPick current id = 119 → 2016-08
+/// Sealed current id = 62 → 2021-05
+///
+/// See docs/superpowers/specs/2026-06-10-ranking-stubs-design.md for rationale.
+///
+public static class RankingPeriodSchedule
+{
+ public enum Family { RankMatch, MasterPoint, TwoPick, Sealed }
+
+ // (Year, Month) of each family's id=1 month, JST.
+ private static readonly Dictionary FamilyAnchors = new()
+ {
+ [Family.RankMatch] = (2016, 5),
+ [Family.MasterPoint] = (2016, 7),
+ [Family.TwoPick] = (2016, 8),
+ [Family.Sealed] = (2021, 5),
+ };
+
+ private static readonly TimeZoneInfo Jst = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
+
+ public static IReadOnlyList GenerateFor(Family family, DateTime nowUtc)
+ {
+ var nowJst = TimeZoneInfo.ConvertTimeFromUtc(nowUtc, Jst);
+ var anchor = FamilyAnchors[family];
+ int currentId = MonthsBetweenInclusive(anchor.Year, anchor.Month, nowJst.Year, nowJst.Month);
+ if (currentId < 1) return Array.Empty();
+
+ var result = new List(currentId);
+ for (int id = currentId; id >= 1; id--)
+ {
+ result.Add(BuildEntry(family, id, anchor));
+ }
+ return result;
+ }
+
+ public static PeriodEntry? TryFindById(Family family, int periodId, DateTime nowUtc)
+ {
+ if (periodId < 1) return null;
+ var nowJst = TimeZoneInfo.ConvertTimeFromUtc(nowUtc, Jst);
+ var anchor = FamilyAnchors[family];
+ int currentId = MonthsBetweenInclusive(anchor.Year, anchor.Month, nowJst.Year, nowJst.Month);
+ if (periodId > currentId) return null;
+ return BuildEntry(family, periodId, anchor);
+ }
+
+ private static PeriodEntry BuildEntry(Family family, int id, (int Year, int Month) anchor)
+ {
+ // id=1 is the anchor month; id=N is anchor month + (N-1) months.
+ int totalMonths = (anchor.Year * 12 + (anchor.Month - 1)) + (id - 1);
+ int year = totalMonths / 12;
+ int month = (totalMonths % 12) + 1;
+
+ var begin = new DateTime(year, month, 1, 2, 0, 0);
+ // End = first day of next month at 02:00:00 minus 1 second = "YYYY-MM+1-01 01:59:59"
+ var end = begin.AddMonths(1).AddSeconds(-1);
+
+ int periodNum = family switch
+ {
+ // Captured offsets: RankMatch period_num = id - 1; MasterPoint period_num = id - 1.
+ // (Capture frame 64 shows rank_match[0] = { id:122, period_num:121 } and
+ // master_point[0] = { id:120, period_num:119 }, two_pick[0] = { id:119, period_num:119 }.)
+ Family.RankMatch => id - 1,
+ Family.MasterPoint => id - 1,
+ _ => id,
+ };
+
+ return new PeriodEntry(
+ Id: id.ToString(CultureInfo.InvariantCulture),
+ PeriodNum: periodNum.ToString(CultureInfo.InvariantCulture),
+ BeginTime: begin.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
+ EndTime: end.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
+ }
+
+ private static int MonthsBetweenInclusive(int startYear, int startMonth, int endYear, int endMonth)
+ => (endYear - startYear) * 12 + (endMonth - startMonth) + 1;
+}
+
+public sealed record PeriodEntry(
+ string Id,
+ string PeriodNum,
+ string BeginTime,
+ string EndTime);
diff --git a/SVSim.UnitTests/Services/RankingPeriodScheduleTests.cs b/SVSim.UnitTests/Services/RankingPeriodScheduleTests.cs
new file mode 100644
index 0000000..fb60a6d
--- /dev/null
+++ b/SVSim.UnitTests/Services/RankingPeriodScheduleTests.cs
@@ -0,0 +1,79 @@
+using SVSim.EmulatedEntrypoint.Services;
+
+namespace SVSim.UnitTests.Services;
+
+public class RankingPeriodScheduleTests
+{
+ // 2026-06-09 17:00 UTC = 2026-06-10 02:00 JST — clearly in JST's June 2026 month.
+ private static readonly DateTime CaptureNowUtc =
+ new(2026, 6, 9, 17, 0, 0, DateTimeKind.Utc);
+
+ [Test]
+ public void RankMatch_at_capture_time_has_current_period_id_122()
+ {
+ var list = RankingPeriodSchedule.GenerateFor(
+ RankingPeriodSchedule.Family.RankMatch, CaptureNowUtc);
+
+ Assert.That(list, Is.Not.Empty);
+ Assert.That(list[0].Id, Is.EqualTo("122"));
+ Assert.That(list[0].BeginTime, Is.EqualTo("2026-06-01 02:00:00"));
+ Assert.That(list[0].EndTime, Is.EqualTo("2026-07-01 01:59:59"));
+ Assert.That(list.Count, Is.EqualTo(122));
+ }
+
+ [Test]
+ public void MasterPoint_at_capture_time_has_current_period_id_120()
+ {
+ var list = RankingPeriodSchedule.GenerateFor(
+ RankingPeriodSchedule.Family.MasterPoint, CaptureNowUtc);
+ Assert.That(list[0].Id, Is.EqualTo("120"));
+ Assert.That(list.Count, Is.EqualTo(120));
+ }
+
+ [Test]
+ public void TwoPick_at_capture_time_has_current_period_id_119()
+ {
+ var list = RankingPeriodSchedule.GenerateFor(
+ RankingPeriodSchedule.Family.TwoPick, CaptureNowUtc);
+ Assert.That(list[0].Id, Is.EqualTo("119"));
+ Assert.That(list.Count, Is.EqualTo(119));
+ }
+
+ [Test]
+ public void Sealed_at_capture_time_has_current_period_id_62()
+ {
+ var list = RankingPeriodSchedule.GenerateFor(
+ RankingPeriodSchedule.Family.Sealed, CaptureNowUtc);
+ Assert.That(list[0].Id, Is.EqualTo("62"));
+ Assert.That(list.Count, Is.EqualTo(62));
+ }
+
+ [Test]
+ public void Schedule_is_descending_by_id()
+ {
+ var list = RankingPeriodSchedule.GenerateFor(
+ RankingPeriodSchedule.Family.RankMatch, CaptureNowUtc);
+ for (int i = 1; i < list.Count; i++)
+ {
+ Assert.That(int.Parse(list[i - 1].Id), Is.GreaterThan(int.Parse(list[i].Id)),
+ $"position {i - 1} ({list[i - 1].Id}) should be > position {i} ({list[i].Id})");
+ }
+ }
+
+ [Test]
+ public void TryFindById_returns_entry_when_id_in_range()
+ {
+ var entry = RankingPeriodSchedule.TryFindById(
+ RankingPeriodSchedule.Family.RankMatch, periodId: 122, CaptureNowUtc);
+ Assert.That(entry, Is.Not.Null);
+ Assert.That(entry!.BeginTime, Is.EqualTo("2026-06-01 02:00:00"));
+ }
+
+ [Test]
+ public void TryFindById_returns_null_for_unknown_id()
+ {
+ var entry = RankingPeriodSchedule.TryFindById(
+ RankingPeriodSchedule.Family.RankMatch, periodId: 9999, CaptureNowUtc);
+ Assert.That(entry, Is.Null);
+ }
+}