intranet-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +38 -0
  3. data/lib/core_extensions.rb +12 -0
  4. data/lib/core_extensions/string.rb +43 -0
  5. data/lib/core_extensions/tree.rb +84 -0
  6. data/lib/core_extensions/webrick/httpresponse.rb +22 -0
  7. data/lib/intranet/abstract_responder.rb +34 -0
  8. data/lib/intranet/core.rb +125 -0
  9. data/lib/intranet/core/builder.rb +98 -0
  10. data/lib/intranet/core/haml_wrapper.rb +60 -0
  11. data/lib/intranet/core/locales.rb +47 -0
  12. data/lib/intranet/core/servlet.rb +42 -0
  13. data/lib/intranet/core/version.rb +8 -0
  14. data/lib/intranet/logger.rb +38 -0
  15. data/lib/intranet/resources/haml/http_error.haml +27 -0
  16. data/lib/intranet/resources/haml/skeleton.haml +52 -0
  17. data/lib/intranet/resources/haml/title_and_breadcrumb.haml +8 -0
  18. data/lib/intranet/resources/locales/en.yml +46 -0
  19. data/lib/intranet/resources/locales/fr.yml +46 -0
  20. data/lib/intranet/resources/www/background.jpg +0 -0
  21. data/lib/intranet/resources/www/error.png +0 -0
  22. data/lib/intranet/resources/www/favicon.ico +0 -0
  23. data/lib/intranet/resources/www/fonts/SourceSansPro.woff2 +0 -0
  24. data/lib/intranet/resources/www/nav.js +25 -0
  25. data/lib/intranet/resources/www/style.css +306 -0
  26. data/spec/core_extensions/string_spec.rb +135 -0
  27. data/spec/core_extensions/tree_spec.rb +208 -0
  28. data/spec/core_extensions/webrick/httpresponse_spec.rb +43 -0
  29. data/spec/intranet/core/fr.yml +5 -0
  30. data/spec/intranet/core/haml_wrapper_spec.rb +70 -0
  31. data/spec/intranet/core/locales_spec.rb +74 -0
  32. data/spec/intranet/core_spec.rb +403 -0
  33. data/spec/intranet/logger_spec.rb +129 -0
  34. data/spec/spec_helper.rb +50 -0
  35. data/spec/test_responder/responder.rb +42 -0
  36. data/spec/test_responder/www/style.css +5 -0
  37. metadata +218 -0
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'haml'
4
+
5
+ module Intranet
6
+ class Core
7
+ # Wrapps the Haml engine from the Haml gem.
8
+ # This module has to be included in any class wanting to generate a HTML/XML
9
+ # from HAML templates.
10
+ module HamlWrapper
11
+ # @!visibility protected
12
+ # The default path to the HAML files.
13
+ DEFAULT_HAML_DIR = File.join(__dir__, '..', 'resources', 'haml')
14
+
15
+ class << self
16
+ # @!visibility protected
17
+ # Haml load path (not to be modified directly)
18
+ attr_accessor :load_path
19
+ end
20
+
21
+ # @!visibility protected
22
+ # Initializes HAML templates load path.
23
+ # @param default_haml_dir [String] The path to the directory containing default HAML
24
+ # templates.
25
+ def self.initialize(default_haml_dir = DEFAULT_HAML_DIR)
26
+ self.load_path = [default_haml_dir]
27
+ end
28
+
29
+ # @!visibility protected
30
+ # Adds a HAML templates directory.
31
+ # @param dir [String] The path to the directory to add.
32
+ def self.add_path(dir)
33
+ HamlWrapper.load_path.unshift(dir) # prepend load_path
34
+ end
35
+
36
+ # Generate the HTML/XML from a given HAML template.
37
+ # @param template [String] The name of the HAML template to use.
38
+ # @param content [Hash] The additional content required to generate the HTML/XML.
39
+ # @return [String] The HTML/XML content.
40
+ def to_markup(template, content = {})
41
+ fd = File.read(find_template(template))
42
+ Haml::Engine.new(fd, escape_html: false).render(self, content)
43
+ rescue Errno::ENOENT # template file does not exists
44
+ ''
45
+ end
46
+
47
+ private
48
+
49
+ # Look for an HAML template in the registered paths.
50
+ # @return [String] The path to the template if it exists, an empty string otherwise.
51
+ def find_template(template)
52
+ HamlWrapper.load_path.each do |dir|
53
+ candidate = File.join(dir, template + '.haml')
54
+ return candidate if File.exist?(candidate)
55
+ end
56
+ ''
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n'
4
+
5
+ module Intranet
6
+ class Core
7
+ # @!visibility protected
8
+ # Wrapps the initialisation of the i18n internationalization gem.
9
+ module Locales
10
+ # The default path to the locales files.
11
+ DEFAULT_LOCALES_DIR = File.join(__dir__, '..', 'resources', 'locales')
12
+
13
+ # Initializes translation units.
14
+ # @param default_locales_dir [String] The path to the directory containing default translation
15
+ # units.
16
+ def self.initialize(default_locales_dir = DEFAULT_LOCALES_DIR)
17
+ I18n.load_path = translation_files(default_locales_dir)
18
+ I18n.available_locales = available_locales(default_locales_dir)
19
+ begin
20
+ I18n.default_locale = ENV['LANG'].split('.')[0].split('_')[0].to_sym
21
+ rescue NoMethodError, I18n::InvalidLocale
22
+ I18n.default_locale = :en
23
+ end
24
+ end
25
+
26
+ # Adds a translation units directory.
27
+ # @param dir [String] The path to the directory to add.
28
+ def self.add_path(dir)
29
+ I18n.load_path = Dir.glob(File.join(dir, '*.yml')) + I18n.load_path # prepend load_path
30
+ end
31
+
32
+ # private
33
+
34
+ def self.translation_files(dir)
35
+ Dir.glob(File.join(dir, '*.yml'))
36
+ end
37
+ private_class_method :translation_files
38
+
39
+ def self.available_locales(dir)
40
+ translation_files(dir).map do |file|
41
+ File.basename(file, File.extname(file)).to_sym
42
+ end
43
+ end
44
+ private_class_method :available_locales
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'webrick'
5
+ require 'webrick/httpstatus'
6
+
7
+ module Intranet
8
+ class Core
9
+ # @!visibility protected
10
+ # WEBrick servlet for the Intranet.
11
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
12
+ # Initializes a new Servlet instance.
13
+ # @param server [WEBrick::HTTPServer] The web server.
14
+ # @param builder [Intranet::Core::Builder] The web pages builder.
15
+ def initialize(server, builder)
16
+ super(server)
17
+ @builder = builder
18
+ end
19
+
20
+ # Processes a GET request issued to the web server.
21
+ # @param request [WEBrick::HTTPRequest] The GET request.
22
+ # @param response [WEBrick::HTTPResponse] Fields +status+, +content_type+ and +body+ are
23
+ # filled with the server response.
24
+ def do_GET(request, response) # rubocop:disable Naming/MethodName
25
+ # See https://github.com/nahi/webrick/blob/master/lib/webrick/httprequest.rb
26
+ # We could use request.request_uri (which is a URI object) but its path is not normalized
27
+ # (it may contain '../' or event start with '..'). Hence we use request.past which has been
28
+ # normalized with HTTPUtils::normalize_path, and request.query for the URL parameters.
29
+ path = request.path
30
+ path += 'index.html' if path[-1] == '/' # path is normalized (ie. does not contain '../')
31
+
32
+ status, content_type, body = @builder.do_get(path, request.query)
33
+
34
+ raise WEBrick::HTTPStatus[status] if WEBrick::HTTPStatus.error?(status)
35
+
36
+ response.status = status
37
+ response.content_type = content_type
38
+ response.body = body
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Intranet
4
+ class Core
5
+ # The version of the Intranet core, according to semantic versionning.
6
+ VERSION = '1.0.0'
7
+ end
8
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'webrick'
4
+
5
+ module Intranet
6
+ # The default logger for the Intranet. It is based on +WEBrick::BasicLog+ but adds timestamp and
7
+ # colors to the messages.
8
+ class Logger < WEBrick::BasicLog
9
+ COLOR = {
10
+ FATAL => "\033[1;37;41m",
11
+ ERROR => "\033[0;31m",
12
+ WARN => "\033[0;33m",
13
+ INFO => "\033[0;36m",
14
+ DEBUG => "\033[0;35m"
15
+ }.freeze
16
+ private_constant :COLOR
17
+
18
+ # Initializes a new logger for $stderr that outputs messages at +level+ or higher.
19
+ # +level+ can be modified later (attribute accessor).
20
+ # @param level The initial log level.
21
+ def initialize(level = INFO)
22
+ super($stderr, level)
23
+ end
24
+
25
+ # Logs a message at a given level if it is above the current log level.
26
+ # @param level The level of the message
27
+ # @param msg [String] The message
28
+ def log(level, msg)
29
+ super(level, COLOR[level] + Time.now.strftime('[%Y-%m-%d %H:%M:%S] ') + msg + "\033[0m")
30
+ end
31
+
32
+ # Logs an object that responds to +to_s+ at level INFO.
33
+ # @param obj [Object] The object to log.
34
+ def <<(obj)
35
+ info(obj.to_s.chomp)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ !!! 5
2
+ %html
3
+ %head
4
+ %title
5
+ = HTMLEntities.new.encode(I18n.t('nav.error.http'), :named) + ' ' + error.to_s + ' | '
6
+ = HTMLEntities.new.encode(I18n.t('nav.error.http_' + error.to_s + '.short'), :named)
7
+ %meta{charset: 'utf-8'}
8
+ %link{rel: 'icon', type: 'image/x-icon', href: '/design/favicon.ico'}
9
+ %link{rel: 'stylesheet', type: 'text/css', href: '/design/style.css'}
10
+ %body
11
+ %header
12
+ %h1
13
+ %a{href: '/index.html'}= Socket.gethostname.capitalize
14
+ %main
15
+ %section#error
16
+ %h2
17
+ = HTMLEntities.new.encode(I18n.t('nav.error.http'), :named) + ' ' + error.to_s
18
+ %br
19
+ = HTMLEntities.new.encode(I18n.t('nav.error.http_' + error.to_s + '.short'), :named)
20
+ %p
21
+ %img{src: '/design/error.png', alt: ''}
22
+ %br
23
+ = HTMLEntities.new.encode(I18n.t('nav.error.http_' + error.to_s + '.full'), :named)
24
+ %br
25
+ %br
26
+ %a{href: '/index.html'}= HTMLEntities.new.encode(I18n.t('nav.back.home'), :named)
27
+ %footer
@@ -0,0 +1,52 @@
1
+ !!! 5
2
+ %html
3
+ %head
4
+ %title= Socket.gethostname.capitalize + ' | ' + body[:title]
5
+ %meta{charset: 'utf-8'}
6
+ %link{rel: 'icon', type: 'image/x-icon', href: '/design/favicon.ico'}
7
+ - css.each do |url|
8
+ %link{rel: 'stylesheet', type: 'text/css', href: url}
9
+ - js.each do |url|
10
+ %script{src: url, defer: 'defer'}
11
+ %body
12
+ %header
13
+ %a{id: 'openmenu', onclick: 'openNavMenu();'}= '&#9776;'
14
+ %h1
15
+ %a{href: '/index.html', title: I18n.t('nav.back.home')}= Socket.gethostname.capitalize
16
+ %nav
17
+ %a{id: 'closemenu', onclick: 'closeNavMenu();'}= '&times;'
18
+ %ul
19
+ - responders.children_nodes.each do |child_id, child_node|
20
+ %li
21
+ - if child_node.children? # Create a dropdown menu entry
22
+ %a= I18n.t(child_id + '.menu')
23
+ %ul
24
+ - child_node.children_nodes.each do |child_url, node|
25
+ %li
26
+ %a{href: '/' + child_id + '/' + child_url + '/index.html'}
27
+ = I18n.t(child_id + '.' + child_url + '.menu')
28
+ - else # Create a regular menu entry
29
+ %a{href: '/' + child_id + '/index.html'}= I18n.t(child_id + '.menu')
30
+
31
+ - if is_home
32
+ %aside
33
+
34
+ %main
35
+ = body[:content]
36
+
37
+ %footer
38
+ %p
39
+ = I18n.t('nav.generated.page') + ' ' + I18n.t('nav.generated.on_host') + ' '
40
+ %em= Socket.gethostname
41
+ = ' ' + I18n.t('nav.generated.on_date') + ' ' + Time.now.strftime(I18n.t('date_format'))
42
+ = ' ' + I18n.t('date_time_separator') + Time.now.strftime(I18n.t('time_format'))
43
+ %br
44
+ %a{onclick: 'openModal();'}= I18n.t('nav.about')
45
+ %aside#modal
46
+ %div#modal-content
47
+ %h2= 'Intranet'
48
+ %h3
49
+ = 'core' + '&nbsp;&nbsp;'
50
+ %em= Intranet::Core::VERSION
51
+ %p
52
+ %a{href: 'https://bitbucket.org/linuxtools/intranet.git', target: '_blank'}= 'https://bitbucket.org/linuxtools/intranet.git'
@@ -0,0 +1,8 @@
1
+ %h2= title
2
+ %ul.breadcrumb
3
+ - nav.each do |name, url|
4
+ - if url.nil?
5
+ %li= name
6
+ - else
7
+ %li
8
+ %a{ href: url }= name
@@ -0,0 +1,46 @@
1
+ # en.yml - Translation file for the IntraNet in en-GB/en-US
2
+ ---
3
+
4
+ en:
5
+ # Date and time format, as inputs to strftime
6
+ date_format: '%Y-%m-%d'
7
+ time_format: '%H:%M'
8
+ date_time_separator: ''
9
+
10
+ # Translations for navigation elements
11
+ nav:
12
+ home: 'Home'
13
+ about: 'About'
14
+ back:
15
+ home: 'Back to the homepage'
16
+ generated:
17
+ page: 'Page generated'
18
+ on_host: 'on'
19
+ on_date: 'on'
20
+ error:
21
+ http: 'Error'
22
+ http_400: {short: 'Bad Request', full: 'The request could not be understood by the server.'}
23
+ http_401: {short: 'Unauthorized', full: 'The requested page requires user authentication.'}
24
+ http_402: {short: 'Payment Required', full: 'The requested page requires payment.'}
25
+ http_403: {short: 'Forbidden', full: 'Access to the requested page is forbidden.'}
26
+ http_404: {short: 'Not Found', full: 'The requested page cannot be found.'}
27
+ http_405: {short: 'Method Not Allowed', full: 'The method specified in the request is not allowed for this resource.'}
28
+ http_406: {short: 'Not Acceptable', full: 'The request could not be accepted by the server.'}
29
+ http_407: {short: 'Proxy Authentication Required', full: 'The requested page requires user authentication with the proxy.'}
30
+ http_408: {short: 'Request Timeout', full: 'The server did not receive the request in the expected delay.'}
31
+ http_409: {short: 'Conflict', full: 'The request could not be completed due to a conflict with the current state of the resource.'}
32
+ http_410: {short: 'Gone', full: 'The requested resource is no longer available at the server and no forwarding address is known.'}
33
+ http_411: {short: 'Length Required', full: 'The request could not be understood by the server.'}
34
+ http_412: {short: 'Precondition Failed', full: 'The request could not be completed by the server.'}
35
+ http_413: {short: 'Request Entity Too Large', full: 'The request could not be completed by the server.'}
36
+ http_414: {short: 'Request-URI Too Long', full: 'The request could not be completed by the server.'}
37
+ http_415: {short: 'Unsupported Media Type', full: 'The request could not be completed by the server.'}
38
+ http_416: {short: 'Requested Range Not Satisfiable', full: 'The request could not be completed by the server.'}
39
+ http_417: {short: 'Expectation Failed', full: 'The request could not be completed by the server.'}
40
+ http_500: {short: 'Internal Server Error', full: 'The server encountered an unexpected error and is unable to fulfill the request.'}
41
+ http_501: {short: 'Not Implemented', full: 'The server does not support the functionality required to fulfill the request.'}
42
+ http_502: {short: 'Bad Gateway', full: 'The server received an invalid response from the upstream server.'}
43
+ http_503: {short: 'Service Unavailable', full: 'The server is temporary unable to handle the request. Please try again later.'}
44
+ http_504: {short: 'Gateway Timeout', full: 'The server did not receive a response from the upstream server in the expected delay.'}
45
+ http_505: {short: 'HTTP Version Not Supported', full: 'The server does not support the HTTP protocol version that was used in the request message.'}
46
+ load: 'Load error!'
@@ -0,0 +1,46 @@
1
+ # fr.yml - Translation file for the IntraNet in fr-FR
2
+ ---
3
+
4
+ fr:
5
+ # Date and time format, as inputs to strftime
6
+ date_format: '%d/%m/%Y'
7
+ time_format: '%Hh%M'
8
+ date_time_separator: 'à '
9
+
10
+ # Translations for navigation elements
11
+ nav:
12
+ home: 'Accueil'
13
+ about: 'À propos'
14
+ back:
15
+ home: 'Retour à l''accueil'
16
+ generated:
17
+ page: 'Page générée'
18
+ on_host: 'sur'
19
+ on_date: 'le'
20
+ error:
21
+ http: 'Erreur'
22
+ http_400: {short: 'Requête malformée', full: 'La syntaxe de la requête est erronée.'}
23
+ http_401: {short: 'Authentification requise', full: 'La page demandée requiert une authentification.'}
24
+ http_402: {short: 'Paiement requis', full: 'La page demandée requiert un paiement.'}
25
+ http_403: {short: 'Accès interdit', full: 'L''accès à la page demandée est interdit.'}
26
+ http_404: {short: 'Page non trouvée', full: 'La page demandée n''existe pas.'}
27
+ http_405: {short: 'Méthode non autorisée', full: 'La méthode de requête n''est pas autorisée.'}
28
+ http_406: {short: 'Page non disponible', full: 'La page demandée n''est pas disponible.'}
29
+ http_407: {short: 'Authentification avec le proxy requise', full: 'La page demandée requiert une authentification avec le proxy.'}
30
+ http_408: {short: 'Requête expirée', full: 'Le client n''a pas produit de requête dans le délai que le serveur était prêt à attendre.'}
31
+ http_409: {short: 'Conflit', full: 'La page demandée n''est pas disponible.'}
32
+ http_410: {short: 'Page déplacée', full: 'La page demandée n''est plus disponible et aucune adresse de redirection n''est connue.'}
33
+ http_411: {short: 'Requête malformée', full: 'La longueur de la requête n''a pas été précisée.'}
34
+ http_412: {short: 'Précondition non vérifiée', full: 'La requête n''a pas pu être traitée par le server.'}
35
+ http_413: {short: 'Requête trop grande', full: 'La requête n''a pas pu être traitée par le server.'}
36
+ http_414: {short: 'URI trop longue', full: 'La requête n''a pas pu être traitée par le server.'}
37
+ http_415: {short: 'Format de requête non supporté', full: 'La requête n''a pas pu être traitée par le server.'}
38
+ http_416: {short: 'Requête incorrecte', full: 'La requête n''a pas pu être traitée par le server.'}
39
+ http_417: {short: 'Requête incorrecte', full: 'La requête n''a pas pu être traitée par le server.'}
40
+ http_500: {short: 'Erreur interne du serveur', full: 'Le serveur a rencontré une erreur et est incapable de poursuivre le traitement de la requête.'}
41
+ http_501: {short: 'Fonction non supportée', full: 'La fonctionnalité requise n''est pas supportée par le serveur.'}
42
+ http_502: {short: 'Réponse invalide', full: 'Le serveur a reçu une réponse invalide depuis le serveur distant.'}
43
+ http_503: {short: 'Service indisponible', full: 'Le serveur est temporairement indisponible.'}
44
+ http_504: {short: 'Délai d''attente de la réponse dépassé', full: 'Le serveur distant n''a pas répondu dans le délai attendu.'}
45
+ http_505: {short: 'Version du protocole HTTP non supportée', full: 'Le serveur ne gère pas la version demandée du protocole HTTP.'}
46
+ load: 'Erreur de chargement !'
@@ -0,0 +1,25 @@
1
+ /**
2
+ * nav.js
3
+ * JavaScript functions for the main navigation menu (used on small screens)
4
+ * and for the modal box.
5
+ */
6
+
7
+
8
+ function openNavMenu() {
9
+ document.querySelectorAll('header nav')[0].style.width = 'auto';
10
+ }
11
+
12
+ function closeNavMenu() {
13
+ document.querySelectorAll('header nav')[0].style.width = '0';
14
+ }
15
+
16
+ function openModal() {
17
+ document.getElementById('modal').style.display = 'block';
18
+ }
19
+
20
+ window.onclick = function(event) {
21
+ if (event.target == document.getElementById('modal')) {
22
+ document.getElementById('modal').style.display = 'none';
23
+ }
24
+ }
25
+
@@ -0,0 +1,306 @@
1
+ /**
2
+ * style.css
3
+ * Design for the IntraNet core.
4
+ */
5
+
6
+ /******************************* External Fonts *******************************/
7
+
8
+ @font-face {
9
+ font-family: "Source Sans Pro";
10
+ font-style: normal;
11
+ font-weight: 300;
12
+ src: url("fonts/SourceSansPro.woff2") format("woff2");
13
+ unicode-range: U+0-FF, U+131, U+152-153, U+2C6, U+2DA, U+2DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215;
14
+ }
15
+
16
+ /******************************* General design *******************************/
17
+
18
+ body, html {
19
+ padding: 0px;
20
+ margin: 0px;
21
+ height: 100%;
22
+ }
23
+ body {
24
+ background: #1e262b url('background.jpg') fixed top right no-repeat;
25
+ font-family: "Source Sans Pro", Cantarell, sans-serif;
26
+ color: black;
27
+ }
28
+ /* Mobile devices only */
29
+ @media only screen and (max-width: 600px), only screen and (max-device-width: 600px) {
30
+ body {
31
+ font-size: 180%;
32
+ }
33
+ }
34
+
35
+ a {
36
+ color: inherit;
37
+ text-decoration: inherit;
38
+ cursor: pointer;
39
+ }
40
+
41
+ /********************************* Navigation *********************************/
42
+
43
+ header {
44
+ position: fixed;
45
+ top: 0px;
46
+ width: 100%;
47
+ margin: auto;
48
+ height: 57px;
49
+ background-color: #1e262b;
50
+ color: white;
51
+ z-index: 2;
52
+ }
53
+ header h1 {
54
+ float: left;
55
+ font-size: 225%;
56
+ font-weight: 500;
57
+ margin: 0px;
58
+ padding: 5px 20px 8px; /* top sides bottom */
59
+ }
60
+ header nav {
61
+ float: right;
62
+ padding: 0px 20px;
63
+ }
64
+ header nav ul {
65
+ margin: 0px;
66
+ padding: 0px;
67
+ }
68
+ header nav > ul > li {
69
+ display: inline-block;
70
+ }
71
+ header nav > ul > li a, header a#openmenu, header a#closemenu {
72
+ display: block;
73
+ padding: 16px 20px;
74
+ margin: 0px;
75
+ font-size: 125%;
76
+ color: rgba(255, 255, 255, 0.7);
77
+ }
78
+ header nav > ul > li a:hover {
79
+ color: rgba(255, 255, 255, 1.0);
80
+ }
81
+
82
+ /* dropdown menus */
83
+ header nav > ul > li > ul {
84
+ display: none;
85
+ background-color: #1e262b;
86
+ position: absolute;
87
+ }
88
+ header nav > ul > li > ul > li {
89
+ display: block;
90
+ }
91
+ header nav > ul > li > ul > li a {
92
+ padding: 8px 20px;
93
+ }
94
+ header nav > ul > li:hover > ul, header nav > ul > li:active > ul {
95
+ display: block;
96
+ }
97
+
98
+ header a#openmenu, header a#closemenu {
99
+ display: none;
100
+ }
101
+
102
+ /* Mobile devices only */
103
+ @media only screen and (max-width: 600px), only screen and (max-device-width: 600px) {
104
+ header {
105
+ height: 100px;
106
+ }
107
+ header a#openmenu {
108
+ display: block;
109
+ position: absolute;
110
+ top: 0;
111
+ left: 0;
112
+ font-size: 190%;
113
+ font-weight: bold;
114
+ color: white;
115
+ }
116
+ header nav a#closemenu {
117
+ display: block;
118
+ font-size: 250%;
119
+ font-weight: bold;
120
+ color: white;
121
+ text-align: right;
122
+ margin-right: 0.5em;
123
+ }
124
+ header h1 {
125
+ float: none;
126
+ text-align: center;
127
+ }
128
+ header nav {
129
+ position: fixed;
130
+ top: 0;
131
+ left: 0;
132
+ height: 100%;
133
+ padding: 0;
134
+ z-index: 1;
135
+ background: inherit;
136
+ font-size: 125%;
137
+ overflow-y: scroll;
138
+ width: 0;
139
+ max-width: 100%;
140
+ }
141
+ header nav > ul {
142
+ height: auto;
143
+ margin: 0px 0px 50px; /* top sides bottom */
144
+ }
145
+ header nav > ul > li {
146
+ display: block;
147
+ }
148
+ header nav > ul > li a {
149
+ color: rgba(255, 255, 255, 1.0);
150
+ margin-top: 35px;
151
+ padding: 10px 150px 10px 40px; /* top right bottom left */
152
+ }
153
+ header nav > ul > li > ul {
154
+ display: block;
155
+ position: static; /* reset absolute positionning */
156
+ }
157
+ header nav > ul > li > ul > li a {
158
+ color: rgba(255, 255, 255, 0.7);
159
+ margin: 0px;
160
+ padding: 10px 100px 10px 90px; /* top right bottom left */
161
+ }
162
+ header nav li a:hover {
163
+ color: inherit;
164
+ background-color: #c2c2c2;
165
+ }
166
+ }
167
+
168
+ /* breadcrump navigation menu */
169
+ ul.breadcrumb {
170
+ list-style: none;
171
+ padding: 0px;
172
+ font-size: 90%;
173
+ margin-bottom: 50px;
174
+ }
175
+ ul.breadcrumb li {
176
+ display: inline;
177
+ }
178
+ ul.breadcrumb li+li:before {
179
+ padding: 4px;
180
+ content: "/\00a0";
181
+ }
182
+ ul.breadcrumb li a, main article a {
183
+ color: #748ea3;
184
+ }
185
+
186
+ /* Mobile devices only */
187
+ @media only screen and (max-width: 600px), only screen and (max-device-width: 600px) {
188
+ ul.breadcrumb {
189
+ display: none;
190
+ }
191
+ }
192
+
193
+ /******************************** Main content *******************************/
194
+
195
+ body > aside {
196
+ height: 45%;
197
+ min-height: 350px;
198
+ max-height: 500px; /* less than height of background.jpg */
199
+ }
200
+
201
+ body > main {
202
+ background: #f2f2f2;
203
+ padding: 57px 0px; /* height of the navigation bar */
204
+ min-height: 500px; /* lower or equal to height of background.jpg (with padding) */
205
+ }
206
+
207
+ body > main section, body > main article, body > main hr, body > footer p {
208
+ width: 75%;
209
+ max-width: 1200px;
210
+ margin: auto;
211
+ }
212
+ body > main hr {
213
+ height: 2px;
214
+ border: 0px;
215
+ background: #1e262b;
216
+ margin-top: 3em;
217
+ margin-bottom: 3em;
218
+ }
219
+
220
+ /* Mobile devices only */
221
+ @media only screen and (max-width: 600px), only screen and (max-device-width: 600px) {
222
+ body > main {
223
+ padding: 100px 0px;
224
+ }
225
+ body > main section, main hr, footer p {
226
+ width: 90%;
227
+ }
228
+ }
229
+
230
+ body > main section h2 {
231
+ font-size: 200%;
232
+ text-align: center;
233
+ margin: 50px 0px 2em; /* top sides bottom */
234
+ }
235
+ body > main article h2 {
236
+ margin-bottom: 40px;
237
+ }
238
+ body > main section h3 {
239
+ margin: 3em 0px 1em; /* top sides bottom */
240
+ }
241
+
242
+ body > main div.loading {
243
+ width: 100px;
244
+ height: 100px;
245
+ margin: auto;
246
+ border: 16px solid #eaeaea;
247
+ border-radius: 50%;
248
+ border-top: 16px solid #a0c5e2;
249
+ animation: spin 2s linear infinite;
250
+ }
251
+ @keyframes spin { /* animation definition */
252
+ 0% { transform: rotate(0deg); }
253
+ 100% { transform: rotate(360deg); }
254
+ }
255
+
256
+ body > footer {
257
+ background: #1e262b;
258
+ text-align: center;
259
+ color: white;
260
+ padding: 15px 0px; /* top sides */
261
+ font-size: 10pt;
262
+ }
263
+ body > footer aside#modal { /* modal box container */
264
+ display: none;
265
+ position: fixed;
266
+ z-index: 10;
267
+ left: 0;
268
+ top: 0;
269
+ width: 100%;
270
+ height: 100%;
271
+ overflow: auto;
272
+ color: black;
273
+ background-color: rgba(0, 0, 0, 0.8);
274
+ }
275
+ body > footer aside#modal #modal-content { /* modal box */
276
+ background-color: #f2f2f2;
277
+ margin: 15% auto;
278
+ padding: 20px 25px 40px; /* top sides bottom */
279
+ border: 1px solid #1e262b;
280
+ width: 400px;
281
+ font-size: 125%;
282
+ }
283
+ /* Mobile devices only */
284
+ @media only screen and (max-width: 600px), only screen and (max-device-width: 600px) {
285
+ body > footer {
286
+ font-size: 13pt;
287
+ }
288
+ }
289
+
290
+
291
+ /********************************* Error pages *********************************/
292
+
293
+ section#error h2 {
294
+ color: #ff3333;
295
+ }
296
+ section#error p {
297
+ text-align: center;
298
+ }
299
+ section#error p img {
300
+ width: 25%;
301
+ margin-bottom: 3em;
302
+ }
303
+ section#error a {
304
+ color: #748ea3;
305
+ }
306
+